diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..a762a40 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,41 @@ +name: Deploy GitHub Pages + +on: + push: + branches: [ "main" ] + paths: + - 'docs/**' + - '.github/workflows/deploy-pages.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-24.04 + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'docs' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + diff --git a/.gitignore b/.gitignore index 0abd1dc..904970c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,3 @@ *.log .md !README.md -docs/ \ No newline at end of file diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/.nojekyll @@ -0,0 +1 @@ + diff --git a/docs/app.js b/docs/app.js new file mode 100644 index 0000000..1476828 --- /dev/null +++ b/docs/app.js @@ -0,0 +1,308 @@ +/** + * DailyCode Discord Bot - Landing Page & Permissions Logic + */ + +const BOT_CLIENT_ID = "1559164347085885511"; + +// Discord Permission Bit Flags (BigInt to handle 64-bit integer values safely) +const PERMISSIONS = { + // General + VIEW_CHANNEL: 1024n, // 0x400 + READ_MESSAGE_HISTORY: 65536n, // 0x10000 + MANAGE_CHANNELS: 16n, // 0x10 + MANAGE_ROLES: 268435456n, // 0x10000000 + ADMINISTRATOR: 8n, // 0x8 + + // Text / Messaging + SEND_MESSAGES: 2048n, // 0x800 + SEND_MESSAGES_IN_THREADS: 274877906944n, // 0x4000000000 + EMBED_LINKS: 16384n, // 0x4000 + ATTACH_FILES: 32768n, // 0x8000 + ADD_REACTIONS: 64n, // 0x40 + USE_EXTERNAL_EMOJIS: 262144n, // 0x40000 + MENTION_EVERYONE: 131072n, // 0x20000 +}; + +// Permission Presets +const PRESETS = { + recommended: [ + "VIEW_CHANNEL", + "READ_MESSAGE_HISTORY", + "SEND_MESSAGES", + "SEND_MESSAGES_IN_THREADS", + "EMBED_LINKS", + "ATTACH_FILES", + "ADD_REACTIONS", + "USE_EXTERNAL_EMOJIS", + "MENTION_EVERYONE" + ], + minimal: [ + "VIEW_CHANNEL", + "READ_MESSAGE_HISTORY", + "SEND_MESSAGES", + "EMBED_LINKS" + ], + admin: [ + "ADMINISTRATOR" + ] +}; + +document.addEventListener("DOMContentLoaded", () => { + initPermissionsCalculator(); + initCommandFilters(); + initCopyButtons(); + initFaqAccordion(); + initMobileMenu(); + initMockupInteractivity(); +}); + +/** + * Initializes the Discord permissions calculator + */ +function initPermissionsCalculator() { + const checkboxes = document.querySelectorAll(".perm-check"); + const permValueEl = document.getElementById("perm-integer"); + const inviteBtn = document.getElementById("custom-invite-btn"); + const copyInviteBtn = document.getElementById("copy-invite-btn"); + const presetBtns = document.querySelectorAll(".preset-btn"); + + if (!checkboxes.length || !permValueEl) return; + + function calculatePermissions() { + let bitmask = 0n; + checkboxes.forEach((cb) => { + if (cb.checked && PERMISSIONS[cb.value]) { + bitmask |= PERMISSIONS[cb.value]; + } + }); + + const bitmaskStr = bitmask.toString(); + permValueEl.textContent = bitmaskStr; + + const oauthUrl = `https://discord.com/oauth2/authorize?client_id=${BOT_CLIENT_ID}&permissions=${bitmaskStr}&scope=bot%20applications.commands`; + + if (inviteBtn) { + inviteBtn.href = oauthUrl; + } + + // Also update any primary CTA add-bot buttons that don't have hardcoded links + document.querySelectorAll(".dynamic-invite-link").forEach((link) => { + link.href = oauthUrl; + }); + + return oauthUrl; + } + + // Checkbox change listener + checkboxes.forEach((cb) => { + cb.addEventListener("change", () => { + // Clear active preset highlight if modified manually + presetBtns.forEach((b) => b.classList.remove("active")); + calculatePermissions(); + }); + }); + + // Preset button clicks + presetBtns.forEach((btn) => { + btn.addEventListener("click", () => { + const presetName = btn.dataset.preset; + const targetPerms = PRESETS[presetName] || []; + + presetBtns.forEach((b) => b.classList.remove("active")); + btn.classList.add("active"); + + checkboxes.forEach((cb) => { + cb.checked = targetPerms.includes(cb.value); + }); + + calculatePermissions(); + }); + }); + + // Copy customized invite URL + if (copyInviteBtn) { + copyInviteBtn.addEventListener("click", () => { + const url = calculatePermissions(); + navigator.clipboard.writeText(url).then(() => { + showToast("Invite URL copied to clipboard!"); + }).catch(() => { + // Fallback + showToast("Link ready: " + url.substring(0, 35) + "..."); + }); + }); + } + + // Initial calculation + calculatePermissions(); +} + +/** + * Slash commands category filtering + */ +function initCommandFilters() { + const filterBtns = document.querySelectorAll(".filter-btn"); + const commandCards = document.querySelectorAll(".command-card"); + + filterBtns.forEach((btn) => { + btn.addEventListener("click", () => { + filterBtns.forEach((b) => b.classList.remove("active")); + btn.classList.add("active"); + + const category = btn.dataset.category; + + commandCards.forEach((card) => { + if (category === "all" || card.dataset.category === category) { + card.style.display = "flex"; + } else { + card.style.display = "none"; + } + }); + }); + }); +} + +/** + * Click-to-copy handler (Brutalist) + */ +function initCopyButtons() { + document.querySelectorAll(".copy-btn").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + const textToCopy = btn.dataset.copy; + if (!textToCopy) return; + + navigator.clipboard.writeText(textToCopy).then(() => { + showToast(`COPIED: "${textToCopy}"`); + const originalContent = btn.innerHTML; + btn.innerHTML = `[✓ COPIED]`; + btn.style.background = "#FF8800"; + btn.style.color = "#000000"; + setTimeout(() => { + btn.innerHTML = originalContent; + btn.style.background = ""; + btn.style.color = ""; + }, 1500); + }); + }); + }); +} + +/** + * FAQ accordion toggle (Brutalist [+] / [-]) + */ +function initFaqAccordion() { + const faqItems = document.querySelectorAll(".faq-item"); + + faqItems.forEach((item) => { + const questionBtn = item.querySelector(".faq-question"); + const answer = item.querySelector(".faq-answer"); + const icon = item.querySelector(".faq-toggle-icon"); + + if (!questionBtn || !answer) return; + + questionBtn.addEventListener("click", () => { + const isOpen = item.classList.contains("active"); + + // Close all other items + faqItems.forEach((other) => { + if (other !== item) { + other.classList.remove("active"); + const otherAns = other.querySelector(".faq-answer"); + const otherIcon = other.querySelector(".faq-toggle-icon"); + if (otherAns) otherAns.style.maxHeight = null; + if (otherIcon) otherIcon.textContent = "[+]"; + } + }); + + // Toggle current + if (isOpen) { + item.classList.remove("active"); + answer.style.maxHeight = null; + if (icon) icon.textContent = "[+]"; + } else { + item.classList.add("active"); + answer.style.maxHeight = answer.scrollHeight + 30 + "px"; + if (icon) icon.textContent = "[−]"; + } + }); + }); +} + +/** + * Mobile navigation toggle + */ +function initMobileMenu() { + const toggle = document.querySelector(".mobile-toggle"); + const menu = document.querySelector(".nav-menu"); + + if (!toggle || !menu) return; + + toggle.addEventListener("click", () => { + menu.classList.toggle("open"); + }); + + menu.querySelectorAll("a").forEach((link) => { + link.addEventListener("click", () => { + menu.classList.remove("open"); + }); + }); +} + +/** + * Mockup interactivity (fun responsive feedback for visitors) + */ +function initMockupInteractivity() { + const joinBtn = document.getElementById("mockup-join-btn"); + const statusBtn = document.getElementById("mockup-status-btn"); + const countSpan = document.getElementById("mockup-joined-count"); + + if (joinBtn && countSpan) { + let joined = false; + let baseCount = 14; + + joinBtn.addEventListener("click", () => { + joined = !joined; + if (joined) { + countSpan.textContent = `${baseCount + 1} Coders`; + joinBtn.textContent = "✓ Joined!"; + joinBtn.style.background = "#248046"; + showToast("Simulated: You joined today's challenge!"); + } else { + countSpan.textContent = `${baseCount} Coders`; + joinBtn.textContent = "Join Challenge"; + joinBtn.style.background = ""; + } + }); + } + + if (statusBtn) { + statusBtn.addEventListener("click", () => { + showToast("Triggered: /challenge status command preview"); + }); + } +} + +/** + * Displays a brutalist toast notification + */ +function showToast(message) { + let toast = document.querySelector(".toast"); + if (!toast) { + toast = document.createElement("div"); + toast.className = "toast"; + document.body.appendChild(toast); + } + + toast.innerHTML = ` + [OK] + ${message} + `; + + toast.classList.add("show"); + + clearTimeout(window.toastTimer); + window.toastTimer = setTimeout(() => { + toast.classList.remove("show"); + }, 2200); +} + diff --git a/docs/assets/banner.png b/docs/assets/banner.png new file mode 100644 index 0000000..0cdb0bd Binary files /dev/null and b/docs/assets/banner.png differ diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000..08823ba Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/challenge-automation.md b/docs/challenge-automation.md new file mode 100644 index 0000000..739fe6d --- /dev/null +++ b/docs/challenge-automation.md @@ -0,0 +1,53 @@ +# Daily posting time + +Server managers can choose when the bot posts each day's problem: + +```text +/challenge automate id:1 channel:#daily-challenges time:00:00 +``` + +`00:00` is 12 AM (midnight). Use `12:00` for noon or `18:30` for 6:30 PM. +The optional `time` uses 24-hour `HH:mm` format and the challenge's timezone, +which is set when creating the challenge and defaults to `Africa/Casablanca`. +Difficulty remains optional and defaults to Easy. + +Omitting `time` selects `00:00`. Existing automations also default to midnight +after upgrading. Repeat `/challenge automate` with the channel and new time to +change the schedule. Include the desired difficulty when reconfiguring. +`/challenge status id:1` shows the saved posting time and timezone. + +The bot checks for due assignments about every minute by default. Posting happens +on the first check at or after the selected time, so it is not guaranteed to occur +at the exact second. If enabled or restarted after that time, it catches up with +today's problem. It does not backfill older days or repost an announcement already +attempted for today. Changing the time does not create a second problem that day. + +When a due assignment has no unused free problem at its selected difficulty, +the bot browses LeetCode's catalog and imports a new problem automatically, then +retries the assignment. No manual problem loading is required. Catalog requests +run outside assignment transactions, and the retry rechecks the current schedule +and automation settings before posting. + +Each fetch attempt scans at most three pages of 50 entries and makes at most three +metadata requests. Scans continue on later checks when necessary. Upstream or +import failures wait five minutes before retrying. If the catalog contains no new +free problems at that difficulty, it checks again after six hours. Imported +problems persist across restarts; the catalog cursor restarts from the beginning. +These retries may delay a post, and the bot still catches up only for the current +challenge day. It never changes the requested difficulty or repeats a problem +within a challenge to fill a gap. + +The schedule follows local time when timezone offsets change. If daylight saving +skips a selected time, that day's posting time shifts forward by the gap; if the +time occurs twice, the first occurrence is used. Each day still has one assignment. + +The deadline remains the next midnight in the challenge timezone, including when +posting later in the day. Submission checks still run about every five minutes. +`/challenge stop-auto id:1` disables daily posting and automatic submission checks. + +Rebuild and restart the deployed bot to apply migration V10 and register the new +Discord option: + +```bash +docker compose up -d --build bot +``` diff --git a/docs/challenge-status.md b/docs/challenge-status.md new file mode 100644 index 0000000..974fcb9 --- /dev/null +++ b/docs/challenge-status.md @@ -0,0 +1,30 @@ +# Challenge status + +Use `/challenge status id:1` to view your saved progress in a challenge you have +joined. The response is private and includes: + +- Total recorded completions. +- Today's status in the challenge's timezone. +- Whether automation is enabled, with its daily posting time and timezone. +- Five assigned days per page, newest first, with problem titles and completion status. + +Use `/challenge status id:1 page:2` to see older assignments. Page defaults to 1. +History remains accessible after the challenge ends or is cancelled. + +“No completion recorded” means the database has no verified completion; it does +not prove that you missed the problem. Days whose deadline preceded your joining +are labelled “Before you joined.” Dates without assignments are not history rows. + +This command does not fetch submissions or change progress. Use +`/challenge check id:1` to check today's submissions, or include `date:YYYY-MM-DD` +for an earlier assignment. + +After bringing this branch's changes onto OCI, rebuild and restart the bot: + +```bash +docker compose up -d --build bot +``` + +The bot registers the subcommand when it connects to Discord. The daily posting +time is stored by migration V10, which runs automatically at startup. See +[daily posting time](challenge-automation.md) for scheduling options. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..2d87b3f --- /dev/null +++ b/docs/index.html @@ -0,0 +1,807 @@ + + + + + + DailyCode — Brutalist LeetCode Discord Bot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ // AUTOMATED DAILY CHALLENGES  •  0 PASSWORDS (BIO VERIFICATION)  •  5-MINUTE AUTO POLL  •  GUILD LEADERBOARDS  •  DEDUPLICATION ENGINE  •  ROLE PINGS  •  100% FREE & OPEN SOURCE  •  // AUTOMATED DAILY CHALLENGES  •  0 PASSWORDS (BIO VERIFICATION)  •  5-MINUTE AUTO POLL  •  GUILD LEADERBOARDS  •  DEDUPLICATION ENGINE  •  ROLE PINGS  •  100% FREE & OPEN SOURCE  •  +
+
+ + +
+
+ +
+ + + STATUS: ONLINE + + LEETCODE API: SYNCED + SPRING BOOT 3 +
+ +
+ +
+

