diff --git a/.claude/skills/a11y-audit.md b/.claude/skills/a11y-audit.md
new file mode 100644
index 00000000..b8c535da
--- /dev/null
+++ b/.claude/skills/a11y-audit.md
@@ -0,0 +1,145 @@
+---
+name: a11y-audit
+description: >
+ On-demand accessibility audit using a multi-phase Red Team / Blue Team pipeline.
+ Load when asked to audit a component, page, or user flow for accessibility.
+ Combines persona-based functional simulation, semantic code audit, axe output
+ integration, and severity-weighted synthesis into a structured report.
+---
+
+# Accessibility Audit Skill
+
+Run a three-phase audit and synthesize the findings into a risk-weighted report.
+
+---
+
+## Phase 1 — Red Team: Persona Simulation (Functional Friction)
+
+Ignore code mechanics. Focus on **task success, cognitive load, and human-centred design**
+for each of the six functional personas below. Flag issues that automated scanners miss.
+Do not report things an axe scan would catch — those belong in Phase 3.
+
+### 1. Screen Reader Navigator (Non-Visual)
+
+Navigates linearly via audio output. Flag:
+- Vague or duplicate link text ("read more", "view" repeated on every card)
+- Missing landmark structure or unlabelled landmarks
+- Dynamic content updates (filters, route changes) not announced
+- Heading order that skips levels or misrepresents the page hierarchy
+- Missing `aria-current="page"` on active nav links
+
+### 2. Power Keyboard User (Motor Limit)
+
+Uses only Tab, Shift+Tab, Enter, Space, and arrow keys. Flag:
+- Focus traps with no Escape route
+- Tab order that does not match visual reading order
+- Interactive elements not reachable by Tab (custom elements without `tabindex`)
+- Focus lost after dynamic UI changes (modal close, filter apply, route change)
+- Missing skip link or skip link with no visible focus state
+
+### 3. Magnification Expert (Low Vision)
+
+Navigates with viewport zoomed to 200–400%. Flag:
+- Horizontal scrolling required at 400% (reflow failure — WCAG 1.4.10)
+- Sticky/fixed elements that consume most of the viewport at high zoom
+- Text truncated with `overflow: hidden` that cannot be reached
+- Focus indicator too small to see when zoomed (less than 2px effective)
+
+### 4. Cognitive Strategist (Neurodivergent)
+
+Sensitive to clutter, complex language, and unpredictable behaviour. Flag:
+- Inconsistent UI controls across pages (same action, different interaction)
+- Missing or ambiguous page titles that do not reflect where the user is
+- Complex multi-step flows with no progress indicator
+- Error states that do not clearly identify the problem and suggest a fix
+- Auto-advancing UI (carousels, toasts) that interrupt the user's reading
+
+### 5. Vestibular User (Motion Sensitivity)
+
+Vulnerable to motion sickness from animations. Flag:
+- Any animation not gated by `@media (prefers-reduced-motion: no-preference)`
+- Parallax or scroll-linked motion effects
+- Auto-playing transitions triggered without user intent
+- Fast or flashing animations in any mode (WCAG 2.3.1 — three flashes threshold)
+
+### 6. Distracted / Fatigued User (Situational Limit)
+
+Navigating under high cognitive load (noisy environment, stressful task). Flag:
+- Unclear system status after actions (no confirmation, no loading state)
+- Session or consent state that is not visually obvious
+- Focus reset to the top of the page after every interaction
+- CTA labels that do not clearly state the outcome ("Submit" vs. "Send report")
+
+---
+
+## Phase 2 — Red Team: Semantic Audit (Manual Code Reasoning)
+
+Switch to code-aware audit mode. Focus on issues that require human judgement:
+
+- **Meaningful sequence (WCAG 1.3.2):** If CSS is stripped, does the logical reading order persist? Check flex/grid layout elements for visual vs. DOM order mismatch.
+- **Link text in context (WCAG 2.4.4):** Do link texts make sense when read in isolation by a screen reader? "View" on every adventure card does not.
+- **ARIA state accuracy (WCAG 4.1.2):** Is `aria-expanded` updated when dropdowns open? Is `aria-current` set on the active route? Is `aria-hidden` misapplied to focusable content?
+- **Status messages (WCAG 4.1.3):** Are dynamic updates (filter results, toast notifications, consent changes) announced via `aria-live` without forcibly moving focus?
+- **Accessible name computation:** Does the accessible name for every interactive element match what a screen reader would actually announce? (Use `Accessible Name and Description Computation 1.2` logic.)
+- **Colour independence:** Is information conveyed by colour alone anywhere? (Difficulty badges, status indicators, link underlines.)
+
+---
+
+## Phase 3 — Deterministic Input: Axe Results
+
+Run the existing test suite to collect axe output:
+
+```bash
+npm run build && npm run test:e2e 2>&1
+```
+
+The Playwright smoke tests in `e2e/smoke.spec.ts` run axe-core with tags
+`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `wcag22aa`, and `best-practice`
+in both dark and light mode against every prerendered route.
+
+Treat axe output as ground truth for mechanical violations. Do not duplicate these
+findings in Phase 1 or Phase 2. Use them as raw data for Phase 4.
+
+If axe output is not available (no build), note this and proceed with Phases 1 and 2 only.
+
+---
+
+## Phase 4 — Blue Team: Synthesis
+
+Merge Phase 1, Phase 2, and Phase 3 findings. Apply these rules before writing the report:
+
+1. **Reframe pure UX issues:** If a "usability" finding creates a functional barrier for a specific persona, cite the persona and the WCAG criterion. If it has no accessibility impact, drop it.
+2. **De-duplicate:** Merge overlapping findings. If Phase 1 found "confusing button label" and Phase 2 found "missing ARIA label on the same button", combine them into one finding.
+3. **Cumulative friction upgrade:** If three or more Low/Moderate findings cluster on the same Critical User Journey (e.g., the adventure filter flow, the consent banner, the challenge detail page), upgrade the overall severity of that flow to Critical.
+4. **Resolve axe Incomplete flags:** Review every axe `incomplete` / `needs review` flag and provide a definitive manual ruling: confirmed violation, confirmed pass, or cannot determine without AT testing.
+
+---
+
+## Output Format
+
+Generate the report as structured Markdown. For every confirmed finding:
+
+```
+## [Severity]: [Finding Title]
+
+**Persona impact:** [Who is impacted and how their functional experience is degraded]
+
+**Evidence:** [Specific page, component, DOM snippet, or user flow step]
+
+**WCAG:** [Criterion number and name, e.g. 2.4.4 Link Purpose (In Context) — AA]
+
+**Fix:** [Specific aria attribute, HTML change, or CSS adjustment — not vague advice]
+```
+
+Severity levels: **Critical** (blocks task completion), **High** (significant barrier, workaround unreasonable), **Medium** (friction, workaround exists), **Low** (best-practice gap, minimal impact).
+
+End the report with a **Cumulative Friction Summary** noting any user journeys where multiple findings cluster.
+
+---
+
+## Strict Rules
+
+- Do not hallucinate DOM elements not present in the provided code or test output.
+- Do not report mechanical issues (missing alt, basic contrast) during Phases 1 and 2; leave those to Phase 3.
+- Do not give vague remediation ("make the button accessible"). Provide the specific attribute, element, or CSS change.
+- Do not mark an axe Incomplete flag as a finding without a manual ruling.
diff --git a/.claude/skills/keyboard.md b/.claude/skills/keyboard.md
new file mode 100644
index 00000000..fa21557b
--- /dev/null
+++ b/.claude/skills/keyboard.md
@@ -0,0 +1,142 @@
+---
+name: keyboard
+description: >
+ Load this skill for every project containing interactive UI elements —
+ buttons, links, modals, dropdowns, sliders, tabs, carousels, or any
+ custom widget. Under no circumstances create an interactive component that
+ cannot be fully operated by keyboard alone. Absolutely always ensure visible
+ focus indicators, logical tab order, and no keyboard traps.
+source: https://github.com/mgifford/accessibility-skills/blob/main/skills/keyboard/SKILL.md
+---
+
+# Keyboard Accessibility Skill
+
+Apply these rules to every interactive UI element and feature.
+
+## Severity Scale
+
+| Level | Meaning |
+|---|---|
+| **Critical** | Blocks task completion entirely for keyboard and AT users |
+| **Serious** | Significantly impairs keyboard access; workaround unreasonable |
+| **Moderate** | Creates friction for keyboard users; workaround exists |
+| **Minor** | Best-practice gap; marginal keyboard impact |
+
+## Critical: No Keyboard Trap
+
+Users must never become unable to move focus away from a component using standard keys
+(Tab, Shift+Tab, Escape, arrow keys). The only exception is an intentional modal dialog
+trap where Escape closes the dialog and returns focus to the trigger.
+
+## Critical: Expected Key Behaviours
+
+| Control | Required keys |
+|---|---|
+| Button | `Enter`, `Space` |
+| Link | `Enter` |
+| Checkbox | `Space` to toggle |
+| Radio group | Arrow keys to move; `Space` to select |
+| Dialog | `Escape` to close; focus trapped inside while open |
+| Tab widget | Arrow keys between tabs; `Enter`/`Space` to activate |
+| Combobox | Arrow keys in list; `Enter` to select; `Escape` to collapse |
+
+## Critical: Dialog Focus Management
+
+Prefer the `inert` attribute (baseline 2023, supported in all modern browsers).
+
+```js
+function openDialog(dialog, trigger) {
+ document.querySelectorAll('body > *:not(#dialog-container)')
+ .forEach(el => el.setAttribute('inert', ''));
+ dialog.removeAttribute('hidden');
+ dialog.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')?.focus();
+}
+
+function closeDialog(dialog, trigger) {
+ document.querySelectorAll('[inert]').forEach(el => el.removeAttribute('inert'));
+ dialog.setAttribute('hidden', '');
+ trigger.focus();
+}
+
+dialog.addEventListener('keydown', e => {
+ if (e.key === 'Escape') closeDialog(dialog, trigger);
+});
+```
+
+This site uses shadcn `Dialog` (Radix UI) for modals. Radix handles focus trapping
+automatically. Verify `inert` is applied to background content and `Escape` works.
+
+## Serious: Focus Visibility
+
+Every focusable element must have a clear, persistent visible focus indicator.
+This project uses the pattern:
+```
+focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm
+```
+Never remove this. Never substitute `ring-primary/xx` for `ring-ring`.
+
+WCAG 2.4.11 minimum: 2px thick, 3:1 contrast against adjacent colours, visible in both modes.
+
+## Serious: Focus Not Obscured (WCAG 2.4.12)
+
+Sticky headers or floating elements can cover the focused element. If this site gains
+a sticky header, add:
+
+```css
+:focus { scroll-margin-top: var(--sticky-header-height, 4rem); }
+```
+
+## Serious: Focus Order
+
+- Use semantic DOM order as the primary mechanism
+- Never use positive `tabindex` values — they override DOM order globally
+- `tabindex="0"` — only to make custom widgets focusable
+- `tabindex="-1"` — only for programmatic focus targets (skip link anchors, modal management)
+
+## Serious: Roving Tabindex for Composite Widgets
+
+Composite widgets (toolbars, radio groups, tab lists) must use roving tabindex so only
+one item is in the tab stop at a time and arrow keys move within the group.
+
+```html
+
+
+
+
+```
+
+See [WAI-ARIA APG: Roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex).
+
+## Moderate: Touch Targets (WCAG 2.5.8)
+
+```css
+button, a, [role="button"] {
+ min-width: 24px;
+ min-height: 24px;
+ /* Recommended: 44×44 for primary actions */
+}
+```
+
+Never use `user-scalable=no` in the viewport meta tag.
+
+## Definition of Done Checklist
+
+- [ ] Tab through entire page: logical order, no unexpected skips
+- [ ] Visible focus indicator on every focusable element in both light and dark modes
+- [ ] All interactive elements activatable with correct keys per widget type
+- [ ] No keyboard trap (except intentional modal trap with working Escape)
+- [ ] Dialog: background content `inert` on open; focus returns to trigger on close
+- [ ] Skip link present, first in DOM, visible on focus, target has `tabindex="-1"`
+- [ ] Composite widgets use roving tabindex
+- [ ] Touch targets meet 24×24px minimum
+
+## Key WCAG Criteria
+
+- 2.1.1 Keyboard (A)
+- 2.1.2 No Keyboard Trap (A)
+- 2.4.3 Focus Order (A)
+- 2.4.7 Focus Visible (AA)
+- 2.4.11 Focus Appearance (AA, WCAG 2.2)
+- 2.4.12 Focus Not Obscured (AA, WCAG 2.2)
+- 2.5.3 Label in Name (A)
+- 2.5.8 Target Size Minimum (AA, WCAG 2.2)
diff --git a/.claude/skills/navigation.md b/.claude/skills/navigation.md
new file mode 100644
index 00000000..b870caa1
--- /dev/null
+++ b/.claude/skills/navigation.md
@@ -0,0 +1,132 @@
+---
+name: navigation
+description: >
+ Load this skill whenever the project contains navigation components —
+ primary navigation menus, dropdown menus, breadcrumbs, pagination, mobile
+ hamburger menus, or in-page jump navigation. Under no circumstances create
+ navigation without proper landmark roles, keyboard support, and accessible
+ labels. Absolutely always wrap navigation in