# ASTERION · validation record

Worker-side verification of the committed `dist/` export. This is the worker's report, not an independent certification.

## 1. Scope and assumptions

- **Reviewed:** the single page (`dist/index.html`) and its flows: select destination (chart markers and radio list), reactor power allocation (sliders, −/+ buttons, presets), jump review dialog, bounded jump sequence, arrived state, blocked states (no destination, current location, engines below 30, fuel below 10), reset dialog, help dialog, ambient-motion pause, reduced-motion variant, persistence across reload.
- **Export:** `npm run build` → `dist/` (index.html, one JS bundle, one CSS file, two latin `.woff2` fonts, favicon). Vite `base: './'`; all asset URLs relative. Size on disk 354 KB.
- **Inferred choices:** dark single theme; cyan accent with green/amber/red status; fictional Tamsin system with origin Halcyon Drift and destinations Cinder Reach, Meridian Halo, The Quiet Shelf; transit time derived from engine allocation, shield capacity from shields, survey confidence and scanner range from scanners; unshielded routes cost 4 hull points on arrival; state persisted in `localStorage`; reset requires confirmation.
- **Exclusions:** no light theme, no localization, no RTL rendering, no audio.

## 2. Coverage by domain

| Domain | Status | Inspected states and evidence | Not performed |
| --- | --- | --- | --- |
| Accessibility | Checked | Skip link is the first tab stop; visible 2 px focus ring on the first control (computed and viewed in `desktop-focus-header.png`); every button has a name and every input a bound label (DOM audit, 0 issues); one `h1`, one `main`; radio group arrow keys; slider Home key; dialog opens with focus on the confirm/cancel button, Shift+Tab stays inside, Escape closes and focus returns to the opener; `role="status"` announcements text verified; reduced motion honoured (`data-motion="off"`, no animation on stars, no streaks, shorter sequence); minimum 24 px targets at all widths (radio cards measured by their label). | Screen-reader session; native 200 % zoom; forced-colors rendering; physical touch device. |
| Layout | Checked | No horizontal overflow at 320, 390, 768, 1024, 1440 px; grid area changes viewed at 390, 768, 1440; masthead stacking; route label kept inside the chart; route endpoint aligned with the marker within 2.2 px at every width. | RTL mirror; 200 % zoom (narrow viewport is not a substitute). |
| Writing | Checked | Source review: verb-first buttons (Prepare jump, Jump to X, Cancel, Reset simulation, Close help), sentence case throughout, hold reasons state the fix, dialog buttons repeat the consequence, consistent terms (destination, jump, reset). Humour is confined to log/remark copy; errors are plain. | — |
| Typography | Checked | Fonts confirmed loaded via `document.fonts.check`; heading sizes descend (28 → 18 → 16); tabular numerals on changing values; wrapping viewed at 320 and 390 (no truncation after fix F3); measure cap on the description. | Real-content widows at every width between breakpoints. |
| Colors | Checked | 29 rendered text pairs measured in Chromium with backgrounds composited through semi-transparent layers; lowest pair 4.90:1 (`.presets__numbers` on control fill), all ≥ 4.5:1. Full list in `test/scratch/verify-log.txt` during the run and summarised in `DESIGN.md`. | `.tag--ok` and `.masthead__note` were not rendered in the sampled state (same tokens as measured pairs: ok-green on surface 11.7:1, muted on surface 5.9:1 computed from tokens, not rendered). |
| UI | Checked | Hover gated by `(hover: hover)`; press scale 0.96; named transition properties; concentric radii 4/8/12/16; primary fill once per view; jump sequence 3.5 s full motion, 2.3 s reduced motion; states viewed: default, selected, hold, ready, in progress, arrived, dialogs. | Animations panel replay at 10 % speed. |

## 3. Findings and fixes

Severity per the Better Interface guide. All fixes were rebuilt and rechecked in the final run.

| # | Sev | Location | Evidence | Fix | Recheck |
| --- | --- | --- | --- | --- | --- |
| F1 | HIGH | `src/components/StarChart.tsx` (SVG `preserveAspectRatio="xMidYMid slice"` with HTML markers by percent) and `src/styles/app.css` `.chart__marker` transform | At 390 px the plotted route ended 65 px away from the destination marker; on desktop the pulse ring centred on the label, not the dot (`mobile-390.png`, `desktop-arrived.png`, first run). | ViewBox now equals the stage's CSS size via `ResizeObserver` (`useStageSize`); marker transform anchors the 14 px dot on the point; flipped markers use `.chart__marker--flip`. | Alignment 2.2 px at 320/390/768/1024; assertion added to the script. |
| F2 | HIGH | `src/styles/app.css` `.masthead` | At 390 and 768 px the identity block and readouts overlapped (`mobile-390.png`, `tablet-768.png`, first run). | Named grid areas; two-row layout below 72 rem, single column below 46 rem; later same-specificity rules overrode the first attempt, fixed with `.masthead .masthead__actions` selectors. | Viewed at 390, 320, 768: no overlap. |
| F3 | MEDIUM | `src/components/DestinationPanel.tsx` stat label, `src/styles/app.css` `.label` nowrap | "Transit at current engines" clipped to "TRANSIT AT CURRENT ENGI" at all widths. | Label renamed "Transit time"; `.stats .label, .gauge .label` allowed to wrap. | No clipped labels in final screenshots. |
| F4 | LOW | `src/components/StatusPanel.tsx` reactor gauge | "100 units" wrapped onto two lines beside the label. | Value shows `100`, note "Stable · 100 units"; `.gauge__value` nowrap. | Viewed at 1440 and 390. |
| F5 | LOW | `src/styles/app.css` `.presets__row` | Four presets wrapped 3 + 1, orphaning "Survey". | 2×2 grid (4 across between 46 and 72 rem). | Viewed at 1440, 768, 390. |
| F6 | LOW | `src/styles/app.css` `.bridge` desktop areas | Jump panel stretched to the status panel's height leaving a large empty block. | Right column became one spanning `side` area (power + status), so the jump row only matches the log. | Viewed at 1440: gap reduced to the log height. |
| F7 | LOW | `src/components/StarChart.tsx` route label | Label box could sit on the marker or clip at the chart edge. | Label placed 40 % along the curve and clamped inside the box. | "route label inside chart" assertion passes at all widths. |
| F8 | MEDIUM (design-time) | `src/components/JumpPanel.tsx` | A natively disabled jump button would be unreachable by keyboard and could not explain itself. | `aria-disabled` with `aria-describedby` pointing at the hold title; activation focuses the hold group and announces the reasons. | "blocked click moves focus to hold" and "status region announces hold" pass. |

