diff --git a/astro.config.mjs b/astro.config.mjs
index 574dbc3..270f16f 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -29,7 +29,11 @@ export default defineConfig({
!page.includes('/study/review') &&
!page.includes('/study/settings') &&
!page.includes('/study/challenges') &&
- !page.includes('/study/cases'),
+ !page.includes('/study/cases') &&
+ // PLAYGROUND_ENABLED (src/lib/feature-flags.ts) is false — the route
+ // renders a "temporarily unavailable" page, so keep it out of the
+ // sitemap. Remove this line when the flag flips back to true.
+ !page.includes('/playground'),
lastmod: new Date(),
}),
],
diff --git a/docs/dark-mode.md b/docs/dark-mode.md
new file mode 100644
index 0000000..6fbda35
--- /dev/null
+++ b/docs/dark-mode.md
@@ -0,0 +1,239 @@
+# Dark Mode — Sweep Cheat-Sheet
+
+Status: **foundation complete** (tokens, activation, toggle, global base styles,
+UI kit). Page-level sweeps are the next phase — this document is the reference
+those sweeps should follow. It assumes you're comfortable with Tailwind's
+`dark:` variant; the only non-standard thing here is *how* dark mode is
+activated (attribute, not the default `.dark` class) and the site-specific
+token names.
+
+Read this top to bottom before sweeping a page. The "hard gates" section at
+the end lists things that must never happen.
+
+## 1. How activation works
+
+- `darkMode: ['selector', '[data-theme="dark"]']` in `tailwind.config.mjs`
+ (Tailwind 3.4.15 `selector` strategy). Every `dark:` utility compiles to
+ `[data-theme="dark"] &`. It is **not** the classic `.dark` class strategy.
+- `data-theme="light"` or `data-theme="dark"` is set on `` by an inline,
+ `is:inline` no-FOUC script in `src/layouts/BaseLayout.astro`, which runs
+ before first paint. Priority: `localStorage.theme` → `prefers-color-scheme`
+ fallback.
+- `window.__setTheme(theme, persist = true)` is the single source of truth for
+ changing theme at runtime. It sets the attribute, persists to
+ `localStorage` (unless `persist` is `false`), and fires a
+ `document.dispatchEvent(new CustomEvent('themechange', { detail: { theme } }))`.
+ **Never** set `data-theme` directly from page code — call `__setTheme`, or
+ read the current value via `document.documentElement.getAttribute('data-theme')`.
+- Cross-tab sync: a `storage` event listener re-applies theme when
+ `localStorage.theme` changes in another tab.
+- OS-preference sync: a `matchMedia('(prefers-color-scheme: dark)')` listener
+ only fires when there is **no stored preference** — once a user has an
+ explicit choice, OS changes are ignored (by design).
+- `color-scheme` (native form controls/scrollbar) is set via CSS in
+ `global.css`, tied to `[data-theme='dark']`, not a static meta tag — so it
+ always matches the *active* theme, not raw OS state.
+
+## 2. Toggle architecture
+
+`src/components/layout/ThemeToggle.astro` is framework-free (no React) and
+safe to render multiple times per page (desktop header + mobile menu both do
+this). Multiple instances share:
+
+- one delegated `click` listener bound once globally (guarded by
+ `window.__themeToggleBound`)
+- one `themechange` listener that re-syncs `aria-pressed` on every instance
+ via the shared `.theme-toggle` class
+
+If you need a new toggle placement, just drop `