# Accessibility

The client targets **WCAG 2.2 AA**. This doc records how accessibility is
built in, what is verified automatically, and the deliberate boundaries.

## Tooling & gates

- **axe-core via Playwright** (`@axe-core/playwright`):
  [`tests/e2e/a11y-axe.spec.ts`](../frontend/tests/e2e/a11y-axe.spec.ts)
  scans every top-level view — login, lobby, lobby/create, and the
  in-game map / report / mail / battle / science-designer / table — with
  the `wcag2a wcag2aa wcag21a wcag21aa wcag22aa` rule tags and asserts
  zero violations. It runs once, on the `chromium-desktop` project (axe's
  contrast and computed-role checks need one real engine; the
  webkit/mobile projects add cost without new signal).
- **Keyboard navigation** (Playwright):
  [`tests/e2e/a11y-keyboard.spec.ts`](../frontend/tests/e2e/a11y-keyboard.spec.ts)
  covers the skip link, Escape-closes-and-restores-focus on menus, and
  the sidebar tab arrow-key pattern.
- **Svelte compiler a11y lint** is on (no suppressions); `pnpm check`
  fails on a11y warnings, so basic role/label/structure issues are caught
  at build time.

## Shared primitives

- `.sr-only` (in [`base.css`](../frontend/src/lib/theme/base.css)) —
  visually hidden, available to assistive tech.
- `.skip-link` — visible only on focus; each layout renders one as its
  first focusable element, pointing at a `tabindex="-1"` main region.
- [`trapFocus`](../frontend/src/lib/a11y/focus-trap.ts) — Svelte action
  for modal dialogs: moves focus in (to `[data-autofocus]`), cycles
  Tab/Shift+Tab within, restores focus on close.
- [`restoreFocus`](../frontend/src/lib/a11y/restore-focus.ts) — Svelte
  action for non-modal popovers/menus: returns focus to the trigger when
  the surface closes, without trapping.
- `--color-focus` token drives a single visible focus ring app-wide
  (`:where(*):focus-visible` in `base.css`).

## Coverage by area

- **Landmarks.** The in-game shell is `header` (banner) + `main` +
  `aside` (complementary); login/lobby/create each wrap content in a
  `main#main-content`. Every layout has a skip link to its main.
- **Forms** (login, lobby, create). Every control has an associated
  label; submit/validation errors use `role="alert"`, async status uses
  `role="status"`, so they are announced.
- **Sidebar tabs** follow the WAI-ARIA tabs pattern: `role="tablist"` /
  `tab` / `tabpanel`, roving `tabindex`, arrow + Home/End keys with
  automatic activation, `aria-controls` / `aria-labelledby` linking tabs
  to the panel.
- **Menus & popovers** (account, view, turn navigator, map toggles,
  mobile "more"): `aria-haspopup` / `aria-expanded` triggers, Escape and
  outside-click dismissal, and `restoreFocus` so focus is never dropped
  to `<body>`.
- **Modal dialog** (mail compose): `role="dialog"` + `aria-modal` +
  `aria-labelledby`, `trapFocus`, Escape to close.

## The map canvas

The PixiJS/WebGL map is a visual surface that cannot be made keyboard- or
screen-reader-navigable without a bespoke effort. It is therefore handled
as an **accessible alternative**: the `<canvas>` carries an `aria-label`
naming it and pointing to where the same information lives, and every
datum it shows — planets, ship groups, routes — is reachable by keyboard
through the sidebar inspector and the tables view. In-canvas keyboard
navigation (cursoring between planets) is intentionally out of scope; it
is noted as a future enhancement in [`ROADMAP.md`](../ROADMAP.md).

## Boundaries / future work

- The deep AA pass focuses on login, lobby, and the in-game shell
  (acceptance surfaces). Secondary views pass the axe structural scan;
  exhaustive keyboard/SR polish of every view interaction is incremental.
- The battle scene (`battle-player/battle-scene.svelte`) is a data-viz
  surface (see [`design-system.md`](design-system.md)); its content is
  summarised by the adjacent text protocol log.
- In-canvas keyboard navigation for the map (deferred, above).