+ Daily LeetCode.
+ Directly in Discord. +

+ +

+ Stop pasting problem links manually. DailyCode automates daily problem drops, verifies participant accounts using profile bio tokens without passwords, checks solutions every 5 minutes, and tracks server leaderboards. +

+ + +
+ + +
+
+
+ # daily-coding-challenges +
+ + + +
+
+ +
+
+
+ Bot Avatar +
+
+ DailyCode + BOT +
+
+ +
@DailyCoders 🚀 Today's Drop is Live!
+ +
+
+ #15. 3Sum + Medium +
+ +

+ Given an integer array nums, return all triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, j != k, and nums[i] + nums[j] + nums[k] == 0. +

+ +
+
+
Active Coders
+
14 Joined
+
+
+
Sync Rate
+
Every 5 Minutes
+
+
+
Tags
+
Array, Two Pointers
+
+
+
Verification
+
Auto-Poll
+
+
+ +
+ + + [ LeetCode ↗ ] +
+
+
+
+
+
+ +
+
+ + +
+
+
100%
+
Free & Open Source
+
+
+
0
+
Passwords Needed
+
+
+
5 MIN
+
Auto Solution Polling
+
+
+
JAVA 21
+
Spring Boot 3 + Postgres
+
+
+ + +
+
+
+ // ARCHITECTURE +