No open findings remain in the reviewed scope.

## 4. Verification

Commands run from the repository root on the final source (no source change after the last build):

```
npm run typecheck                       # tsc --noEmit -p tsconfig.json → exit 0, no errors
npm run build                           # vite build → exit 0; dist/assets: index-*.js 261 KB (81 KB gz), index-*.css 29 KB (6 KB gz), 2 woff2 fonts 63 KB
node test/scratch/sim.test.mjs          # reducer rules (esbuild-compiled simulation.ts) → "all assertions passed"
PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright node test/scratch/verify.mjs
                                        # → "116/116 checks passed; 0 failed", exit 0
```

`verify.mjs` starts a static server for `dist/` under `http://127.0.0.1:<random>/preview/` (so relative asset paths are exercised from a subpath), launches the installed Playwright Chromium 1246 headless, and runs everything in one bounded foreground command, then closes the browser and server.

Viewports and contexts:

| Context | Size | What was exercised |
| --- | --- | --- |
| Desktop | 1440 × 900 | Full journey: initial state 40/35/25 and 0 unallocated; jump blocked with "Select a destination"; skip link; focus ring; select Meridian Halo via chart, radio sync, route drawn, detail updated; arrow keys in the radio group; +/− budget edges (raise disabled at 0 unallocated, slider `max` = value + unallocated); transit estimate changes with engines; Home key sets engines 0 → hold and slider help explain "at least 30"; blocked activation focuses hold and announces; Cruise preset; Enter opens the review dialog with focus on "Jump to Meridian Halo", fuel "100 → 90"; Escape closes and returns focus; Shift+Tab stays inside; confirm → overlay, button `aria-busy`, duplicate click ignored; sequence 3498 ms; fuel 90, header position, arrived banner, log entries, "already at Meridian Halo" hold, marker "current position"; contrast of 29 rendered pairs; reset dialog focuses Cancel, reset restores fuel 100 and Halcyon Drift; help lists 3 steps; pause toggle sets `data-motion="off"` and `aria-pressed`; selection persists across reload. Console: 0 errors/warnings. Network: 0 failed or 4xx/5xx. |
| Reduced motion | 1280 × 800, `prefers-reduced-motion: reduce` | `data-motion="off"`, pause button replaced by a note, stars not animating, no streak elements, sequence 2310 ms, hull wear to 93 % on the unshielded Cinder Reach route, 0 console errors. |
| Mobile | 390 × 844, touch, DPR 2 | No overflow, all targets ≥ 24 px, route/marker alignment, label inside chart, review dialog fits the viewport, full jump completes; 0 console/network failures. |
| Narrow | 320 × 568 | No overflow, targets, alignment, label. |
| Tablet | 768 × 1024 | Same checks; two-column layout viewed. |
| Laptop | 1024 × 768 | Same checks; two-column layout. |

Screenshots (viewed, not only saved): `artifacts/desktop.png` (1440 × 900, arrived at Meridian Halo with the Quiet Shelf route plotted) and `artifacts/mobile.png` (390 × 844, route plotted to The Quiet Shelf). Working screenshots at other states and widths were inspected during the review in `test/scratch/shots/` (initial, focus, engines-low, dialog, jumping, arrived, help, mobile dialog, mobile jumping, 320, 768, 1024).

## 5. Limitations

- **Assigned MCP browser:** the environment's browser launcher is plain Playwright MCP with no preview server and no `test/scratch/browser/preview.json`; it refuses `file:` URLs, and the assignment forbids background shell servers, so a foreground server could not stay alive between MCP tool calls (a 40 s bounded server received 0 requests because tool calls run sequentially). Rendered inspection was therefore performed with the Playwright library driving the same Chromium build inside one bounded foreground command, as the guide permits. The MCP session was opened and closed but made no observation of the site.
- Not performed: screen-reader session, native browser zoom at 200 %, forced-colors rendering, RTL mirror, physical touch device, Animations-panel replay. An accessibility snapshot or DOM audit is not a screen-reader test.
- Contrast for `.tag--ok` and `.masthead__note` was computed from their token values rather than rendered.
- The hosted IPFS preview and the public GitHub repository are requested deliveries handled by the publisher; no CID or URL was produced in this workspace.

## 6. Completion

**Complete for the stated scope.** Typecheck, production build, reducer tests and the rendered journey checks all pass on the final export; all eight findings are fixed and rechecked; the limitations above are the remaining unverified areas.
