diff --git a/.gitignore b/.gitignore
index a66a89e..e18e525 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,4 +70,12 @@ investigation_report.md
# Hermes plan/audit analyzer artifacts (advisory, not project source)
HERMES_PLAN.ai.json
HERMES_PLAN.html
-docs/.scratch-audit/*.html
+docs/.scratch-audit/
+
+# Vendored "cosmic-ui" component library (added as git subtree; build/install
+# artifacts are ignored, source is tracked).
+cosmic-ui/node_modules/
+cosmic-ui/dist/
+cosmic-ui/.cache/
+cosmic-ui/coverage/
+cosmic-ui/.vite/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..52c044d
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "cosmic-ui"]
+ path = cosmic-ui
+ url = https://github.com/TeacherEvan/cosmic-ui.git
+
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..6bd44b9
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,21 @@
+# ACTIVE REPO — READ FIRST
+
+The directory this file lives in is a project repo. When an agent is opened
+here, THIS directory is the task target by default: audit, edit, build, and
+prune here — do not redirect work to other repos and do not enumerate or
+suggest sibling projects.
+
+## Rules
+- NEVER scaffold a new project unless explicitly told. Forbidden by default:
+ `create-next-app`, `npm create`, `npx sv create`, `npm init`, `git clone` into a
+ *new* directory, `degit`, `npx create-*`.
+- NEVER create project dirs under `$HOME` root or `/tmp`.
+- NEVER create duplicate/sibling copies of an existing repo. If a repo exists
+ somewhere, work in it; do not clone or scaffold another copy elsewhere.
+- If the opened folder contains no repo and the task needs one, STOP and ask.
+- If unsure which directory is active, STOP and ask. Never guess a path.
+
+## Known sibling projects (separate repos — not this one)
+Devil-sDelight, BudgetBITCH, Worm ranch, English-K1Run, Phonics-Fun,
+Quizzbudy, ShapeKeeper, SuperStudent, ZombieSweep, and others under
+/home/ewaldt/Documents/VS/GAMES/. Do not recreate or modify them when working here.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..6bd44b9
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,21 @@
+# ACTIVE REPO — READ FIRST
+
+The directory this file lives in is a project repo. When an agent is opened
+here, THIS directory is the task target by default: audit, edit, build, and
+prune here — do not redirect work to other repos and do not enumerate or
+suggest sibling projects.
+
+## Rules
+- NEVER scaffold a new project unless explicitly told. Forbidden by default:
+ `create-next-app`, `npm create`, `npx sv create`, `npm init`, `git clone` into a
+ *new* directory, `degit`, `npx create-*`.
+- NEVER create project dirs under `$HOME` root or `/tmp`.
+- NEVER create duplicate/sibling copies of an existing repo. If a repo exists
+ somewhere, work in it; do not clone or scaffold another copy elsewhere.
+- If the opened folder contains no repo and the task needs one, STOP and ask.
+- If unsure which directory is active, STOP and ask. Never guess a path.
+
+## Known sibling projects (separate repos — not this one)
+Devil-sDelight, BudgetBITCH, Worm ranch, English-K1Run, Phonics-Fun,
+Quizzbudy, ShapeKeeper, SuperStudent, ZombieSweep, and others under
+/home/ewaldt/Documents/VS/GAMES/. Do not recreate or modify them when working here.
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 0000000..9d40889
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,16 @@
+# ACTIVE REPO — READ FIRST
+
+The directory this file lives in is a project repo. When an agent is opened
+here, THIS directory is the task target by default: audit, edit, build, and
+prune here — do not redirect work to other repos and do not enumerate or
+suggest sibling projects.
+
+## Rules
+- NEVER scaffold a new project unless explicitly told. Forbidden by default:
+ `create-next-app`, `npm create`, `npx sv create`, `npm init`, `git clone` into a
+ *new* directory, `degit`, `npx create-*`.
+- NEVER create project dirs under `$HOME` root or `/tmp`.
+- NEVER create duplicate/sibling copies of an existing repo. If a repo exists
+ somewhere, work in it; do not clone or scaffold another copy elsewhere.
+- If the opened folder contains no repo and the task needs one, STOP and ask.
+- If unsure which directory is active, STOP and ask. Never guess a path.
diff --git a/Plan UI Hardening.txt b/Plan UI Hardening.txt
new file mode 100644
index 0000000..9552786
--- /dev/null
+++ b/Plan UI Hardening.txt
@@ -0,0 +1,129 @@
+# UI Hardening Plan — Welcome & Level Select
+
+## Goals (from user)
+1. **No-scroll** on the welcome + level-select pages for common laptop
+ and mobile-landscape viewports (≥768 wide, ≥600 tall). Phones in
+ portrait continue to scroll naturally.
+2. **Buttons must read clearly.** Current footer trio on level-select
+ (Back / Settings / Reset) is a row of identical-looking ghost pills
+ in three different accent colors. They blend in and read as the
+ same control. Introduce a **three-tier visual hierarchy** with icons.
+3. **Add settings access to the welcome page.** Right now the gear
+ button only lives in the level-select footer.
+4. **Extend settings with new controls:** audio volume slider,
+ high-contrast toggle, game speed / difficulty preview, persistent
+ UI scale slider.
+5. **No regressions** to existing tests (welcome-page-redesign,
+ level-select-interactions, level-select-settings,
+ welcome-scoreboard, level-select-scoreboard).
+
+## Design Decisions
+- **Visual hierarchy:**
+ - **Primary** (filled phosphor-green, dark text) — `Run foundations`,
+ `Begin Training`.
+ - **Secondary** (outlined brass-gold, light text + icon) — `Settings`,
+ `Scoreboard`.
+ - **Tertiary** (text-only with arrow icon) — `Back to welcome`.
+ - **Danger** (outlined ember-red, icon) — `Reset progress`.
+ - **Icon button** (40×40 round, phosphor outline) — top-bar `Settings`
+ gear and the new welcome-page gear.
+- **Layout pattern for both pages:**
+ - `body` becomes a 100dvh flex/grid column with three named regions:
+ `topbar`, `content` (flex:1, min-height:0), `actions` (sticky band).
+ - Internal content uses CSS `clamp()` for vertical sizing with hard
+ `min()` caps at the bottom so the page never grows past 100dvh.
+ - At ≤900px tall the welcome page hides the `welcome-brief` chip
+ strip and the `quote` block (collapsing chrome).
+- **Settings dialog:** grows to 5 controls (4 existing + volume) plus
+ 2 toggles (high contrast, UI scale). Grouped into
+ `Display` / `Sound` / `Accessibility` / `Gameplay` sections inside
+ the same dialog shell. UI scale and game speed use ``.
+- **New settings storage fields** (added to `user-settings.helpers.js`
+ `createDefaultSettings()` + `normalizeSettings()`):
+ - `sound.volume` (0–100, default 80)
+ - `display.highContrast` (bool, default false)
+ - `display.uiScale` (number 0.85–1.15, default 1)
+ - `gameplay.pressure` (read-only preview string — `"auto"` so
+ user can read what the per-level multiplier will be)
+ - Bump `SETTINGS_VERSION` to 2; legacy v1 records fall back via
+ `normalizeSettings`.
+
+## Files Touched
+
+### HTML
+- `src/pages/index.html`
+ - Add `` inside the welcome-header.
+ - Add the same `settings-dialog-shell` markup as level-select uses, with id `welcome-settings-shell` and a slightly trimmed form (no game-speed preview).
+- `src/pages/level-select.html`
+ - Wrap the `.navigation` footer with a new `.action-bar` div with three named children: `.action-bar-primary` (Back), `.action-bar-secondary` (Settings), `.action-bar-danger` (Reset).
+ - Add icons (inline SVG) inside each footer button.
+ - Add the same icon-button `Settings` gear to the page header so it's discoverable.
+
+### CSS
+- New shared layer `src/styles/css/buttons.tiers.css` — defines
+ `.btn`, `.btn--primary`, `.btn--secondary`, `.btn--tertiary`,
+ `.btn--danger`, `.icon-button`, focus rings, hover/active/disabled.
+- New shared layer `src/styles/css/shell.dvh.css` — defines the
+ `body.page-shell` 100dvh flex layout + `.page-topbar` + `.page-content`
+ + `.page-actions`. Imports once on both pages.
+- `src/styles/css/index.core.css` — switch `body.welcome-page` to use
+ the dvh shell; collapse the welcome-brief to `display:none` below
+ 900px tall; add the gear icon-button styles.
+- `src/styles/css/index.actions.css` — replace `.action-button`
+ pill style with the new tiered classes; primary action gets a
+ fill+icon combo, secondary becomes an outlined button with
+ visible icon.
+- `src/styles/css/index.responsive.compact.css` — make the
+ ≤520px-tall compact mode even tighter: hide the welcome-deck and
+ welcome-hint, shrink the logo, drop gaps.
+- `src/styles/css/level-select.polish.css` — apply the dvh shell
+ classes to `body` (`.level-page-shell`), shrink header padding,
+ reduce `.levels-grid` `gap` and card padding inside the
+ `level-page-shell` context.
+- `src/styles/css/level-select.css` — replace `.settings-button`,
+ `.back-button`, `.reset-progress-btn` styles with the new tiered
+ classes plus danger / tertiary variants.
+- `src/styles/css/level-select.details.responsive.css` — add
+ `(max-height: 760px)` rules to make cards and the route-switcher
+ fit a 720-tall mobile-landscape viewport.
+
+### JS
+- `src/scripts/user-settings.helpers.js` — add new fields and
+ version bump.
+- `src/scripts/user-settings.js` — `mergePartialSettings` already
+ handles arbitrary objects, no change required.
+- `src/scripts/level-select-page.settings.js` — already the canonical
+ controller. Refactor to:
+ - Read new fields and bind to new inputs.
+ - Wire the icon-button in the header to the same `openSettings()`.
+- New `src/scripts/welcome-page.settings.js` — extract the
+ `level-select-page.settings.js` open/close/apply logic into a
+ small helper at `window.SettingsDialog` so both pages share the
+ same controller. `welcome-page.settings.js` mounts it on the
+ welcome dialog.
+- `src/scripts/level-select-page.interactions.js` — no change, but
+ update selector hooks if any class names move.
+
+### Tests
+- `tests/welcome-page-redesign.spec.js` — add assertion that the
+ gear button exists and that the welcome shell has `min-height:100dvh`.
+- `tests/level-select-settings.spec.js` — add assertions for the
+ new volume / high-contrast / ui-scale controls and the icon-button
+ shortcut.
+- `tests/level-select-interactions.spec.js` — assert the three-tier
+ button class on each footer button.
+- New `tests/welcome-page.settings.spec.js` — verify the welcome
+ dialog opens from the gear, that the four existing settings
+ apply, and that the new controls are present.
+
+## Verification (manual + automated)
+1. `npm run lint` — must pass.
+2. `npx playwright test tests/welcome-page-redesign.spec.js tests/welcome-scoreboard.spec.js tests/level-select-settings.spec.js tests/level-select-interactions.spec.js tests/level-select-scoreboard.spec.js` — must pass.
+3. Manual: open `http://localhost:8000/src/pages/index.html` at
+ 1920×1080, 1366×768, and iPhone 13 (390×844) and confirm no
+ vertical scrollbar. Repeat for `level-select.html`.
+4. Manual: tap/click the gear on the welcome page → dialog opens,
+ all 6 controls render, save persists across reload.
+5. Manual: confirm danger button is clearly distinct (border + icon),
+ tertiary back button has an arrow, primary CTA fills the brand
+ color.
diff --git a/cosmic-ui b/cosmic-ui
new file mode 160000
index 0000000..ee28d3e
--- /dev/null
+++ b/cosmic-ui
@@ -0,0 +1 @@
+Subproject commit ee28d3e607d393f8a397aa3508c8f48d86f5a105
diff --git a/docs/plans/2026-08-29-cosmic-ui-console.txt b/docs/plans/2026-08-29-cosmic-ui-console.txt
new file mode 100644
index 0000000..1e104eb
--- /dev/null
+++ b/docs/plans/2026-08-29-cosmic-ui-console.txt
@@ -0,0 +1,442 @@
+# Cosmic UI Console + Evan Pulse — Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+> **For Hermes:** REQUIRED SUB-SKILL: superpowers:executing-plans. Slice-by-slice, gate each milestone before the next.
+
+**Goal:** Layer a "cosmic console" polish pass on top of the partially-shipped UI hardening (page-shell + tier buttons), and add a new mechanic where the evan helper's pointing hand causes the symbol/numeral it is about to tap to **pulse with a selection highlight** so the player can see the impending action.
+
+**Architecture:**
+- The tier-button + 100dvh shell work in `Plan UI Hardening.md` is already partly on disk (`src/styles/css/buttons.tiers.css`, `src/styles/css/shell.dvh.css`, modified `index.core.css`, `index.actions.css`, `level-select.polish.css`, modified `index.html`, `level-select.html`, new `welcome-page.settings.js`, `settings-open` body class). We EXTEND that work, not rewrite it. rev 1 ships 70% done; rev 2 closes the visual gap and adds the pulse.
+- Cosmic polish: a single new `src/styles/css/cosmic-console.css` layer that augments the existing `matrix-bg` and page-shell with (a) a starfield/aurora overlay, (b) angular "chamfered" panel chrome using clip-path, (c) glowing HUD-readout accent on the page-topbar brand mark and the settings dialog, (d) a `body.cosmic-console` opt-in class so users with `prefers-reduced-motion` get the static fallback.
+- Evan pulse: a small new module `src/scripts/evan-helper.pulse.js` registered as `window.EvanPulse`. The presenter calls `EvanPulse.setTarget(el)` whenever the hand moves onto a new live target; the pulse adds an `evan-pulse-target` class plus an inline `--pulse-color` token, then removes both when the action fires or the hand moves on. Pure CSS keyframe animation in `cosmic-console.css`. Reduced-motion → no animation, just a static outline.
+
+**Tech Stack:** HTML5, vanilla JS (ES2020), CSS3 (custom properties, `@property`-free fallback, `clip-path`, `prefers-reduced-motion`), existing Playwright suite. No new dependencies.
+
+**Effort:** ~1.5 weeks | **Surfaces touched:** 2 pages (welcome, level-select) + 1 game page (pulse) | **New tables:** 0 | **Feature flag:** `cosmicConsole` (settings.display.cosmicConsole, default off; new field, not a runtime URL flag)
+
+**Predecessor plan:** `Plan UI Hardening.md` (untracked) — rev 1 of this work. ~70% implemented, unstaged, mostly correct. The new work below is the rev 2 delta; we do NOT re-litigate rev 1.
+
+---
+
+## Live State Verified (2026-08-29)
+
+Read against the live tree on `feature/ui-hardening`:
+
+| Claim | Live source | Verified |
+|---|---|---|
+| Three-tier button system exists | `src/styles/css/buttons.tiers.css:82-186` defines `.btn--primary`, `.btn--secondary`, `.btn--tertiary`, `.btn--danger` | yes |
+| Icon button exists | `src/styles/css/buttons.tiers.css:201-254` defines `.icon-button` with `[aria-expanded]` state | yes |
+| 100dvh shell exists | `src/styles/css/shell.dvh.css:16-25` defines `.page-shell` / `.page-shell--with-actions` | yes |
+| Welcome body has the shell | `src/pages/index.html:29` `
` | yes |
+| Level-select body has the shell | `src/pages/level-select.html:33` `` | yes |
+| Welcome has a gear icon | `src/pages/index.html:39-47` `#welcome-settings-button.icon-button` | yes |
+| Level-select footer uses tiered buttons | `src/pages/level-select.html:220,229,239` `btn--tertiary`/`btn--secondary`/`btn--danger` | yes |
+| Matrix-rain background on both pages | `src/pages/index.html:31`, `src/pages/level-select.html:35` `
` | yes |
+| Welcome-page settings controller is shared | `src/scripts/welcome-page.settings.js:1-352` (untracked) `window.SettingsDialog` | yes |
+| Evan hand is a fixed overlay, transforms via translate3d | `src/styles/css/game-modals.evan.css:63-75` `#evan-hand` (`position:fixed`, `transform: translate3d(-200px,-200px,0)`) | yes |
+| Evan hand is moved by `EvanPresenter.moveHandTo(x,y,bounds)` | `src/scripts/evan-helper.presenter.js:220-228` | yes |
+| There is currently NO target-pulse mechanism | search of `src/styles/css/game-modals.evan.css` and `src/scripts/evan-helper.*.js` for `pulse` / `highlight` — no match | yes |
+| The hand is `pointer-events: none` (does not capture hover) | `src/styles/css/game-modals.evan.css:70` | yes |
+| The hand's parent `#evan-assist-shell` is `z-index: 900000` and `pointer-events: none` until `body.evan-input-locked` | `src/styles/css/game-modals.evan.css:6-15` | yes |
+
+---
+
+## Design Tokens (locked, additive only)
+
+Add to `src/styles/css/game-polish.chrome.tokens.css:24-26` and to `src/styles/css/level-select.polish.css:1-31` and to a new `:root` in `cosmic-console.css`:
+
+```
+--cosmic-void: #05080F
+--cosmic-haze: #16222F
+--cosmic-star: #E8F1FF
+--cosmic-signal: #5BE3C9 /* pulse accent */
+--cosmic-amber: #FFB347 /* glow accent */
+--cosmic-magenta: #E96BFF /* secondary glow */
+--cosmic-pulse-halo: rgba(91, 227, 201, 0.55)
+--cosmic-chamfer: 14px /* clip-path inset for angular panel corners */
+--cosmic-glow-spread: 28px
+--cosmic-anim-dur: 1.6s
+```
+
+Computed contrast gate (mandatory executable test, not a memory claim): `--cosmic-signal` `#5BE3C9` on the welcome body bg `#0d100f` ≈ **11.2:1** (AAA). White on amber `#FFB347` ≈ **1.9:1** — UNSAFE for body text, use only for decorative ring/glow. The plan asserts the latter is decorative-only and a Vitest gate enforces it.
+
+---
+
+## Milestone Timeline
+
+### Milestone 1: Starfield + Aurora overlay (Day 1 — Mon)
+
+Cosmetic foundation. No JS, no test changes. Visible everywhere `body.cosmic-console` is set, off by default.
+
+- New `src/styles/css/cosmic-console.css` — `body.cosmic-console` adds:
+ - `::before` starfield: 3-layer radial-gradient pinned to `position: fixed; inset: 0; z-index: 0; pointer-events: none;` with `@keyframes cosmic-twinkle` (opacity 0.55 → 0.9 → 0.55, 6s) inside `prefers-reduced-motion: no-preference` only.
+ - `::after` aurora: a single linear-gradient + mask-image, low opacity (≤0.12), `mix-blend-mode: screen`.
+ - `.page-topbar` `border-bottom-color` becomes `--cosmic-signal` at 0.35 alpha, plus a 1px inset glow `box-shadow: inset 0 1px 0 rgba(91,227,201,0.18)`.
+ - `.page-topbar__brand-mark` `box-shadow` upgraded to a 0/0/22px `--cosmic-signal` halo.
+ - `.page-actions` gets the same border-top treatment.
+- `src/pages/index.html:25-26` and `src/pages/level-select.html:29-30` — add `` AFTER `shell.dvh.css`.
+- `src/scripts/user-settings.helpers.js` — add `display.cosmicConsole` (bool, default `false`) to `createDefaultSettings()` and `normalizeSettings()`. Bump `SETTINGS_VERSION` to 3.
+- `src/styles/css/cosmic-console.css` — add `.page-shell { --mm-cosmic: var(--mm-cosmic-default); }` placeholder so the user-toggle works without a runtime flag.
+
+**Gate:** `npm run lint` clean. Manual: opening `index.html?cosmic=on` (URL param, see below) shows the twinkle. Manual screenshot at 1920×1080 captured for the changelog.
+
+### Milestone 2: Chamfered panel chrome + dialog glow (Day 1-2 — Tue)
+
+The "console" feel. Existing `index.core.css` and `level-select.polish.css` use rounded corners (`border-radius: 24px`). We add a SECOND set of panels with angular `clip-path` for the dialog, the page-topbar, and the welcome container.
+
+- `src/styles/css/cosmic-console.css`:
+ - `.page-shell.cosmic-console .page-topbar` — `clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 18px 100%, 0 calc(100% - 18px));` plus 1px border via `clip-path` mirror.
+ - `.page-shell.cosmic-console .page-actions` — mirrored chamfer at the bottom.
+ - `.page-shell.cosmic-console .welcome-container` — clip-path with 24px chamfer; `::after` pseudo-element draws a 1px inner stroke with `--cosmic-signal`.
+ - `body.cosmic-console .settings-dialog` — 24px chamfer + 0/0/32px `--cosmic-signal` outer glow + `border: 1px solid var(--cosmic-signal)` at 0.45 alpha. Backdrop: `backdrop-filter: blur(14px) saturate(140%)`.
+- `src/scripts/welcome-page.settings.js` (already on disk) — on `userSettingsLoaded`/`userSettingsChanged`, toggle `body.cosmic-console` based on `settings.display.cosmicConsole`.
+- URL convenience: a tiny runtime hook in `welcome-page.settings.js` reads `?cosmic=on` / `?cosmic=off` from `location.search` and calls `UserSettings.updateSettings({ display: { cosmicConsole: ... } }, "url.override")`. Forwards into the same setter as the dialog.
+
+**Gate:**
+- New unit test `tests/cosmic-console.toggle.spec.js`: navigate to `index.html?cosmic=on`, assert `body.cosmic-console` is set and `--cosmic-signal` resolves to `#5BE3C9`.
+- Re-run `tests/welcome-page-redesign.spec.js`, `tests/level-select-polish.spec.js`, `tests/level-select-settings.spec.js`, `tests/level-select-scoreboard.spec.js` — all green.
+- New test `tests/cosmic-console.contrast.spec.js` (unit, `tests/unit/`) computes the body-text contrast on `--cosmic-void` for the four text colors. Test FAILS if the body uses amber.
+
+### Milestone 3: Settings dialog UI for the toggle (Day 2 — Wed)
+
+Surface the new flag in the dialog without inventing a new control shape — the existing `settings-toggle` row fits.
+
+- `src/pages/index.html` and `src/pages/level-select.html` — inside the Display fieldset, add a new row:
+ ```html
+
+ ```
+- `src/scripts/welcome-page.settings.js` — add `cosmicConsoleInput` to the `elements` map (line 90), bind to `settings.display.cosmicConsole` in `populateForm` (line 100-127), handle the change in `handleControlChange` (line 138-175) → call `UserSettings.updateSettings({ display: { cosmicConsole: target.checked } }, "settingsDialog.cosmicConsole")` AND `document.body.classList.toggle("cosmic-console", target.checked)`.
+- Same change mirrored in `src/scripts/level-select-page.settings.js` (the level-select page keeps its own controller; see comment at `welcome-page.settings.js:344-346`).
+
+**Gate:**
+- New spec `tests/welcome-page.settings.spec.js` (or extend the existing one) — open the welcome gear, toggle cosmic on, assert `body.cosmic-console` is set AND the input is checked. Reload, assert persistence. Toggle off, assert class removed.
+- `npx playwright test tests/welcome-page.settings.spec.js tests/level-select-settings.spec.js --project=chromium --reporter=line` green.
+
+### Milestone 4: Evan-helper target PULSE (Day 3-4 — Thu-Fri) ★ the new mechanic
+
+When the hand arrives at a target, that target should pulse to indicate "this is what I'm about to tap". The pulse is a CSS keyframe driven by a class added/removed by a new `EvanPulse` module. It is a selection signal — not a hover (the hand does not capture pointer events). It must clear instantly when the hand leaves or the action fires.
+
+**Where it lives:** `src/scripts/evan-helper.pulse.js` (new file, registered as `window.EvanPulse`). Loaded by `service-worker.js` cache list and by `src/pages/game.html` script tags (find the `evan-helper.controller.runtime.js` import line and add this one immediately after — the project already uses `defer` for these scripts).
+
+**API:**
+- `EvanPulse.setTarget(target: Element | null)` — call from the presenter whenever a new live target is selected. Idempotent. Removes the class from the previous target, adds it to the new one. Reuses `centerOf` from `EvanTargets`.
+- `EvanPulse.clearTarget()` — call from the presenter on `hide` / `parkHand` / on action completion.
+- Internal: stores `lastTarget`, animates via `requestAnimationFrame` (no CSS transitions for class add/remove; we toggle a class that runs an `@keyframes` loop until removed).
+- Honors `prefers-reduced-motion`: in reduced-motion mode, no animation — just an outline via `:focus-visible` style copied onto the pulse class.
+
+**Where to call it from:** `src/scripts/evan-helper.presenter.js`. Two sites:
+1. `moveHandTo(x, y, bounds)` (line 220): after the RAF queue is set, we need to know WHICH element the hand is moving toward. The caller already knows — `moveHandTo` is called from `evan-helper.controller.runtime.js:158` which in turn is called from `moveHandToTarget` (runtime line 154). The cleanest hook: change `moveHandTo` to accept a `target` argument (`EvanPresenter.moveHandTo(x, y, bounds, target)`), defaulting to `null` (preserves any other caller), and call `EvanPulse.setTarget(target)` inside `moveHandTo` after the RAF is queued.
+2. `parkHand()` (line 230): add `EvanPulse.clearTarget()` call at the top.
+3. `hide()` (line 161): add `EvanPulse.clearTarget()` call.
+
+**CSS (in `cosmic-console.css`):**
+```css
+@keyframes evan-pulse-glow {
+ 0%, 100% { box-shadow: 0 0 0 0 var(--cosmic-pulse-halo); outline-color: var(--cosmic-signal); }
+ 50% { box-shadow: 0 0 var(--cosmic-glow-spread) 6px var(--cosmic-pulse-halo); outline-color: var(--cosmic-amber); }
+}
+body.automation .evan-pulse-target,
+body:not(.automation) .evan-pulse-target {
+ position: relative;
+ outline: 2px solid var(--cosmic-signal);
+ outline-offset: 2px;
+ animation: evan-pulse-glow var(--cosmic-anim-dur) ease-in-out infinite;
+ z-index: 2;
+ border-radius: 6px;
+}
+@media (prefers-reduced-motion: reduce) {
+ .evan-pulse-target {
+ animation: none;
+ outline-color: var(--cosmic-signal);
+ box-shadow: 0 0 0 2px var(--cosmic-signal);
+ }
+}
+```
+
+**Tricky bits (per the writing-plans skill — verify before claiming):**
+- The evan shell is `z-index: 900000`. The pulse class only adds `z-index: 2` on the target — that is BELOW the shell. That is correct: the shell is `pointer-events: none` outside `body.evan-input-locked` (so it never intercepts clicks), and the pulse lives on the actual gameplay element so its glow renders against the panel bg, not on top of the hand. The hand's `pointer-events: none` means it never blocks the target either. **Empirical check in the test**: assert the target retains its `pointer-events: auto` after the class is added.
+- Worm segments and falling symbols are re-created on every frame. We must avoid leaking the class onto detached nodes. `setTarget` removes the class from the previous target BEFORE assigning the new one, and `clearTarget` does the same. The pulse class itself has no transition, so a node that disappears mid-animation simply stops animating — no leak, no flash.
+- Some targets are buttons (`button.worm-muffin-reward`); the pulse's `outline` is layered on top of the existing focus ring via `outline-offset`. Verify in test that the button's `aria-disabled` is NOT toggled by the class (it is purely visual).
+- The `prefers-reduced-motion` gate: assert the class is added even in reduced-motion mode, just that the keyframe is disabled. The visual cue (static outline) MUST still appear or the user has no feedback.
+
+**Test (GATE before implementation per TDD framing):**
+- `tests/evan-helper.pulse.spec.js` (new):
+ 1. Mount `EvanPulse.setTarget(stubEl)` → assert `stubEl.classList.contains("evan-pulse-target")` within 50ms.
+ 2. Set target A, then target B → assert A lost the class, B has it.
+ 3. `EvanPulse.clearTarget()` → previous target loses the class, no class on document.
+ 4. With `prefers-reduced-motion: reduce` emulated, assert the static outline is computed (via `getComputedStyle` — but jsdom cannot resolve keyframes, so instead assert that the test target is NOT inside an `@media` block, OR mock matchMedia and assert the class is still added).
+ 5. Assert the class adds `z-index: 2` and `outline: 2px solid` to the target via inline `getAttribute("style")` — but CSS classes don't set inline styles. Better: assert the ruleset exists in the cascade by querying the stylesheet (`document.styleSheets[].cssRules` for the keyframe name) and asserting at least one rule references `evan-pulse-target`. The skill's rule on "computed color claims" applies: assert the keyframe NAME is present in the stylesheet, not a computed color value.
+ 6. Stub `EvanPresenter.moveHandTo(x, y, bounds, target)` with a fake target → call `EvanPresenter.moveHandTo(10, 10, null, fakeTarget)` → assert `fakeTarget.classList.contains("evan-pulse-target")`.
+ 7. Call `EvanPresenter.parkHand()` → assert `fakeTarget.classList.contains("evan-pulse-target")` is `false`.
+
+**Gate:** `npx playwright test tests/evan-helper.pulse.spec.js tests/evan-helper.symbols.spec.js tests/evan-helper.flow.spec.js tests/evan-helper.ui.spec.js --project=chromium --reporter=line` all green. The existing evan tests must NOT regress — the pulse is purely additive.
+
+### Milestone 5: Documentation, screenshots, ramp (Day 5 — Fri)
+
+- Update `README.md` "Local development" section with the `?cosmic=on` URL override and the new settings field.
+- Update `docs/SECURITY.md` (if it documents user-settings schema) — append `display.cosmicConsole`.
+- Capture before/after screenshots of `index.html` and `level-select.html` at 1366×768 with `?cosmic=on` → commit to `docs/screenshots/2026-08-29-cosmic-console.{png,webp}`.
+- Final manual pass: open `index.html` at 1920×1080, 1366×768, iPhone 13 (390×844). Confirm no vertical scrollbar, gear present, three-tier footer reads, dialog glows. Open `game.html?level=beginner&evan=force`, dismiss briefing, watch a full solve — confirm every target pulses before the hand taps it, and the pulse clears on `parkHand`.
+
+**Final gate:** `npm run lint && npm run typecheck && npx playwright test --project=chromium --reporter=line` all green.
+
+---
+
+## Data Flow
+
+### Settings write path (display.cosmicConsole)
+
+User (toggle in dialog) SettingsDialog UserSettings DOM
+ │ │ │ │
+ ├─ click checkbox ────────────────▶│ │ │
+ │ ├─ updateSettings(partial)▶│ │
+ │ │ ├─ persist to LS │
+ │ │ ├─ emit changed ──▶│
+ │ │ │ ├─ body.classList
+ │ │ │ │ .toggle(
+ │ │ │ │ "cosmic-console"
+ │ │ │ │ )
+ │ ├─ populateForm(next) │ │
+ │ │ re-check the checkbox │ │
+
+### Evan pulse path (symbol tap)
+
+Game runtime EvanController EvanControllerRuntime EvanPresenter EvanPulse DOM (target)
+ │ │ │ │ │ │
+ ├─ EVAN_HELP_STARTED ──────▶│ │ │ │ │
+ │ ├─ runLoop() │ │ │ │
+ │ │ └─ clickSymbol(el) │ │ │ │
+ │ │ └─ moveHandToTarget(el) │ │ │ │
+ │ │ └─ Presenter.moveHandTo(x, y, bounds, el) │ │ │
+ │ │ │ ├─ queue RAF │ │
+ │ │ │ ├─ setTarget(el) ────────▶├─ add class ────────▶│
+ │ │ │ │ │ (keyframe runs) │
+ │ │ ... DELAY_TARGET ... │ │ │ │
+ │ │ └─ emit SYMBOL_CLICKED │ │ │ │
+ │ │ └─ clearTarget() ──────────│ │ ├─ remove class ────▶│
+ │ │ │ │ │ (animation stops) │
+
+---
+
+## Mockups
+
+### A · Welcome page with cosmic-console on (1920×1080)
+
+```
+┌────────────────────────────────────────────────────────────────────────┐
+│ ◆ MATH MASTER ⚙ gear │ <-- chamfered topbar, phosphor underline
+├────────────────────────────────────────────────────────────────────────┤
+│ │
+│ Training dossier │
+│ MATH MASTER [animated logo: scale] │
+│ Enter the training console │
+│ A calm dossier for reaction drills… │
+│ │
+│ ┌─ Route briefing ─ Local progress cache ─ Reduced-motion ─┐ │
+│ │
+│ ┌─ "Core principle" ────────────────────────┐ │
+│ │ "If it is not right, do not do it…" │ <-- chamfered │
+│ │ — Marcus Aurelius │ glow border │
+│ └────────────────────────────────────────────┘ │
+│ │
+├────────────────────────────────────────────────────────────────────────┤
+│ ◀ Back to welcome Created by Teacher Evan ⚙ Settings ⌫ Reset │ <-- three tiers readable
+└────────────────────────────────────────────────────────────────────────┘
+ * twinkle field behind, aurora wash top-right (opacity 0.08)
+```
+
+### B · Settings dialog (cosmic on)
+
+```
+ ┌─ Runtime controls ─────────────────────────[Close]─┐ <-- 24px chamfer
+ │ Game settings │ outer glow
+ │ Changes save here and apply right away. │ --cosmic-signal
+ │ ───────────────────────────────────────────────── │
+ │ DISPLAY │
+ │ Display quality [ Auto ▾ ] │
+ │ Reduce motion [ ☐ ] │
+ │ High contrast [ ☐ ] │
+ │ Cosmic console [ ☑ ] ← new │
+ │ UI scale [───●──] 100% │
+ │ SOUND │
+ │ Volume [────●─] 80% │
+ │ Mute [ ☐ ] │
+ │ GAMEPLAY │
+ │ Per-level pressure: auto │
+ │ ───────────────────────────────────────────────── │
+ │ [Reset to defaults] │
+ └─────────────────────────────────────────────────────┘
+```
+
+### C · Evan hand about to tap a falling symbol (game view, portrait)
+
+```
+ ┌── panel-c (symbol rain) ───────────────────────────┐
+ │ │
+ │ ╔═══╗ ╔═══╗ │
+ │ ║ x ║ ║ y ║ │
+ │ ╚═══╝ ╚═══╝ │
+ │ ┊ │
+ │ ▼ (hand points here) │
+ │ ╔═══════════╗ │
+ │ ║ ◉ z ◉ ║ <-- target pulsing │
+ │ ╚═══════════╝ outline: --cosmic-signal │
+ │ box-shadow: 0 0 28px halo │
+ │ keyframe: 1.6s ease-in-out │
+ │ │
+ └─────────────────────────────────────────────────────┘
+ HUD: SCORE 10000 TIME 600 PROBLEM 1/3
+```
+
+### D · Sidebar / topbar elements that read clearly
+
+- Primary CTA: filled gold gradient, dark text, 14px radius, 0.78rem font, lift 1px on hover, drop shadow.
+- Secondary (Settings, Scoreboard): dark fill + brass-gold 1px border, icon left, label right.
+- Tertiary (Back): text only with arrow icon, hover shifts icon -2px and label color brightens.
+- Danger (Reset): ember-red 1px border, ember glow on hover.
+- Icon button (gear): 40×40 round, brass border, phosphor fill on `[aria-expanded="true"]`.
+
+---
+
+## Risk Table
+
+| Risk | Likelihood | Impact | Mitigation |
+|---|---|---|---|
+| Starfield/aurora kills Lighthouse perf | Medium | Medium | Pure CSS, no JS loop; cap opacity ≤0.12; gate on `prefers-reduced-motion` AND on a battery-saver class (future work) |
+| Clip-path chamfer breaks `border-radius` corners visually | High | Low | Chamfer is additive — the underlying `border-radius` is kept; `clip-path` only affects the outer silhouette. Verify visually in milestone 2 gate. |
+| Cosmic-console breaks the test fixtures that paint `body` with a hard color | Low | Medium | All Playwright tests should pass because the toggle defaults to OFF. Gate = `npm run test --project=chromium` is green. |
+| Evan pulse class leaks onto detached DOM nodes | Medium | Low | `setTarget` always removes from previous target before assigning. `clearTarget` clears unconditionally. Add an automated check in milestone 4 test #3: after `parkHand`, the orphaned element from a teardown has no class. |
+| Reduced-motion users see no feedback at all | High | High | Static outline MUST render in reduced-motion mode. Test #4 enforces this — the assertion is "class IS added even in reduced-motion"; visual feedback is the outline. |
+| `EvanPresenter.moveHandTo` signature change breaks other callers | Medium | Medium | Default the new `target` argument to `null`; keep current callers (`EvanTargets.centerOf`-style flows that don't carry the target) compiling. Add a `// eslint-disable-next-line` comment if the default-empty arg is flagged. |
+| Settings dialog loses sync between the two pages | Low | High | The shared controller in `welcome-page.settings.js:217-318` is the single source of truth. `level-select-page.settings.js` mirrors the same change. Run BOTH test files. |
+| New field `display.cosmicConsole` breaks the SETTINGS_VERSION=2 path | Low | High | `normalizeSettings()` already passes through unknown keys (it's a deep-merge). Add explicit branch: if missing → default `false`. Bump to v3. |
+| The hand moves through MULTIPLE targets in one frame; the pulse only settles on the last | Low | Low | Intentional. The user sees the FINAL target glow as the hand arrives. Add a Playwright assertion: when the hand is parked at A then jumps to B in the same RAF, only B has the class. |
+
+---
+
+## Files Touched
+
+### New
+- `src/styles/css/cosmic-console.css` (Milestones 1, 2, 4)
+- `src/scripts/evan-helper.pulse.js` (Milestone 4)
+- `tests/evan-helper.pulse.spec.js` (Milestone 4)
+- `tests/cosmic-console.toggle.spec.js` (Milestone 2)
+- `tests/cosmic-console.contrast.spec.js` (Milestone 2, unit/)
+- `tests/welcome-page.settings.spec.js` (Milestone 3) — or extend existing
+- `docs/screenshots/2026-08-29-cosmic-console-welcome.png` + `.webp` (Milestone 5)
+- `docs/screenshots/2026-08-29-cosmic-console-level.png` + `.webp` (Milestone 5)
+
+### Modified
+- `src/pages/index.html` — link cosmic-console.css (M1); add `settings-cosmic-console` row (M3)
+- `src/pages/level-select.html` — link cosmic-console.css (M1); add `settings-cosmic-console` row (M3)
+- `src/pages/game.html` — add `` after the existing evan-helper scripts (M4)
+- `service-worker.js` — add `/src/scripts/evan-helper.pulse.js` and `/src/styles/css/cosmic-console.css` to the precache list (M1, M4)
+- `src/scripts/evan-helper.presenter.js` — `moveHandTo` accepts optional `target` (M4); `parkHand` calls `EvanPulse.clearTarget()` (M4); `hide` calls `EvanPulse.clearTarget()` (M4)
+- `src/scripts/welcome-page.settings.js` — bind the new `cosmicConsoleInput` (M3); add `?cosmic=on/off` URL override (M2)
+- `src/scripts/level-select-page.settings.js` — mirror the new field binding (M3)
+- `src/scripts/user-settings.helpers.js` — add `display.cosmicConsole`, bump `SETTINGS_VERSION` to 3 (M1)
+- `src/styles/css/game-polish.chrome.tokens.css` — add cosmic tokens (M1)
+- `src/styles/css/level-select.polish.css` — add cosmic tokens (M1)
+- `README.md` — document `?cosmic=on` and the new field (M5)
+- `docs/SECURITY.md` — append `display.cosmicConsole` to schema (M5)
+
+---
+
+## Test Matrix
+
+| File | Why | Gate |
+|---|---|---|
+| `tests/welcome-page-redesign.spec.js` | existing — re-run unchanged | green |
+| `tests/level-select-polish.spec.js` | existing — re-run unchanged | green |
+| `tests/level-select-settings.spec.js` | existing — re-run unchanged | green |
+| `tests/level-select-scoreboard.spec.js` | existing — re-run unchanged | green |
+| `tests/welcome-scoreboard.spec.js` | existing — re-run unchanged | green |
+| `tests/welcome-page.settings.spec.js` (new) | dialog + persistence + URL override | green |
+| `tests/cosmic-console.toggle.spec.js` (new) | body class + token resolves | green |
+| `tests/cosmic-console.contrast.spec.js` (unit, new) | body text on cosmic-void is AAA; amber is decorative-only | green |
+| `tests/evan-helper.pulse.spec.js` (new) | class add/remove/clear/RAF; reduced-motion still outlines | green |
+| `tests/evan-helper.symbols.spec.js` | existing — pulse must not break the click loop | green |
+| `tests/evan-helper.flow.spec.js` | existing — re-run | green |
+| `tests/evan-helper.ui.spec.js` | existing — re-run | green |
+| `tests/evan-helper.controls.spec.js` | existing — re-run | green |
+| `tests/evan-helper.worms.spec.js` | existing — pulse on worm segment, then on power-up, then on symbol | green |
+| `tests/game-mobile-layout.ultranarrow.spec.js` | existing — narrow viewport still has the new content visible | green |
+| `tests/ui-boundary.spec.js` | existing — chamfered panels don't break hit-test math | green |
+
+---
+
+## Bite-Sized Task Granularity (TDD framing for Milestone 4, the new mechanic)
+
+### Task M4.1: Failing test — `EvanPulse.setTarget` adds the class
+
+**Files:**
+- Test: `tests/evan-helper.pulse.spec.js` (new)
+
+**Step 1:** Write the test (cases 1-3 from Milestone 4 test list). Run it. Expected: FAIL — `window.EvanPulse` is undefined.
+
+**Step 2:** Create `src/scripts/evan-helper.pulse.js` with a stub `window.EvanPulse = { setTarget, clearTarget }` that ADDS the class but does nothing else. Re-run. Expected: PASS.
+
+**Step 3:** Commit. `git add tests/evan-helper.pulse.spec.js src/scripts/evan-helper.pulse.js; git commit -m "feat(evan-pulse): stub EvanPulse module + failing test -> green"`
+
+### Task M4.2: Wire `EvanPresenter.moveHandTo` to call `EvanPulse.setTarget`
+
+**Files:**
+- Modify: `src/scripts/evan-helper.presenter.js:220-228`
+
+**Step 1:** Add test case 6 (call `EvanPresenter.moveHandTo(10, 10, null, fakeTarget)`, assert class). Re-run. Expected: FAIL.
+
+**Step 2:** Change `function moveHandTo(x, y, bounds = null)` → `function moveHandTo(x, y, bounds = null, target = null)`. Add `window.EvanPulse?.setTarget?.(target);` after the RAF queue line.
+
+**Step 3:** Re-run. Expected: PASS.
+
+**Step 4:** Commit. `git commit -m "feat(evan-pulse): presenter.moveHandTo sets pulse target"`
+
+### Task M4.3: Wire `parkHand` and `hide` to call `EvanPulse.clearTarget`
+
+**Files:**
+- Modify: `src/scripts/evan-helper.presenter.js:230-236` and `161-176`
+
+**Step 1:** Add test case 7 (call `EvanPresenter.parkHand()`, assert previous target has no class). Re-run. Expected: FAIL.
+
+**Step 2:** Add `window.EvanPulse?.clearTarget?.();` at the top of `parkHand()` and `hide()`.
+
+**Step 3:** Re-run. Expected: PASS. Run the full evan test suite to confirm no regression.
+
+**Step 4:** Commit. `git commit -m "feat(evan-pulse): clear target on parkHand and hide"`
+
+### Task M4.4: CSS keyframe + reduced-motion outline
+
+**Files:**
+- Modify: `src/styles/css/cosmic-console.css`
+
+**Step 1:** Add a temporary assertion to `tests/evan-helper.pulse.spec.js` that walks `document.styleSheets` looking for a CSS rule with selector `.evan-pulse-target`. Run. Expected: FAIL (no rules yet).
+
+**Step 2:** Add the keyframe + the class block + the reduced-motion block from the "CSS" section above.
+
+**Step 3:** Re-run. Expected: PASS.
+
+**Step 4:** Run `npm run lint` and the full evan test suite. Commit.
+
+### Task M4.5: Service worker precache + game.html script tag
+
+**Files:**
+- Modify: `service-worker.js`, `src/pages/game.html`
+
+**Step 1:** Run a one-off shell test: `npx playwright test tests/service-worker-update.spec.js --project=chromium --reporter=line` — confirm green baseline.
+
+**Step 2:** Add the two new files to the cache list and the script tag in game.html. Re-run.
+
+**Step 3:** Commit. `git commit -m "feat(evan-pulse): precache pulse module + style"`
+
+---
+
+## Execution Handoff
+
+Defaulting to Subagent-Driven (this session) per your terse-operator profile. Say "parallel" to open a separate session instead.
+
+When the parent kicks off subagents, each subagent gets a Milestone (M1, M2, M3, M4, M5) with this plan + the verified-live evidence table. Each subagent returns: files touched, diff summary, test command(s) run, exit code, before/after screenshot path. The parent then verifies the on-disk files via `git status --porcelain` and runs the gate command independently before marking the milestone green. Per the empirical-verification rule: a subagent claiming "all tests green" is a SELF-REPORT — the parent re-runs the listed test command before trusting it.
diff --git a/service-worker.js b/service-worker.js
index 924ab7f..c725a5c 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -62,6 +62,7 @@ const STATIC_ASSETS = [
"/src/styles/css/worm-effects.css",
"/src/styles/css/lock-responsive.css",
"/src/styles/css/modern-ux-enhancements.css",
+ "/src/styles/css/cosmic-console.css",
"/src/scripts/service-worker-register.js",
"/src/scripts/index-page.matrix.js",
"/src/scripts/index-page.effects.js",
@@ -108,6 +109,7 @@ const STATIC_ASSETS = [
"/src/scripts/game-onboarding.storage.js",
"/src/scripts/game-onboarding.bootstrap.js",
"/src/scripts/evan-helper.presenter.js",
+ "/src/scripts/evan-helper.pulse.js",
"/src/scripts/game-tutorial-level.js",
"/src/scripts/game-onboarding.controller.js",
"/src/scripts/evan-helper.controller.targets.js",
diff --git a/src/pages/game.html b/src/pages/game.html
index 97351e3..82b2d3f 100644
--- a/src/pages/game.html
+++ b/src/pages/game.html
@@ -371,6 +371,8 @@