Engineered for Consistency

+

No fluff, no bloat. Pure functionality designed to keep developer communities active.

+
+ +
+ +
+
+
+ 01 + CRON SCHEDULER +
+

Automated Daily Drops

+

+ Schedule daily drops at any time using /challenge automate time:09:00 channel:#daily role:@Coders. The bot handles question selection and alerts automatically. +

+
+
> Fully Hands-Off
+
+ + +
+
+
+ 02 + BIO VERIFY +
+

Zero Password Risk

+

+ Connect your LeetCode handle with /leetcode connect, paste a short security token into your public bio summary, and verify in seconds with /leetcode verify. +

+
+
> No Credentials Shared
+
+ + +
+
+
+ 03 + AUTO POLL +
+

5-Minute Auto-Check

+

+ Participants don't need to manually post solutions. DailyCode runs a periodic background check that inspects recent submissions and marks completions automatically. +

+
+
> Instant Verification
+
+ + +
+
+
+ 04 + STANDINGS +
+

Server Leaderboard

+

+ Run /challenge leaderboard to see server rankings, streaks, and total solved counts. Friendly competition drives habitual problem solving. +

+
+
> Live Guild Rankings
+
+ + +
+
+
+ 05 + DEDUPLICATION +
+

No Repeat Problems

+

