What changed, and why
The Chronicle is the game's prose telling of the altered timeline, rewritten after every turn. The final telling — the one that carries the win/loss framing — is the run's epilogue, shown on the end screen.
The bug: that refresh is fired non-blocking (so mid-game the dice reveal never waits on prose), but the end screen renders the instant the run ends. At that moment the epilogue is still being written, so chronicle still holds the previous turn's telling. The loading placeholder was gated on !chronicle && chronicleLoading, so with a stale telling present it never showed. The end screen presented the prior-turn article as the epilogue, then snapped to the real one when it landed — a misleading double-take on the run's payoff moment.
The fix is one new store flag, epiloguePending, that means specifically the epilogue is being written. The end screen shows the "final account…" wait while it's set, then cross-fades the epilogue in. Three files of logic, two of tests; no change to how the Chronicle is generated or to mid-game behavior.
epiloguePending: tracking the terminal telling
A new piece of store state. It's deliberately distinct from the existing chronicleLoading, which is true during any refresh (every turn fires one). epiloguePending is true only while the terminal telling is in flight.
The flag, beside the chronicle state it qualifies.
stores/game.ts · 1397 lines
⋯ 283 lines hidden (lines 1–283)
⋯ 1104 lines hidden (lines 294–1397)
It's set where the run actually ends. After a resolved turn, sendMessage fires the (non-blocking) refresh, then — only on a terminal status — marks the epilogue pending and arranges to clear it when that refresh settles.
The terminal turn: fire the refresh, mark it pending, clear it when it settles.
stores/game.ts · 1397 lines
⋯ 1077 lines hidden (lines 1–1077)
⋯ 299 lines hidden (lines 1099–1397)
The never-blanks guarantee still holds
A failed final refresh must fall back to the prior telling, never hang on the loading state. That guarantee is unchanged: refreshChronicle catches its own errors and keeps whatever telling was already there, returning a promise that resolves (never rejects).
On failure: keep the prior chronicle, clear loading — the panel never blanks.
stores/game.ts · 1397 lines
⋯ 774 lines hidden (lines 1–774)
⋯ 608 lines hidden (lines 790–1397)
Because that promise always resolves, the .finally from the previous section always fires and clears epiloguePending. So on a failed epilogue the end screen leaves the loading state and falls back to rendering the prior telling (see the v-else-if next) — it reveals or it falls back, but it never spins. And resetGame clears the flag too, so Play Again starts clean (stores/game.ts:1384).
The end screen: the wait, then a cross-fade
The epilogue block now keys off epiloguePending. While pending, it shows the "final account…" line; otherwise it renders the telling. The two are wrapped in a Transition so the swap is a gentle opacity cross-fade rather than an abrupt text replacement.
Loading while pending; the epilogue article otherwise; a cross-fade between them.
components/EndGameScreen.vue · 260 lines
⋯ 34 lines hidden (lines 1–34)
⋯ 209 lines hidden (lines 52–260)
The transition name is computed, and goes empty under prefers-reduced-motion — an empty name disables the CSS classes, so reduced-motion gets an instant, class-free swap. This mirrors Chronicle.vue's rewrite reveal exactly.
The pending flag and the motion-gated transition name.
components/EndGameScreen.vue · 260 lines
⋯ 168 lines hidden (lines 1–168)
⋯ 86 lines hidden (lines 175–260)
The fade itself — the same opacity/duration tokens Chronicle.vue uses.
components/EndGameScreen.vue · 260 lines
⋯ 195 lines hidden (lines 1–195)
⋯ 52 lines hidden (lines 209–260)
Tests that pin the behavior
Two suites. The store tests are the discriminating ones — they'd fail if the flag were never set, never cleared, or set for a non-terminal turn.
Pending during the in-flight terminal refresh; cleared on resolve (epilogue stored) and on failure (prior telling kept); not set mid-run; cleared by reset.
tests/unit/stores/game.spec.ts · 1473 lines
⋯ 470 lines hidden (lines 1–470)
⋯ 925 lines hidden (lines 549–1473)
The component tests assert what the end screen renders for each store state — the three acceptance cases.
Pending → loading, not the stale article; landed → the epilogue; failed → the prior telling, never stuck on loading.