What changed, and why
The end screen — the victory/defeat verdict — closes with a section called "the history you wrote": every change the player made to the timeline, in order. It was a hand-rolled flat list. Each row showed a headline, an "era · figure" line, and a bare signed %.
During play, that same data (gameStore.timelineEvents) is rendered by the game's signature object: the Spine (TimelineLedger). The Spine is a valence-colored dot-axis you scroll and tap, carrying the roll, the per-node Δ equation, and the ⚡ anachronism / ✒ craft / ⚑ staked marks, with a detail strip below. So the run's history read richer during play than it did at the verdict, where the whole game's payoff should land hardest.
This change drops the bespoke list and renders the Spine at the verdict instead. The blast radius is small: three files. The end screen swaps in <TimelineLedger readonly />; the Spine gains one optional readonly prop that adapts it to a finished run; and two tests pin the new behavior. In-game usage passes nothing, so the board's Spine is unchanged.
The end screen now renders the Spine
The replacement is nearly a drop-in. TimelineLedger reads the store directly and takes no required props, and the end screen runs against the same populated store, so rendering it is one tag. The v-if="timeline.length > 0" guard stays (no empty Spine at the verdict), and the mt-6 spacing matches the section rhythm above it.
The hand-rolled <ol> is gone; the Spine renders the history instead.
components/EndGameScreen.vue · 235 lines
⋯ 76 lines hidden (lines 1–76)
⋯ 151 lines hidden (lines 85–235)
The old block carried data-testid="final-timeline" and a local deltaClass helper that colored each row's %. Both are removed: the testid folds into the Spine's own data-testid="timeline-ledger", and deltaClass had no other caller, so it's deleted rather than left dangling.
A read-only mode for the Spine
The Spine is built for a live run: it ends the axis with a "▷ now · your move" present marker, leans into a "the timeline trembles…" state while a move resolves, and frames its lead in the present tense ("the timeline you're rewriting"). None of that fits a finished run. So TimelineLedger gains one optional prop, readonly, that turns those live affordances off.
The prop, and the three places it gates: the lead copy, the loading empty state, and the live present marker.
components/TimelineLedger.vue · 202 lines
⋯ 7 lines hidden (lines 1–7)
⋯ 3 lines hidden (lines 13–15)
⋯ 30 lines hidden (lines 19–48)
⋯ 54 lines hidden (lines 53–106)
⋯ 87 lines hidden (lines 116–202)
Three small gates, all keyed on props.readonly. The lead reads "the history you wrote" instead of "the timeline you're rewriting" (line 11). The live "the timeline trembles…" branch is skipped (line 17) — a verdict has no move in flight. And the "▷ now · your move" node is dropped (line 51) — a finished run has no next move. Tap-to-inspect stays on, so the verdict's timeline is still explorable.
The tests that pin it
Two discriminating tests, each failing on the old code and passing on the new. The first asserts the verdict renders the shared Spine (timeline-ledger + timeline-event present, the dead final-timeline absent) rather than a bespoke list. The second asserts the Spine renders read-only: no .spine-now present marker. A populated store comes from the store's own addTimelineEvent, so the test exercises the real render path.
The new describe block — render-via-Spine, and read-only.
tests/unit/components/EndGameScreen.spec.ts · 184 lines
⋯ 117 lines hidden (lines 1–117)
⋯ 41 lines hidden (lines 144–184)
npx vitest run is green at 720 (the +2 here). npx nuxt typecheck is clean. The e2e win/lose specs (tests/e2e/features/end-game.spec.ts) and the mission-select timeline-ledger specs pass. An independent a11y-source pass over both changed components found no gaps — the interactive Spine nodes are keyboard-complete and named, and the dialog stays focus-trapped.