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