+ The bot tracks historical drops per guild and avoids repeated problems, ensuring members get diverse algorithm problems across arrays, trees, dynamic programming, and graphs. +

+
+
> Unique Catalog Drops
+
+ + +
+
+
+ 06 + PINGS +
+

Targeted Role Mentions

+

+ Avoid server-wide noise. Specify a role like @DailyCoders so only opted-in members receive notifications when the daily problem drops. +

+
+
> Clean Server Comms
+
+
+
+
+ + +
+
+
+ // SETUP GUIDE +

Ready in 3 Commands

+

No external web dashboards. Everything operates through Discord native slash commands.

+
+ +
+
+
01
+

Invite Bot

+

Authorize DailyCode on your server with permissions to post embeds in your chosen channel.

+
> [ 1-Click OAuth2 Authorize ]
+
+ +
+
02
+

Set Automation

+

Configure the daily drop time, target channel, and optional role ping.

+
/challenge automate time:09:00 channel:#daily
+
+ +
+
03
+

Connect & Solve

+

Members connect their LeetCode account with a bio token and start solving daily.

+
/leetcode connect username:you
+
+
+
+
+ + +
+
+
+ // CLI REFERENCE +

Slash Commands

+

Click the copy button on any command to copy its syntax directly.

+
+ +
+ + + + +
+ +
+ +
+
+
+ /leetcode connect + +
+

Binds your Discord handle to your LeetCode username and provides a temporary verification token.

+
+
+ username: string +
+
+ + +
+
+
+ /leetcode verify + +
+

Verifies that the generated token is present in your public LeetCode profile bio and finalizes connection.

+
+
+ no arguments +
+
+ + +
+
+
+ /challenge automate + +
+

Schedules recurring daily challenge drops in a specific channel at a chosen hour (HH:mm) with optional role ping.

+
+
+ time: HH:mm + channel: #channel + role: @role (optional) +
+
+ + +
+
+
+ /challenge stop-auto + +
+

Deactivates automated daily challenge drops for the current server.

+
+
+ no arguments +
+
+ + +
+
+
+ /challenge create + +
+

Manually creates a new coding challenge for the server with a target number of problems.

+
+
+ name: string + target: integer +
+
+ + +
+
+
+ /challenge join + +
+

Enrolls your linked LeetCode handle into the active server challenge.

+
+
+ challenge_id: int (optional) +
+
+ + +
+
+
+ /challenge today + +
+

Displays today's active LeetCode question, difficulty, description snippet, tags, and problem link.

+
+
+ no arguments +
+
+ + +
+
+
+ /challenge status + +
+

Shows paginated participant progress, completion status, and active participants for the challenge.

+
+
+ challenge_id: int (optional) +
+
+ + +
+
+
+ /challenge check + +
+

Triggers an immediate verification check for participants without waiting for the 5-minute background cron.

+
+
+ no arguments +
+
+ + +
+
+
+ /challenge leaderboard + +
+

Displays the server leaderboard ranking users by completed challenges, streaks, and solve rate.

+
+
+ no arguments +
+
+ +
+
+
+ + +
+
+
+ // PERMISSION MATRIX +

Discord Permission Calculator

+

Fine-tune the exact OAuth2 permissions bitmask to invite DailyCode with.

+
+ +
+
+ Presets: + + + +
+ +
+ +
+
// GENERAL
+
+ + + + + +
+
+ + +
+
// TEXT & MESSAGING
+
+ + + + + + + +
+
+ + +
+
// NOTIFICATIONS
+
+ + + + + + + +
+
+
+ +
+
+ // CALCULATED PERMISSION INTEGER + 274878486976 +
+ +
+ + + + INVITE WITH SELECTED PERMS ↗ + +
+
+
+
+
+ + +
+
+
+ // INQUIRIES +

Frequently Asked Questions

+

Common questions regarding security, polling, and hosting.

+
+ +
+
+ +
+ Yes. DailyCode is 100% free under the MIT license. You can invite the hosted bot to your server or self-host your own container instance backed by PostgreSQL. +
+
+ +
+ +
+ Never. DailyCode uses a secure bio token verification flow. The bot generates a temporary alphanumeric token. You paste it into your public LeetCode profile bio summary, run /leetcode verify, and remove it immediately after. +
+
+ +
+ +
+ An internal scheduled background task polls LeetCode's public recent accepted submissions every 5 minutes. If a participant solves the active question, the bot marks their challenge status as solved. You can also trigger an immediate check with /challenge check. +
+
+ +
+ +
+ Yes. Every server maintains isolated challenge configurations, dedicated channels, deduplicated problem history, and distinct guild leaderboards. +
+
+ +
+ +
+ DailyCode is a Spring Boot 3 Java application. Clone the repository, provide your DISCORD_TOKEN and DATABASE_URL in environment variables, and run with Docker (docker compose up) or Maven (mvn spring-boot:run). +
+
+
+
+
+ + +
+
+
+
+

