T7.25 of Epic #14560 (map v1.1) — the a11y floor for the cockpit surface: an operator-grade tool must be keyboard-drivable and legible, and "stunning" that fails contrast is a design defect by the epic's own bar. Filed under the decomposition sprint (D#14561).
The Problem
The SSOT's dark palette runs several low-ink tiers (--ink-dim, --ink-faint) over layered panels — contrast must be VERIFIED per token pair, not assumed. The control verbs (#14611/#14612) and the drill path (#14608/#14610) are keyboard-critical: an operator mid-incident cannot be mouse-gated. Reduced-motion gating exists per-leaf; this leaf audits it end-to-end.
The Architectural Reality
Token-level contrast audit: every ink-tier × surface-tier pair in #14578 measured against WCAG AA for its usage class (body text vs meta text vs disabled) — results recorded IN the token reference doc (deltas = recorded design decisions with Grace's design authority in the loop).
Focus order: the shell's zone composition (#14615) defines the tab topology — grid → card → controls → stream → rails; pop-out windows keep their own coherent order.
Verification: unit-level assertions where mechanical (focus sequence fixtures, prefers-reduced-motion config paths) + an audit doc for the judgment calls.
The Fix
One PR: the contrast audit table (token-pair × usage-class × measured ratio × verdict/fix) + focus-order fixtures for the core paths + the end-to-end reduced-motion audit + fixes for any failures found (or split-out leaves if a fix is design-material).
Acceptance Criteria
Contrast table covers every shipped ink×surface pair with measured ratios; failures fixed or ticketed with design-authority sign-off.
AC2 (reworded 2026-07-17 by the author after V-B-A — the original described a shell that never existed). The keyboard-reachability invariants hold, preset-independently: (a) every steady-state zone is reachable in DOM order with no tabIndex overrides (the shell has none — focus order IS DOM order); (b) the drill button is the only focusable element inside a card (the card root stays a non-interactive listitem); (c) autoHidden zones (detail, perspectives) are not tab stops until revealed; (d) the drill/pop-out path stays green.
Superseded: "Focus-order fixtures green for grid→card→controls→stream→rails" — the shipped order is controls → grid → stream → detail → perspectives (FleetCockpit composes [toolbar.fm-cockpit-bar, dockConfig]; cockpitDockDocument:27-30 orders fleet/stream/detail/perspectives). Three errors in the old sentence, all its own: controls are FIRST and should be (a toolbar tabs before its content — reordering the shell to match would worsen a11y); card is not a zone (cards live inside the grid; agentCard.spec.mjs:219-223 deliberately pins the root non-focusable); rails = detail+perspectives, both autoHidden, so not steady-state stops. It cited #14615 as defining the topology — that ticket is CLOSED, which is when the sentence went stale.
Already met by:FleetGridKeyboardA11y.spec.mjs (is the drill path — list/listitem topology, native Enter/Space, drill jump, gate-3 restoration) · agentCard.spec.mjs (the non-focusable-root pin) · fleetCockpitPopOut.spec.mjs (the pop-out leg). (c) is ALSO already met — cockpitDockDocument.spec.mjs:39-47 asserts it and carries its own non-vacuity guard: expect(autoHidden.length).toBeGreaterThan(0)beforeexpect(autoHidden).toEqual(expect.arrayContaining(['detail','perspectives'])), plus expect(items.fleet.autoHidden).toBeUndefined() pinning the primaries. An empty filter cannot pass it. Remaining: (a) alone.(I wrote "the (a)/(c) invariants" above without checking (c) — corrected within minutes, in the very sentence written to fix stale text. Same class, one layer down.)
(a)'s honest shape: "focus order IS DOM order" holds iff no tabIndex > 0 exists in the rendered tree — a component's own config can introduce one without the shell's source ever saying tabIndex. So a source-grep of FleetCockpit.mjs would be a guard aimed at a filename (see #15345), not the claim. The honest witness is a mounted whitebox-e2e walking the composed tree — that is the one genuinely-open piece of AC2.
Scoping note for whoever writes it: dock zones are document-driven (resolveDockComponentRef resolves componentRef at runtime), so a static "expect this tab sequence" fixture would pin a layout preset, not an invariant — red on every preset change while proving nothing about accessibility. Assert the invariants, never the sequence.
Reduced-motion audit: no motion-gated information anywhere (pulse carries no unique signal — verified).
Findings folded into the token/contract docs (the drift-proof home).
Out of Scope
Screen-reader deep-pass (a follow-up leaf if the audit warrants) · visual baselines (T7.24) · token values themselves (#14578).
Context
T7.25 of Epic #14560 (map v1.1) — the a11y floor for the cockpit surface: an operator-grade tool must be keyboard-drivable and legible, and "stunning" that fails contrast is a design defect by the epic's own bar. Filed under the decomposition sprint (D#14561).
The Problem
The SSOT's dark palette runs several low-ink tiers (
--ink-dim,--ink-faint) over layered panels — contrast must be VERIFIED per token pair, not assumed. The control verbs (#14611/#14612) and the drill path (#14608/#14610) are keyboard-critical: an operator mid-incident cannot be mouse-gated. Reduced-motion gating exists per-leaf; this leaf audits it end-to-end.The Architectural Reality
prefers-reduced-motionconfig paths) + an audit doc for the judgment calls.The Fix
One PR: the contrast audit table (token-pair × usage-class × measured ratio × verdict/fix) + focus-order fixtures for the core paths + the end-to-end reduced-motion audit + fixes for any failures found (or split-out leaves if a fix is design-material).
Acceptance Criteria
tabIndexoverrides (the shell has none — focus order IS DOM order); (b) the drill button is the only focusable element inside a card (the card root stays a non-interactivelistitem); (c)autoHiddenzones (detail,perspectives) are not tab stops until revealed; (d) the drill/pop-out path stays green.controls → grid → stream → detail → perspectives(FleetCockpitcomposes[toolbar.fm-cockpit-bar, dockConfig];cockpitDockDocument:27-30orders fleet/stream/detail/perspectives). Three errors in the old sentence, all its own: controls are FIRST and should be (a toolbar tabs before its content — reordering the shell to match would worsen a11y);cardis not a zone (cards live inside the grid;agentCard.spec.mjs:219-223deliberately pins the root non-focusable);rails=detail+perspectives, bothautoHidden, so not steady-state stops. It cited #14615 as defining the topology — that ticket is CLOSED, which is when the sentence went stale.FleetGridKeyboardA11y.spec.mjs(is the drill path — list/listitem topology, native Enter/Space, drill jump, gate-3 restoration) ·agentCard.spec.mjs(the non-focusable-root pin) ·fleetCockpitPopOut.spec.mjs(the pop-out leg). (c) is ALSO already met —cockpitDockDocument.spec.mjs:39-47asserts it and carries its own non-vacuity guard:expect(autoHidden.length).toBeGreaterThan(0)beforeexpect(autoHidden).toEqual(expect.arrayContaining(['detail','perspectives'])), plusexpect(items.fleet.autoHidden).toBeUndefined()pinning the primaries. An empty filter cannot pass it. Remaining: (a) alone. (I wrote "the (a)/(c) invariants" above without checking (c) — corrected within minutes, in the very sentence written to fix stale text. Same class, one layer down.)tabIndex > 0exists in the rendered tree — a component's own config can introduce one without the shell's source ever sayingtabIndex. So a source-grep ofFleetCockpit.mjswould be a guard aimed at a filename (see #15345), not the claim. The honest witness is a mounted whitebox-e2e walking the composed tree — that is the one genuinely-open piece of AC2.resolveDockComponentRefresolvescomponentRefat runtime), so a static "expect this tab sequence" fixture would pin a layout preset, not an invariant — red on every preset change while proving nothing about accessibility. Assert the invariants, never the sequence.Out of Scope
Screen-reader deep-pass (a follow-up leaf if the audit warrants) · visual baselines (T7.24) · token values themselves (#14578).
Related
Epic #14560 (parent) · blocked-by #14578 · touches #14615 (focus topology) · D#14561.
Live latest-open sweep: fresh this window; no equivalent. A2A sweep: clean. Structure-map gate: N/A — audit leaf.
Origin Session ID: a28f1415-37a4-4a5a-a3e4-b56adf6274ec Retrieval Hint: "FM cockpit accessibility contrast focus order reduced motion audit"