deck

star 2

Front door for the deck — the operating substrate for ALL persistent work. AUTO-INVOKE when the user references the deck/methodology/workflow OR at session start as a reminder that every user request producing persistent work must flow through Skill(create-card) → Skill(advance-card) → Skill(finish-card). XP-style story cards on a kanban board, designed for AI-agent collaborators.

zauberzeug By zauberzeug schedule Updated 5/29/2026

name: deck description: Front door for the deck — the operating substrate for ALL persistent work. AUTO-INVOKE when the user references the deck/methodology/workflow OR at session start as a reminder that every user request producing persistent work must flow through Skill(create-card) → Skill(advance-card) → Skill(finish-card). XP-style story cards on a kanban board, designed for AI-agent collaborators.

The Deck

deck/ is the project's work-tracking surface. Each subdirectory is one card: a unit of work — bug, story, epic, idea, derivation gap, doc-drift catch — with frontmatter-driven status on a kanban board.

Heritage & philosophy: agile for the age of agents

The deck inherits three traditions deliberately:

  • XP story cards (Beck, 1999). One card = one unit of work, small enough to fit on an index card, with enough context that any team member can pick it up. We keep the size discipline; the medium is markdown instead of cardboard.
  • Scrum's Definition of Done (Sutherland & Schwaber). Every card carries a checkbox-list DoD as a closure contract. `goc done` refuses to flip the status until every box is `- [x]`. The contract is machine-checkable, not a verbal handoff.</li> <li><strong>Kanban (Anderson, Toyota lineage).</strong> Status mutates; position on disk does NOT. A card stays at <code>deck/<title>/</code> for life — no moving to <code>done/</code>, no archiving. Cross-references stay valid through every state change.</li> </ul> <p>The argument for keeping these now: original agile was a response to <strong>human handoff costs</strong> — the 1990s problem of getting a feature from analyst to developer to QA without losing intent. AI agents are the most aggressive handoff-stress-test ever invented: dozens of sub-agents, scheduled cron loops, and parallel /loop iterations all read the same cards and mutate the same frontmatter. What was "discipline" for human teams becomes <strong>structurally load-bearing</strong> under that load:</p> <ul> <li><strong>Slug-as-URL.</strong> <code>deck/<title>/</code> is a stable handle that survives status changes, agent rewrites, and supersession chains. Renaming breaks every commit message and prior cross-reference.</li> <li><strong>Machine-checkable DoDs.</strong> A swarm cannot pattern-match "did we really finish this" the way a stand-up meeting can. The CLI's unchecked-box count is the closure gate.</li> <li><strong>Self-contained card bodies.</strong> Hallway context evaporates between /loop iterations. Each card must carry its own evidence, reproduction recipe, and decision framing — agents cannot rely on conversational continuity.</li> </ul> <h2>Game of Cards as the runtime, not a workflow</h2> <p>The deck is the <strong>implementation</strong> of the human's intent, not a ceremony they opt into. A vibe coder types "I want a CSV export button" and the system silently: files the card, drafts the body from their words, advances it, implements, closes, commits. They see the button. They do not see the card. If they ever ask "what's in flight?", <code>Skill(scan-deck)</code> shows them the bookkeeping. If they never ask, they never need to know it exists.</p> <p>This separates the methodology's <strong>rigor</strong> (machine-checkable DoDs, audit-trail commits, supersession chains, hook-enforced routing) from its <strong>visibility</strong> (zero by default). Same principle as a database engine: the user writes SQL; transactions, locks, and write-ahead logs happen invisibly. Our cards are the rows; deck.py is the engine; the skills are the query interface.</p> <p>Three operating modes coexist:</p> <ul> <li><strong>Session mode.</strong> A human is talking. The UserPromptSubmit hook detects work intent in their prompt; Claude runs the card pipeline silently while answering. Card operations are NEVER announced unless the user explicitly asks to see the deck.</li> <li><strong>Autonomous mode.</strong> No human is steering. <code>Skill(pull-card)</code> runs on <code>/loop pull-card 30m</code> or <code>/schedule pull-card weekday 09:00</code>, draining the <code>human_gate: none</code> queue. <code>Skill(audit-deck)</code> runs on a slower cadence to keep the queue fed. The deck advances overnight. The human wakes up to commits that closed cards they never explicitly claimed.</li> <li><strong>Andon-cord mode.</strong> A human is unblocking the line. When <code>pull-card</code> hit a question only a human could answer, it raised the gate (<code>none → decision</code> or <code>none → session</code>) and parked the card. <code>Skill(scan-deck)</code> surfaces parked cards (triage default on bare invocation or "what's up?"); <code>Skill(scan-deck) decisions to make</code> walks each decision-gated card via <code>AskUserQuestion</code> and calls <code>Skill(decide-card)</code> per answer. Gate lowered → next <code>pull-card</code> claims and implements per the recorded decision. Lean's pulled-cord pattern: humans resolve the cause, agents restart the line.</li> </ul> <p>Multiple Claude sessions on the same project work cards in parallel. The <code>status: active</code> field is the soft lock; git's merge handles the rare race when two sessions claim the same card simultaneously (whichever commits first wins). The user can have N parallel chats going while M scheduled agents work the deck — they ride the events as they occur, present or absent as resources allow.</p> <h2>What this looks like in practice</h2> <p><strong>Session mode (silent runtime).</strong> When the user asks for persistent work — "rename the button to Export", "add a CSV export", "fix the auth bug" — run the GoC pipeline silently:</p> <ol> <li>Check the deck — does a card already cover this? (<code>goc --tag <topic></code>)</li> <li>If not, file a card: <code>goc new <kebab-title></code> and edit the body.</li> <li>Claim it: <code>goc status <title> active</code>.</li> <li>Implement.</li> <li>Close: <code>goc done <title></code>, then commit the work and closure.</li> </ol> <p>Card operations are NEVER announced — the user sees the code, not the bookkeeping. <strong>No-card exceptions</strong> (zero work, no card): exploration ("explain X", "why is Y this way?"), one-shot tooling ("git status", "rebase this"), course-corrections inside an active card.</p> <p><strong>Autonomous mode.</strong> Before recommending or claiming new work, agents check <code>goc --status active</code> and treat listed cards as already-claimed soft locks. The pull principle is what makes this safe: work isn't pushed at agents on a timer; agents pull on their own terms, filtered to gate=none. The human steers by curating WHAT'S in the queue and at what gate.</p> <p><strong>Andon-cord path.</strong> When a human asks "what's up?" / "where do you need me?", surface parked cards (oldest-first, with <code>## Decision required</code> body section preview). Decision recorded → gate lowered with <code>goc decide <title> --decision "..." --because "..."</code> → next pull-card claims and implements per the recorded decision.</p> <h2>Daily CLI verbs</h2> <table> <thead> <tr> <th>Verb</th> <th>What it does</th> </tr> </thead> <tbody><tr> <td><code>goc</code></td> <td>Show the open queue (value-sorted).</td> </tr> <tr> <td><code>goc --board</code></td> <td>Multi-column kanban view. A <code>⏳</code> after an open card's <code>[contribution]</code> marker flags any of three signals: <code>human_gate != none</code> (parked for a human — <strong>not pullable</strong>), an active impediment overlay (<code>waiting_on</code> / future <code>waiting_until</code> — <strong>not pullable</strong>), or an advisory derived dependency-block (a non-terminal <code>advanced_by</code> prereq — <strong>still pullable</strong>, just flagged as "has an open upstream"). Only the first two hide a card from <code>pull-card</code> / <code>next-card</code> / <code>goc --ready</code>; a dependency-block does not. So <code>⏳</code> ⇏ unpullable — check the cause.</td> </tr> <tr> <td><code>goc --status done --since YYYY-MM-DD</code></td> <td>Recently closed cards.</td> </tr> <tr> <td><code>goc new <title></code></td> <td>Scaffold a new card under <code>.game-of-cards/deck/<title>/</code>.</td> </tr> <tr> <td><code>goc status <title> <state></code></td> <td>Flip status (open/active/disproved/superseded).</td> </tr> <tr> <td><code>goc wait <title> --reason <r> [--until <date>]</code></td> <td>Set the impediment overlay for exogenous waits; <code>--clear</code> to drop it.</td> </tr> <tr> <td><code>goc done <title></code></td> <td>Close + DoD enforcement (no auto-commit).</td> </tr> <tr> <td><code>goc decide <title> --decision X --because Y</code></td> <td>Lower gate from decision/session → none.</td> </tr> <tr> <td><code>goc validate</code></td> <td>Validate every card's frontmatter (pre-commit-friendly).</td> </tr> </tbody></table> <p>Run <code>goc --help</code> for the full verb list. Schema and enum constraints surface in <code>goc validate</code> error messages. Project-local tag extensions live in <code>.game-of-cards/canonical-tags.md</code>.</p> <h2>The worldview: Game of Cards</h2> <p>The methodology has a name: <strong>Game of Cards</strong> — the <em>Game of Thrones</em> echo is deliberate. Work in a complex codebase plus an AI-agent swarm is <strong>political</strong> (cards have stakeholders, factions, conflicting opinions), <strong>unforeseeable</strong> (you cannot predict which threads activate or which cards get superseded), <strong>high-stakes</strong> (a wrongly- flipped DoD ripples through commit history and supersession chains), and <strong>uncontrollable</strong> (you don't drive the swarm; events surface, you respond).</p> <p>The 1990s agile methodologies optimized for human-team handoffs. Game of Cards adds the worldview layer: <strong>you ride the events as they occur</strong>. <code>Skill(next-card)</code> exists because work is <em>taken</em> when capacity exists, not <em>pushed</em> when work appears (Kanban pull, made structural). <code>Skill(audit-deck)</code> exists because the deck reflects emergent reality — new cards surface as agents discover gaps the original plan didn't anticipate. The architecture already encoded the no-control stance; the name finally gives it words.</p> <p>Hence the rebrand: not "todos" (an undifferentiated list) but <strong>the deck</strong> (named cards with stable handles, lifecycle, and contracts). Not "ticket tracker" but a card surface designed for the read-pattern of a swarm.</p> <h2>The deck layout</h2> <pre><code>deck/ SCHEMA.md # canonical schema (frontmatter IS the schema) README.md # navigation + conventions deck.py # CLI; computes filtered views from frontmatter <title>/ # one dir per card; never moves on state change README.md # frontmatter + dashboard body — latest knowledge + current state log.md # append-only journal — history, details, decisions, flow reproduce.py # OPTIONAL — declared in DoD when present [other validation scripts] </code></pre> <p>One title, one directory, forever. Status changes mutate frontmatter, not paths.</p> <p>Two files, two edit disciplines: the README is the <strong>dashboard</strong> (rewritten in place as understanding evolves so a cold reader sees only what is true now); <code>log.md</code> is the <strong>append-only journal</strong> (history, details, decisions, and flow preserved verbatim, never rewritten). See <code>Skill(card-schema)</code>'s "What goes where" subsection for the routing rule.</p> <h2>Lifecycle</h2> <pre><code> open ──→ active ──→ done (terminal, DoD-100% required) │ ├──→ disproved (terminal; body documents rebuttal) └──→ superseded (terminal; typed `superseded_by` → successor, rationale appended to log.md) </code></pre> <p><code>open</code> is the queue. <code>active</code> claims the work. <code>done</code>, <code>disproved</code>, and <code>superseded</code> are all terminal — none deletes the directory; the forensic record stays.</p> <p>A card that is <strong>temporarily not workable</strong> does not move to a separate status; the three-axis model expresses it as either derived dependency-readiness (an <code>advances</code> prereq is still open and shows as "awaiting" in the queue, advisory only) or the stored impediment overlay (<code>waiting_on</code> + optional <code>waiting_until</code>, set via <code>goc wait</code>, hides the card from the ready queue until cleared or the date elapses). See <code>Skill(card-schema)</code> "Three-axis 'stuck' model" for the full contract.</p> <p><strong>Closure is not frozenness.</strong> Terminal status retires the card from the live queue but does not freeze the directory. When new evidence surfaces later — a bug found weeks after close, an assumption invalidated by follow-up work, a successor card that reframes the original — file a new card AND amend the closed one with a forward pointer (one-line dated entry appended to <code>log.md</code>, optional <code>> Later evidence:</code> pointer at the top of the README body). The deck is the durable record of what was learned, not just what shipped; each closed card stays as the live entry-point to the full thread. See <code>Skill(finish-card)</code> "After closure" for the cross-reference format.</p> <p><code>status</code>, the <code>waiting_on</code> overlay, and <code>human_gate</code> are three orthogonal axes. An exogenous wait on an agent-observable condition (upstream release, PR merge, dependency publication, scheduled re-check) sits as <code>waiting_on: external</code> (+ optional <code>waiting_until</code>) on a <code>status: open</code> card with <code>human_gate: none</code> — a future autonomous run verifies the condition cleared and <code>goc wait <title> --clear</code>s the overlay without human involvement. Raise <code>human_gate</code> to <code>decision</code>/<code>session</code> only when human judgement is the unblocker. See <code>Skill(card-schema)</code> for the full orthogonality contract.</p> <h2>The 9 action skills</h2> <p>One skill per job; compose, don't bundle.</p> <ul> <li><code>Skill(scan-deck)</code> — browse the deck. Triage default surfaces parked cards (gate ≠ none); also filtered queues, kanban board, JSON dump, and the "decisions to make" Q&A flow. Read-only by default; the Q&A mode calls <code>Skill(decide-card)</code> per answer.</li> <li><code>Skill(next-card)</code> — auto-pick the highest-leverage open <code>gate=none</code> card to work on next. Read-only; does NOT flip status.</li> <li><code>Skill(create-card)</code> — file a new card with proper frontmatter and a DoD scaffold (reproduce.py is authored by hand for bug-class cards, not scaffolded by the tool).</li> <li><code>Skill(advance-card)</code> — flip status (open→active, *→open, *→disproved, *→superseded) and manage the <code>waiting_on</code> impediment overlay (<code>goc wait</code>). Wraps <code>goc status</code> and <code>goc wait</code>. Status + overlay only — <code>human_gate</code> is <code>decide-card</code>'s responsibility.</li> <li><code>Skill(decide-card)</code> — the human's Andon-cord lowering action. Records <code><decision> + <because></code> on a parked card and flips gate <code>decision</code>/<code>session</code> → <code>none</code>. Status stays <code>open</code> so the next <code>pull-card</code> claims and implements per the recorded decision.</li> <li><code>Skill(finish-card)</code> — close a card: tick DoD, append closure log, run <code>goc done <title></code>, then run any project-specific post-close or commit handoff defined by the consuming repo's hook.</li> <li><code>Skill(card-schema)</code> — schema reference (read-only): required fields, enums, canonical tags with predicates, DoD detection, relationship invariants.</li> <li><code>Skill(refine-deck)</code> — hygiene pass: retag stale cards, prune unverified parks, surface cards citing defunct file:lines, propose new canonical tags.</li> <li><code>Skill(audit-deck)</code> — discovery: hunt for one previously-undocumented defect / derivation gap / doc drift / missing test. Files via <code>Skill(create-card)</code>.</li> <li><code>Skill(standup)</code> — daily read: active + impeded cards (those carrying a <code>waiting_on</code> overlay), closures since yesterday, cards waiting on a human decision gate.</li> <li><code>Skill(retrospective)</code> — backwards analysis of the last N closed cards: cluster by tag, surface recurring failure modes, propose generalization candidates.</li> </ul> <h2>Migration legacy</h2> <p>Most pre-2026-05-01 entries (≈135 of 270) carry the placeholder <code>definition_of_done: |\n - [x] (criteria not found in source README; manual review needed)</code>. The legacy <code>bugs/</code> and <code>tasks/</code> trees did not encode a closure contract per entry — closure was implicit ("the FIXED.md row appeared"). The migration could not reconstruct what each prior closure verified, so it inserted a checked placeholder for already-<code>done</code> entries (validation passes; forensic record is the closing commit's body) and an unchecked placeholder for still-<code>open</code> entries (forces the next <code>Skill(finish-card)</code> run to write a real DoD before <code>goc done <title></code> will accept closure).</p> <p>The two skills <code>find-todo</code> and <code>work-todo</code> were the v1 names. Their responsibilities split:</p> <ul> <li>v1 <code>/find-todo</code> → <code>Skill(audit-deck)</code> (discovery + filing).</li> <li>v1 <code>/work-todo</code> → <code>Skill(next-card)</code> + <code>Skill(advance-card)</code> + <code>Skill(finish-card)</code> (selection, status mutation, closure).</li> </ul> <p>Old commits referencing <code>/find-todo</code> or <code>/work-todo</code> continue to make sense — the workflows are conserved, just decomposed.</p> </article> </div> <!-- Right: Metadata & Command Sidebar --> <div class="w-full lg:w-80 shrink-0 flex flex-col gap-6" data-astro-cid-7zzsworf> <!-- Install Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-mono" data-astro-cid-7zzsworf>Install via CLI</span> <div class="flex flex-col gap-2" data-astro-cid-7zzsworf> <div id="detail-install-cmd" class="font-mono text-[11px] p-3 rounded-lg bg-black/40 border border-border select-all break-all text-primary font-bold leading-relaxed" data-astro-cid-7zzsworf> npx skills add https://github.com/zauberzeug/game-of-cards --skill deck </div> <button id="detail-copy-btn" class="w-full py-2.5 rounded-lg bg-primary hover:bg-primary-hover text-on-primary font-sans font-bold text-sm shadow transition-all active:scale-95 flex items-center justify-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px]" data-astro-cid-7zzsworf>content_copy</span> <span data-astro-cid-7zzsworf>Copy Command</span> </button> </div> </div> <!-- Details & Stats Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm text-on-surface" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>Repository Details</span> <div class="flex flex-col gap-3.5" data-astro-cid-7zzsworf> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>star</span> Stars </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>2</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>call_split</span> Forks </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>0</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>navigation</span> Branch </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant" data-astro-cid-7zzsworf>main</span> </div> <div class="flex justify-between items-start text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5 mt-0.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>article</span> Path </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant truncate max-w-[150px]" title="SKILL.md" data-astro-cid-7zzsworf>SKILL.md</span> </div> </div> </div> <!-- Occupations Tag Card --> <!-- Related Creators Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-3 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>More from Creator</span> <div class="flex items-center gap-2" data-astro-cid-7zzsworf> <img class="w-8 h-8 rounded-full border border-border" src="https://avatars.githubusercontent.com/u/2843826?v=4" alt="zauberzeug" onerror="this.src='https://avatars.githubusercontent.com/u/9919?v=4'" data-astro-cid-7zzsworf> <div class="flex flex-col min-w-0" data-astro-cid-7zzsworf> <span class="font-bold text-sm truncate text-on-surface" data-astro-cid-7zzsworf>zauberzeug</span> <a href="/?creator=zauberzeug" class="text-xs text-primary hover:underline font-semibold transition-all" data-astro-cid-7zzsworf>Explore all skills →</a> </div> </div> </div> </div> </div> </div> </div> <script> const copyBtn = document.getElementById("detail-copy-btn"); const installCmd = document.getElementById("detail-install-cmd"); if (copyBtn && installCmd) { copyBtn.addEventListener("click", () => { const cmd = installCmd.textContent.trim(); navigator.clipboard.writeText(cmd).then(() => { const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ` <span class="material-symbols-outlined text-[16px]">check</span> <span>Copied!</span> `; copyBtn.style.background = "#10b981"; copyBtn.style.borderColor = "#10b981"; setTimeout(() => { copyBtn.innerHTML = originalText; copyBtn.style.background = ""; copyBtn.style.borderColor = ""; }, 1500); }); }); } </script> </div> <!-- Footer --> <footer class="border-t border-border bg-surface-container-low text-on-surface-variant py-8 px-gutter mt-16 rounded-xl"> <div class="max-w-container-max mx-auto flex flex-col md:flex-row justify-between items-center gap-6"> <div class="flex items-center gap-2"> <div class="w-6 h-6 rounded bg-primary bg-opacity-20 flex items-center justify-center"> <span class="material-symbols-outlined text-primary text-sm">code_blocks</span> </div> <span class="font-bold text-on-surface text-sm">SkillMD</span> </div> <div class="flex flex-wrap justify-center gap-6 text-sm"> <a href="/about" class="hover:text-primary transition-colors">About Us</a> <a href="/contact" class="hover:text-primary transition-colors">Contact Us</a> <a href="/privacy" class="hover:text-primary transition-colors">Privacy Policy</a> <a href="/terms" class="hover:text-primary transition-colors">Terms of Service</a> <a href="/support" class="hover:text-primary transition-colors">Support</a> </div> <div class="text-xs text-on-surface-variant/80"> © 2026 SkillMD. All rights reserved. </div> </div> </footer> </main> <!-- Script for Theme Toggle, Mobile Menu, and Sidebar Filter Redirection --> <script> // Theme setup const savedTheme = localStorage.getItem("theme") || "dark"; function applyTheme(theme) { document.documentElement.classList.remove("dark", "green", "dracula", "nord"); if (theme === "dark") { document.documentElement.classList.add("dark"); } else if (theme === "green") { document.documentElement.classList.add("dark", "green"); } else if (theme === "dracula") { document.documentElement.classList.add("dark", "dracula"); } else if (theme === "nord") { document.documentElement.classList.add("dark", "nord"); } document.documentElement.setAttribute("data-theme", theme); const themeMoon = document.getElementById("theme-moon"); const themeSun = document.getElementById("theme-sun"); const themeLeaf = document.getElementById("theme-leaf"); const themeDracula = document.getElementById("theme-dracula"); const themeNord = document.getElementById("theme-nord"); if (themeMoon && themeSun && themeLeaf && themeDracula && themeNord) { themeMoon.style.display = theme === "dark" ? "inline" : "none"; themeSun.style.display = theme === "light" ? "inline" : "none"; themeLeaf.style.display = theme === "green" ? "inline" : "none"; themeDracula.style.display = theme === "dracula" ? "inline" : "none"; themeNord.style.display = theme === "nord" ? "inline" : "none"; } } applyTheme(savedTheme); const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (themeToggleBtn) { themeToggleBtn.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme") || "dark"; let newTheme = "dark"; if (currentTheme === "dark") { newTheme = "light"; } else if (currentTheme === "light") { newTheme = "green"; } else if (currentTheme === "green") { newTheme = "dracula"; } else if (currentTheme === "dracula") { newTheme = "nord"; } else { newTheme = "dark"; } applyTheme(newTheme); localStorage.setItem("theme", newTheme); }); } // Mobile menu toggle and sidebar logic const mobileMenuToggle = document.getElementById("mobile-menu-toggle"); const sidebarMenu = document.getElementById("sidebar-menu"); const sidebarOverlay = document.getElementById("sidebar-overlay"); function isMobile() { return window.innerWidth < 768; // 768px is the 'md' breakpoint in Tailwind } function openSidebar() { if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.remove("hidden"); } } function closeSidebar() { if (sidebarMenu && isMobile()) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } if (mobileMenuToggle && sidebarMenu) { mobileMenuToggle.addEventListener("click", (e) => { e.stopPropagation(); if (isMobile()) { const isClosed = sidebarMenu.classList.contains("-translate-x-full"); if (isClosed) { openSidebar(); } else { closeSidebar(); } } }); document.addEventListener("click", (e) => { if (isMobile()) { if (!sidebarMenu.contains(e.target) && !mobileMenuToggle.contains(e.target)) { closeSidebar(); } } }); if (sidebarOverlay) { sidebarOverlay.addEventListener("click", () => { if (isMobile()) { closeSidebar(); } }); } // Collapse sidebar when clicking a filter button, creator button, or nav item inside it sidebarMenu.addEventListener("click", (e) => { if (isMobile()) { const clickTarget = e.target.closest("button, a"); if (clickTarget) { closeSidebar(); } } }); // Sync sidebar state on window resize window.addEventListener("resize", () => { if (!isMobile()) { // Desktop: sidebar should be visible, no overlay if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } else { // Mobile: start collapsed if (sidebarMenu) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } }); } // If not on homepage, redirect on sidebar filter click const isHomepage = window.location.pathname === "/"; document.querySelectorAll("#occupation-filters .filter-btn").forEach(btn => { btn.addEventListener("click", (e) => { const occ = e.currentTarget.getAttribute("data-occupation"); if (!isHomepage) { window.location.href = occ ? `/?occupation=${encodeURIComponent(occ)}` : "/"; } }); }); document.querySelectorAll("#creator-filters .creator-btn").forEach(btn => { btn.addEventListener("click", (e) => { const creator = e.currentTarget.getAttribute("data-creator"); if (!isHomepage) { window.location.href = `/?creator=${encodeURIComponent(creator)}`; } }); }); </script> </body> </html>