Level Up Your Server's Coding Habit

+

Add DailyCode to your server in 30 seconds and let automated problems fuel daily practice.

+
+
+ + + Add to Discord ↗ + + + Star on GitHub ★ + +
+
+
+
+ + + + + + + diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..30a8162 --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,1199 @@ +/* ======================================================== + DAILYCODE — BRUTALIST DARK DEVELOPER THEME + No blur. No fake glow. High contrast, sharp edges, hard shadows. + Color scheme strictly inspired by logo: #080808, #FF5E00, #FF8800, #FFA116, #FFFFFF + ======================================================== */ + +:root { + --bg-page: #080809; + --bg-panel: #101013; + --bg-panel-hover: #16161a; + --bg-panel-contrast: #1a1a20; + + --border-muted: #24242b; + --border-strong: #3e3e48; + --border-accent: #ff8800; + --border-white: #ffffff; + + --accent-orange: #ff5e00; + --accent-amber: #ff8800; + --accent-gold: #ffa116; + --accent-yellow: #ffc01e; + + --text-main: #f5f5f7; + --text-dim: #9b9ba8; + --text-muted: #626270; + --text-inverse: #080809; + + --discord-blurple: #5865f2; + + --font-display: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; + + --shadow-black-sm: 3px 3px 0px #000000; + --shadow-black: 5px 5px 0px #000000; + --shadow-accent-sm: 3px 3px 0px #ff8800; + --shadow-accent: 5px 5px 0px #ff8800; + --shadow-white-sm: 3px 3px 0px #ffffff; + --shadow-white: 5px 5px 0px #ffffff; +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; + font-family: var(--font-display); + background-color: var(--bg-page); + color: var(--text-main); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +body { + position: relative; + overflow-x: hidden; + min-height: 100vh; + background-color: var(--bg-page); +} + +/* Subtle Stark Technical Grid - Behind content, above background */ +.grid-overlay { + position: fixed; + inset: 0; + background-image: + linear-gradient(to right, #1c1c24 1px, transparent 1px), + linear-gradient(to bottom, #1c1c24 1px, transparent 1px); + background-size: 40px 40px; + pointer-events: none; + z-index: 1; +} + +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + position: relative; + z-index: 2; +} + +/* Typography & Headings */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-display); + font-weight: 800; + letter-spacing: -0.03em; + text-transform: uppercase; + color: var(--text-main); +} + +.mono-label { + font-family: var(--font-mono); + font-size: 0.8rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +a { + color: inherit; + text-decoration: none; +} + +.text-orange { + color: var(--accent-amber); +} + +/* Navbar */ +.navbar { + position: sticky; + top: 0; + background: var(--bg-page); + border-bottom: 2px solid var(--border-strong); + height: 72px; + z-index: 100; +} + +.nav-container { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; +} + +.brand-wrap { + display: flex; + align-items: center; + gap: 14px; +} + +.brand-logo-frame { + width: 40px; + height: 40px; + border: 2px solid var(--accent-amber); + background: #000; + box-shadow: 3px 3px 0px var(--accent-orange); + display: flex; + align-items: center; + justify-content: center; +} + +.brand-logo { + width: 32px; + height: 32px; + display: block; +} + +.brand-title { + font-size: 1.35rem; + font-weight: 900; + letter-spacing: -0.02em; +} + +.brand-badge { + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 700; + padding: 3px 8px; + background: var(--accent-amber); + color: #000; + border: 1px solid #000; +} + +.nav-menu { + display: flex; + align-items: center; + gap: 28px; + list-style: none; +} + +.nav-menu a { + font-family: var(--font-mono); + font-size: 0.85rem; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + transition: color 0.15s ease; +} + +.nav-menu a:hover { + color: var(--accent-amber); +} + +.nav-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.mobile-toggle { + display: none; + background: #000; + border: 2px solid var(--border-strong); + color: var(--text-main); + padding: 8px 12px; + font-family: var(--font-mono); + font-size: 1.1rem; + cursor: pointer; +} + +/* Brutalist Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 0.88rem; + font-weight: 700; + text-transform: uppercase; + padding: 10px 20px; + border-radius: 0px; + cursor: pointer; + text-decoration: none; + transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease; +} + +.btn-primary { + background: var(--accent-amber); + color: #000000; + border: 2px solid #000000; + box-shadow: var(--shadow-white-sm); +} + +.btn-primary:hover { + background: var(--accent-gold); + transform: translate(-2px, -2px); + box-shadow: 5px 5px 0px #ffffff; +} + +.btn-primary:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 0px #ffffff; +} + +.btn-secondary { + background: var(--bg-panel); + color: var(--text-main); + border: 2px solid var(--border-strong); + box-shadow: var(--shadow-black-sm); +} + +.btn-secondary:hover { + background: var(--bg-panel-contrast); + border-color: var(--text-main); + transform: translate(-2px, -2px); + box-shadow: var(--shadow-white-sm); +} + +.btn-secondary:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 0px var(--text-main); +} + +.btn-accent { + background: #000000; + color: var(--accent-amber); + border: 2px solid var(--accent-amber); + box-shadow: var(--shadow-accent-sm); +} + +.btn-accent:hover { + background: var(--accent-amber); + color: #000000; + transform: translate(-2px, -2px); + box-shadow: var(--shadow-accent); +} + +.btn-accent:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 0px var(--accent-amber); +} + +.btn-sm { + padding: 6px 14px; + font-size: 0.8rem; +} + +.btn-lg { + padding: 14px 28px; + font-size: 1rem; + letter-spacing: 0.03em; +} + +.btn-icon { + width: 16px; + height: 16px; + fill: currentColor; + flex-shrink: 0; +} + +/* Ticker Bar */ +.ticker-bar { + position: relative; + z-index: 5; + background: var(--accent-amber); + color: #000000; + border-bottom: 2px solid #000000; + overflow: hidden; + white-space: nowrap; + padding: 8px 0; + font-family: var(--font-mono); + font-size: 0.82rem; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.ticker-track { + display: inline-block; + animation: ticker-slide 28s linear infinite; +} + +@keyframes ticker-slide { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} + +/* Hero Section */ +.hero-section { + padding: 70px 0 60px; + border-bottom: 2px solid var(--border-muted); +} + +.hero-status-strip { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 28px; + flex-wrap: wrap; +} + +.status-tag { + font-family: var(--font-mono); + font-size: 0.76rem; + font-weight: 700; + padding: 4px 10px; + background: #111115; + border: 1px solid var(--border-strong); + color: var(--text-dim); + display: inline-flex; + align-items: center; + gap: 8px; +} + +.status-dot { + width: 8px; + height: 8px; + background: var(--accent-amber); +} + +.hero-grid { + display: grid; + grid-template-columns: 1.15fr 0.85fr; + gap: 40px; + align-items: center; +} + +.hero-headline { + font-size: clamp(2.4rem, 4.8vw, 4.4rem); + font-weight: 900; + line-height: 1.05; + margin-bottom: 24px; + letter-spacing: -0.04em; +} + +.hero-desc { + font-size: 1.1rem; + color: var(--text-dim); + margin-bottom: 32px; + line-height: 1.6; + max-width: 580px; +} + +.hero-cta-row { + display: flex; + gap: 14px; + flex-wrap: wrap; +} + +/* Brutalist Discord / Terminal Mockup Card */ +.mockup-card { + background: var(--bg-panel); + border: 2px solid var(--border-strong); + box-shadow: 8px 8px 0px var(--accent-amber); + position: relative; +} + +.mockup-bar { + background: #181820; + border-bottom: 2px solid var(--border-strong); + padding: 10px 16px; + display: flex; + align-items: center; + justify-content: space-between; + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 700; + color: var(--text-dim); +} + +.mockup-dots { + display: flex; + gap: 6px; +} + +.mockup-dot { + width: 10px; + height: 10px; + background: var(--border-strong); +} + +.mockup-inner { + padding: 22px; +} + +.mockup-user-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.mockup-avatar-frame { + width: 38px; + height: 38px; + border: 2px solid var(--accent-amber); + background: #000; + flex-shrink: 0; +} + +.mockup-avatar-frame img { + width: 100%; + height: 100%; + display: block; +} + +.mockup-bot-name { + font-weight: 800; + font-size: 0.95rem; +} + +.mockup-bot-pill { + font-family: var(--font-mono); + background: var(--discord-blurple); + color: #fff; + font-size: 0.65rem; + font-weight: 800; + padding: 2px 5px; +} + +.mockup-role-tag { + font-family: var(--font-mono); + display: inline-block; + background: #1c1d2e; + color: #9aa5ff; + border: 1px solid #3c4485; + padding: 3px 8px; + font-size: 0.8rem; + font-weight: 700; + margin-bottom: 14px; +} + +/* Problem Box Inside Mockup */ +.mockup-problem-box { + border: 2px solid var(--border-strong); + border-left: 6px solid var(--accent-amber); + background: #0b0b0e; + padding: 16px; +} + +.mockup-problem-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + gap: 10px; +} + +.mockup-problem-title { + font-size: 1.05rem; + font-weight: 800; + color: #ffffff; +} + +.mockup-difficulty-badge { + font-family: var(--font-mono); + font-size: 0.7rem; + font-weight: 800; + text-transform: uppercase; + background: var(--accent-amber); + color: #000; + padding: 2px 7px; + border: 1px solid #000; +} + +.mockup-problem-desc { + font-size: 0.86rem; + color: var(--text-dim); + margin-bottom: 14px; + line-height: 1.5; +} + +.mockup-matrix { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + border-top: 1px solid var(--border-muted); + border-bottom: 1px solid var(--border-muted); + padding: 10px 0; + margin-bottom: 14px; +} + +.matrix-item { + font-family: var(--font-mono); + font-size: 0.76rem; +} + +.matrix-label { + color: var(--text-muted); + text-transform: uppercase; + font-size: 0.68rem; + margin-bottom: 2px; +} + +.matrix-val { + color: #ffffff; + font-weight: 700; +} + +.mockup-btns-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.mockup-action-btn { + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 800; + text-transform: uppercase; + padding: 6px 12px; + background: var(--bg-panel-contrast); + color: var(--text-main); + border: 1px solid var(--border-strong); + cursor: pointer; + transition: all 0.1s ease; +} + +.mockup-action-btn:hover { + background: var(--accent-amber); + color: #000; + border-color: #000; +} + +.mockup-action-btn.active { + background: var(--accent-amber); + color: #000; +} + +/* Stats Ribbon */ +.stats-strip { + display: grid; + grid-template-columns: repeat(4, 1fr); + border-bottom: 2px solid var(--border-muted); + position: relative; + z-index: 2; +} + +.stats-box { + padding: 28px 24px; + background: var(--bg-panel); + border-right: 2px solid var(--border-muted); + text-align: left; +} + +.stats-box:last-child { + border-right: none; +} + +.stats-num { + font-size: 2rem; + font-weight: 900; + color: var(--accent-amber); + margin-bottom: 4px; + line-height: 1; +} + +.stats-desc { + font-family: var(--font-mono); + font-size: 0.8rem; + text-transform: uppercase; + color: var(--text-dim); +} + +/* Sections Common */ +.section { + padding: 80px 0; + border-bottom: 2px solid var(--border-muted); +} + +.section-head { + margin-bottom: 44px; +} + +.section-subtitle { + color: var(--text-dim); + font-size: 1.05rem; + margin-top: 8px; + max-width: 650px; +} + +/* Features Stark Matrix */ +.features-matrix { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +.feature-box { + background: var(--bg-panel); + border: 2px solid var(--border-strong); + padding: 28px; + display: flex; + flex-direction: column; + justify-content: space-between; + box-shadow: var(--shadow-black); + transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; +} + +.feature-box:hover { + border-color: var(--accent-amber); + box-shadow: 6px 6px 0px var(--accent-amber); + transform: translate(-2px, -2px); +} + +.feature-box-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-muted); +} + +.feature-idx { + font-family: var(--font-mono); + font-size: 1.1rem; + font-weight: 900; + color: var(--accent-amber); +} + +.feature-box-title { + font-size: 1.25rem; + margin-bottom: 10px; +} + +.feature-box-desc { + font-size: 0.92rem; + color: var(--text-dim); + line-height: 1.6; + margin-bottom: 18px; +} + +.feature-tag { + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + padding: 3px 8px; + background: #000; + border: 1px solid var(--border-strong); + color: var(--accent-amber); + display: inline-block; + width: fit-content; +} + +/* 3 Steps Pipeline */ +.steps-matrix { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.step-box { + background: var(--bg-panel); + border: 2px solid var(--border-strong); + padding: 32px 24px; + box-shadow: var(--shadow-black); + position: relative; +} + +.step-num-big { + font-size: 3rem; + font-weight: 900; + color: var(--border-strong); + line-height: 1; + margin-bottom: 12px; +} + +.step-box:hover .step-num-big { + color: var(--accent-amber); +} + +.step-box-title { + font-size: 1.2rem; + margin-bottom: 8px; +} + +.step-box-desc { + color: var(--text-dim); + font-size: 0.92rem; + margin-bottom: 18px; +} + +.step-code-bar { + font-family: var(--font-mono); + font-size: 0.8rem; + background: #000; + border: 1px solid var(--border-strong); + padding: 8px 12px; + color: var(--accent-gold); +} + +/* Slash Commands Explorer */ +.commands-controls { + display: flex; + gap: 8px; + margin-bottom: 28px; + flex-wrap: wrap; +} + +.filter-btn { + font-family: var(--font-mono); + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + padding: 8px 14px; + background: var(--bg-panel); + color: var(--text-dim); + border: 2px solid var(--border-strong); + cursor: pointer; +} + +.filter-btn:hover { + color: #fff; + border-color: #fff; +} + +.filter-btn.active { + background: var(--accent-amber); + color: #000; + border-color: #000; + box-shadow: 3px 3px 0px #ffffff; +} + +.commands-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.command-card { + background: var(--bg-panel); + border: 2px solid var(--border-strong); + padding: 20px; + display: flex; + flex-direction: column; + justify-content: space-between; + box-shadow: 4px 4px 0px #000; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.command-card:hover { + border-color: var(--accent-amber); + box-shadow: 4px 4px 0px var(--accent-amber); +} + +.command-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + gap: 12px; +} + +.command-name { + font-family: var(--font-mono); + font-size: 0.95rem; + font-weight: 700; + color: var(--accent-gold); + background: #000; + border: 1px solid var(--border-strong); + padding: 4px 8px; +} + +.copy-btn { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 800; + text-transform: uppercase; + background: var(--bg-panel-contrast); + color: var(--text-main); + border: 1px solid var(--border-strong); + padding: 4px 10px; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; +} + +.copy-btn:hover { + background: var(--accent-amber); + color: #000; + border-color: #000; +} + +.command-card-desc { + font-size: 0.9rem; + color: var(--text-dim); + margin-bottom: 14px; +} + +.command-args { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.arg-badge { + font-family: var(--font-mono); + font-size: 0.72rem; + background: #000; + border: 1px solid var(--border-muted); + color: var(--text-muted); + padding: 2px 6px; +} + +.arg-badge.req { + color: var(--accent-amber); + border-color: var(--accent-orange); +} + +/* Permissions Matrix */ +.perms-panel { + background: var(--bg-panel); + border: 2px solid var(--border-strong); + box-shadow: 8px 8px 0px #000; + padding: 32px; +} + +.perms-presets-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 28px; + padding-bottom: 16px; + border-bottom: 2px solid var(--border-muted); + flex-wrap: wrap; +} + +.preset-btn { + font-family: var(--font-mono); + font-size: 0.8rem; + font-weight: 800; + text-transform: uppercase; + background: #000; + color: var(--text-dim); + border: 2px solid var(--border-strong); + padding: 6px 14px; + cursor: pointer; +} + +.preset-btn:hover { + color: #fff; + border-color: #fff; +} + +.preset-btn.active { + background: var(--accent-amber); + color: #000; + border-color: #000; + box-shadow: 3px 3px 0px #ffffff; +} + +.perms-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 32px; + margin-bottom: 36px; +} + +.perm-col-title { + font-size: 1rem; + font-weight: 800; + color: var(--accent-amber); + margin-bottom: 14px; + padding-bottom: 6px; + border-bottom: 2px solid var(--border-strong); +} + +.perm-items-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.perm-item-row { + display: flex; + align-items: flex-start; + gap: 10px; + cursor: pointer; +} + +.perm-item-row input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--accent-amber); + margin-top: 3px; + cursor: pointer; + flex-shrink: 0; + border-radius: 0; +} + +.perm-text-wrap { + display: flex; + flex-direction: column; +} + +.perm-item-name { + font-family: var(--font-mono); + font-weight: 700; + font-size: 0.85rem; + color: #ffffff; +} + +.perm-item-why { + font-size: 0.76rem; + color: var(--text-muted); +} + +.perms-readout { + background: #000; + border: 2px solid var(--accent-amber); + box-shadow: 4px 4px 0px var(--accent-amber); + padding: 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + flex-wrap: wrap; +} + +.perms-calc-box { + display: flex; + flex-direction: column; +} + +.perms-calc-label { + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 800; + text-transform: uppercase; + color: var(--text-muted); +} + +.perms-calc-val { + font-family: var(--font-mono); + font-size: 1.6rem; + font-weight: 900; + color: var(--accent-amber); +} + +.perms-cta-wrap { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +/* FAQ Accordion */ +.faq-stack { + display: flex; + flex-direction: column; + gap: 12px; + max-width: 900px; +} + +.faq-item { + background: var(--bg-panel); + border: 2px solid var(--border-strong); + box-shadow: 4px 4px 0px #000; +} + +.faq-question { + width: 100%; + padding: 18px 22px; + background: transparent; + border: none; + color: #ffffff; + font-family: var(--font-display); + font-size: 1.1rem; + font-weight: 800; + text-align: left; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.faq-toggle-icon { + font-family: var(--font-mono); + font-weight: 800; + font-size: 1.2rem; + color: var(--accent-amber); +} + +.faq-answer { + max-height: 0; + overflow: hidden; + transition: max-height 0.25s ease-out, padding 0.25s ease-out; + padding: 0 22px; + color: var(--text-dim); + font-size: 0.94rem; + line-height: 1.65; +} + +.faq-item.active .faq-answer { + padding-bottom: 20px; + border-top: 1px solid var(--border-muted); +} + +/* Brutalist Banner / Callout */ +.action-callout { + background: #000; + border: 3px solid var(--accent-amber); + box-shadow: 8px 8px 0px var(--accent-amber); + padding: 50px 36px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 30px; + flex-wrap: wrap; +} + +.callout-title { + font-size: clamp(1.8rem, 3.2vw, 2.6rem); + font-weight: 900; + margin-bottom: 8px; +} + +.callout-desc { + color: var(--text-dim); + font-size: 1.05rem; + max-width: 600px; +} + +/* Footer */ +.footer { + background: #040405; + border-top: 2px solid var(--border-strong); + padding: 48px 0 28px; + position: relative; + z-index: 2; +} + +.footer-matrix { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr; + gap: 32px; + margin-bottom: 40px; +} + +.footer-col-title { + font-family: var(--font-mono); + font-size: 0.82rem; + font-weight: 800; + text-transform: uppercase; + color: #ffffff; + margin-bottom: 14px; +} + +.footer-col-links { + list-style: none; + display: flex; + flex-direction: column; + gap: 8px; +} + +.footer-col-links a { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-dim); + transition: color 0.15s ease; +} + +.footer-col-links a:hover { + color: var(--accent-amber); +} + +.footer-bottom { + border-top: 1px solid var(--border-muted); + padding-top: 20px; + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--text-muted); + flex-wrap: wrap; + gap: 12px; +} + +/* Brutalist Toast */ +.toast { + position: fixed; + bottom: 24px; + right: 24px; + background: #000000; + color: #ffffff; + border: 2px solid var(--accent-amber); + box-shadow: 4px 4px 0px #000; + padding: 12px 18px; + font-family: var(--font-mono); + font-size: 0.85rem; + font-weight: 700; + display: flex; + align-items: center; + gap: 10px; + z-index: 9999; + transform: translateY(120%); + opacity: 0; + transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease; + pointer-events: none; +} + +.toast.show { + transform: translateY(0); + opacity: 1; +} + +/* Responsive */ +@media (max-width: 992px) { + .hero-grid { + grid-template-columns: 1fr; + } + .features-matrix { + grid-template-columns: 1fr 1fr; + } + .steps-matrix { + grid-template-columns: 1fr; + } + .commands-grid { + grid-template-columns: 1fr; + } + .perms-grid { + grid-template-columns: 1fr; + } + .footer-matrix { + grid-template-columns: 1fr 1fr; + } + .stats-strip { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 640px) { + .features-matrix { + grid-template-columns: 1fr; + } + .footer-matrix { + grid-template-columns: 1fr; + } + .stats-strip { + grid-template-columns: 1fr; + } + .mobile-toggle { + display: block; + } + .nav-menu { + display: none; + position: absolute; + top: 72px; + left: 0; + right: 0; + background: var(--bg-page); + border-bottom: 2px solid var(--border-strong); + flex-direction: column; + padding: 20px; + gap: 16px; + } + .nav-menu.open { + display: flex; + } + .action-callout { + flex-direction: column; + align-items: flex-start; + } +}