04-ui-ux.md · 210 requirements

04 — UI/UX: The Every Last City Client

This document specifies the Every Last City client. It owns the presentation of a game whose turn has three phases: an Orders phase in which every player writes conditional policies against a board that does not move, a Cascade in which the engine resolves every unit in initiative order with no input at all, and a Reckoning of bookkeeping (10-turn-model.md TM-020). Almost everything distinctive about this client follows from that shape. There is no unit-by-unit prompting loop, no "next unit awaiting orders" key, and no moment at which a player watches a result and then decides what to do next. In their place this document specifies four load-bearing surfaces:

Around those sit the map, the unit and city panels, the Industry and economy screens, the combat preview, and the input, onboarding, accessibility and mobile work that is independent of any of it.

The client is a web client: a Phaser 3 WebGL canvas renders the tile map and nothing else, and every other surface is real DOM (React 18 + TypeScript). The browser is the primary delivery target; Tauri 2 wraps the same bundle for desktop and mobile. §1 fixes that split and the canvas/DOM coordination contract (UX-010 through UX-060) that the rest of the document builds on.

Status: Draft v0.2 · Owner: unassigned · Depends on: docs/design/00-direction.md, docs/design/01-decision-turn-model.md, 01-game-rules.md (map, terrain, vision, fog, the Intelligence Picture, setup), 02-units-and-industry.md (unit schema, the Industry ladder, the production queue), 03-architecture.md (determinism, the order log, views, budgets), 05-multiplayer.md (transport, presence, lobbies, notifications), 06-ai.md (seat disclosure), 07-modding-content.md (localisation, generator schemas, content editing), 08-services-platform.md (accounts and profiles), 10-turn-model.md (phases, Orders, the Cascade, Initiative, Stance, playback), 11-combat.md (the prediction contract and the readout), 12-economy.md (the visible brakes and what the player sees), 13-command.md (the order vocabulary, Formations, the Dispatch), 14-victory.md (the Terms, the Watch, the Board, the Chronicle)


1. Client foundations

UX-010 The client MUST be one TypeScript codebase (@everylastcity/client, strict: true) composed of exactly two presentation layers, with a strict division of responsibility:

Layer Technology Owns
Map canvas Phaser 3, WebGL The tile map and nothing else: terrain, rivers and installations, units, cities, fog and intelligence regions, on-map overlays (UX-460), order previews, Cascade playback animation (§5), and the minimap
Interface DOM — React 18 + TypeScript, plain CSS Everything else: menus and screens (§2), the setup wizard and the Terms (§3), the Orders board and its readouts (§4), the after-action report (§5), every HUD region (§7), the unit panel and order-stack editor (§9), the Dispatch (§9), production, Industry and economy screens (§10), the combat preview (§11), the Watch, the Board and the Chronicle (§12), settings and key binds (§13), onboarding and the encyclopedia (§14)

The split MUST be strict in both directions: the canvas MUST NOT draw buttons, text entry, tables, lists, or dialog chrome, and the DOM MUST NOT draw map content. No third UI technology may be introduced. Where this document says "button", "dialog", "panel", "list", or "table", it means a DOM element; where it says the map "renders", it means the Phaser layer.

Rationale: the game is UI-dense, not action-dense. An order stack editor, a fourteen-column production table, a fourteen-row victory matrix and an encyclopedia are the client's real workload, and a game engine's control toolkit is the wrong instrument for all four. The canvas earns its place only for the tile map, where thousands of sprites must be batched. Confining the engine to the map is also what makes the browser a first-class platform rather than a port (UX-018), and it is what turns accessibility (§15) and data-dense UI (UX-045) from engine-limited compromises into native browser capabilities.

UX-012 Layer composition and z-order. The canvas MUST occupy a single full-bleed layer beneath a single DOM overlay root; the two MUST share one coordinate origin and one resize observer. DOM elements MUST NOT be inserted into the canvas layer and Phaser MUST NOT be asked to manage DOM (no engine-managed DOM containers); the only sanctioned bridges between the layers are the input contract of UX-014 and the projection of UX-016.

Stacking MUST be declared once as a named scale and MUST NOT be worked around with ad-hoc values: map canvas → map-anchored overlays → docked HUD panels → floating panels → modal dialogs and their scrim → context menus → command palette → toasts → tooltips → drag ghosts. Every layer above the canvas MUST live in one stacking context, so a modal always covers a tooltip and a menu always covers a panel. At most one modal scrim may exist at a time.

The DOM overlay root MUST default to pointer-events: none, with interactive subtrees opting back in, so the map remains directly reachable through the empty space between HUD regions. Every docked or floating surface MUST publish the screen rectangle it occludes, and all map-centring behaviour (UX-490, UX-870, UX-980, jump-to-location from any report or Dispatch item) MUST centre on the visible map area rather than the raw viewport.

Implementation status (2026-09-23), the stacking half. The scale is declared once, as the --z-* tokens on :root in packages/client/src/shell/app.css, in this requirement's order, and every z-index in the client names one (test/stacking.test.ts holds the order and refuses a literal). Two names sit outside the list and say why: --z-screen, above everything, for MP-400's hand-off, which replaces the layout rather than covering it; and --z-raised, a sticky row lifted within its own component. Not met: the canvas is a grid cell, not a full-bleed layer under one overlay root, so the overlay's pointer-events: none, the published occlusion rectangles and centring on the visible area (and UX-016's keep-clear of HUD panels, which reads those rectangles) wait on that restructure.

A reading recorded for the spec owner. Modals are native <dialog>s opened with showModal(), per UX-1320.1 and UX-1325 — it is what supplies the focus trap, the backdrop and Escape. A modal dialog lives in the browser's top layer, above every z-index, so while one is open the layers this list puts above modals (context menus, the palette, toasts, tooltips, drag ghosts) sit under it; the command palette is itself such a dialog. The order holds for everything the scale can place. Meeting it above an open modal means moving those layers into the top layer too (popover), which is a decision about UX-1325's native-element rule rather than a token, and is left open here.

UX-014 Input routing and hit-testing. There MUST be exactly one input pipeline, with DOM hit-testing authoritative:

  1. Pointer input MUST use Pointer Events. An event that lands on an interactive DOM element belongs to the DOM and MUST NOT reach the map; only events that reach the canvas or a pointer-events: none region of the overlay are map input. The client MUST NOT implement a second, competing hit-test cascade over map entities in the DOM.
  2. Every map drag — pan, drag-box selection (UX-840), the order drag-path (UX-670), geometry authoring (UX-735), stack drag-to-host (UX-770) — MUST use pointer capture, so a drag that crosses a HUD panel or leaves the window completes correctly instead of stranding the interaction.
  3. Over the map surface only, the client MUST suppress browser defaults that would otherwise steal the gesture: context menu, text-selection and long-press callout, double-tap zoom, overscroll/pull-to-refresh, and wheel page-scroll/browser-zoom (touch-action: none). Over DOM panels these defaults MUST continue to work — native scrolling, native selection, native copy, and the native context menu in text fields are requirements, not oversights (UX-045).
  4. Keyboard commands (§13) MUST dispatch only when focus is on the map surface or on a DOM element that does not itself consume the key. While a text input, textarea, or contenteditable holds focus, every printable character plus Backspace, Delete, Home, End, and the arrow keys MUST go to that field and MUST NOT fire a game command.
  5. Escape MUST resolve topmost-layer-first in this order: cancel an in-progress map interaction (drawing a Screen line, painting a Survey region, placing March waypoints, picking an attack target, siting a Depot, assembling a Formation) → dismiss the topmost menu, palette, or tooltip → close the topmost dialog → clear the selection (UX-680). Escape MUST always produce a visible effect at the first layer that can consume it.
  6. Gamepad input (UX-1240) MUST be polled on the render loop and dispatched through the same command dispatcher as keyboard, never by synthesising DOM focus events.

UX-016 Map-anchored UI. Interface that belongs to a map location — the hover location readout (UX-520), unit and city tooltips, the context menu (UX-600), the touch radial menu (UX-1400), the stack/cargo widget (UX-770), the route ETA readout (UX-670), endurance and range callouts (UX-880), Dispatch pins (UX-810), Bellwether markers (UX-1165), and Cascade playback callouts (§5) — is DOM and MUST be positioned by a single world→screen projection exported by the canvas layer. No other code may compute map screen coordinates.

Implementation status (2026-09-23). The projection is packages/client/src/map/projection.ts (project, its inverse unproject, and place for flip, shift, dismiss and clamp with a direction), and map/anchors.ts is the registry that re-places registered elements from Phaser's own post-render with the frame the map was just drawn with — so every camera change the bullet lists reaches the anchors in the map's frame, through one hook. Anchors are cells; the registry owns no elements (UX-050). The context menu (UX-600) is the first anchored surface. Not yet met: the visible area place keeps inside is the whole canvas, not the canvas less UX-012's occlusion rectangles, so an anchored element can still sit under a HUD panel; fog does not dismiss; and the other surfaces the requirement names are not on the registry. The cylinder rule is built and pinned as arithmetic, but the scene passes flat until UX-410's seam exists.

Rationale: these surfaces sit on the map but are still menus, tooltips, and readouts. Keeping them in the DOM is what makes them keyboard-operable and screen-reader-readable (UX-1320, UX-1325) — canvas-drawn equivalents would be invisible to assistive technology and would have to reimplement focus, hit-testing, and text layout by hand.

UX-018 Platform delivery. The browser is the primary platform and MUST implement every requirement in this document; no requirement may be satisfied only in a packaged build.

The iOS path carries the tightest constraints of the six targets, because Tauri 2 mobile is young and iOS mandates WKWebView; it is tracked as open question 2 rather than assumed away.

UX-020 The client MUST NOT mutate game state. Every player action MUST be expressed as one or more order records submitted to @everylastcity/core and appended to the authoritative log (10-turn-model.md TM-200, TM-210); there MUST be no other path by which player intent reaches the simulation. All display state MUST be rendered from the core's read-only, per-player fog-filtered views. Any information shown to a player MUST come from that player's filtered view (01-game-rules.md GR-1180; 03-architecture.md AR-420) — the UI layer can never leak fogged data. This binds both layers equally: the canvas draws only what the filtered view contains, and no DOM panel, table, tooltip, or accessibility affordance (UX-1320) may surface a value the view withholds.

Partially held 2026-09-27: each clause stands under its own row; no test holds this invariant whole, so the id stays untitled. The direction of flow — order records up, validated results down — is pinned in net-and-ai-contract-pair.test.ts as the turn model’s contract pair (TM-2700), where the wire protocol names itself and the stream rides its own named kinds; fog-filtered presentation stands under the rows that read the view, each overlay of UX-460 drawn from it and nothing else; and foundations.test.ts disclaims this id rather than claiming it. The universal negative — no other path by which player intent reaches the simulation — has no test of its own, because a scan can list what is present but cannot prove what is absent.

UX-030 The phase MUST be explicit. The client MUST show, at all times and without a click, which of Setup, Orders, Cascade, Reckoning or Ended the game is in (TM-090), and MUST make each transition legible rather than incidental. During the Cascade and the Reckoning the client MUST NOT present any control that implies input is possible, MUST NOT offer a confirmation, an opportunity prompt, or a mid-resolution decision of any kind, and MUST NOT queue an order for later submission (TM-050, TM-260).

Name and transition built 2026-09-24 (client/src/shell/phase-badge.ts): a badge on the facts bar carries TM-090's word for the phase, read off the view, and stays when the rest of the interface is hidden. A reading worth recording: this client never observes the Cascade or the Reckoning as a phase, because closing a turn resolves both inside one applyOrder (UX-335) — the view goes from Orders on turn T to Orders on turn T + 1. So the transition is keyed on the phase and the turn: while the playback is up the badge says "watching turn T's Cascade", and afterwards "after turn T's Cascade and Reckoning". The input clause is UX-335's machinery — the playback replaces the order controls rather than disabling them — and is held there.

Rationale: the single most confusing thing a client of this shape can do is accept an input that looks like it changed something and did not. TM-050 forbids a mid-Cascade prompt in the engine; this requirement forbids the interface from implying one exists. Everything a prompt would ask is expressible in advance through Sanctions (§9), and where it genuinely is not, the answer is a richer Sanction vocabulary, not a prompt.

UX-040 No rules arithmetic in the client. Every number the client displays MUST come from a core export. Specifically the client MUST NOT compute, re-derive, approximate, interpolate, or round-trip: Initiative or any of its terms (TM-1080), any combat figure (11-combat.md CB-2470), any economic quantity (12-economy.md EC-050), any victory value (14-victory.md VC-3170), any command-layer decision or score (13-command.md CM-100), or any prediction (CB-2140 — there is exactly one resolution function and the preview is implemented by calling it). Per-mille figures MUST be displayed in per-mille or as a percentage derived from the per-mille value, and MUST NOT be re-derived from a percentage (CB-2480).

Sorting, filtering, grouping and number formatting in the client are display-only: they MUST NOT influence the content or ordering of any order record submitted to the core, so locale-sensitive collation and number formatting can never reach rules computation.

Rationale: a second implementation of any of this arithmetic is a divergence waiting to be discovered by a player mid-game, and the specifications it would diverge from have each gone to deliberate trouble to make one implementation sufficient. The client's job is layout, not physics.

UX-045 Data-dense interface baseline. Every list and table surface in this document MUST be built from real DOM data structures and MUST provide, without per-screen re-invention:

This baseline binds at least: the Roster (UX-800), the Dispatch (UX-810), the production list (UX-920), the Production Overview (UX-1050), the Empire Ledger (UX-1070), the Requisition panel (§9), the Doctrine list (§9), the after-action report (UX-355), the Board and the Chronicle (§12), the Key Bind dialog (UX-1200), the encyclopedia (UX-1290), and the Load Game list (UX-080).

Rationale: this is the specific reason the interface is DOM. A four-hundred-row production table with sortable columns, sticky headers, live filtering, and IME-capable inline renaming is a solved problem in the browser and a bespoke engineering project in a game engine's control toolkit.

UX-050 Performance. The client MUST sustain 60 fps pan and zoom, and MUST render first feedback for any input (selection highlight, route preview, button press state) within 100 ms, at every map size tier of 01-game-rules.md GR-230 — Duel 40×28 through Vast 240×168 — on the project's reference hardware.

The client's stress fixture MUST be a Vast game: 240×168 tiles on a cylinder, 8 players, the GR-230 default city count of 269, and 3,200 live units, which is the largest population any neighbouring document budgets against (14-victory.md VC-310). Because the browser build is the slowest of the six targets, these numbers MUST be measured in the browser build on the reference device; a passing packaged build does not satisfy UX-050. no-test-needed: frame-rate and first-feedback budgets bind named reference hardware — 60 fps pan and zoom, 100 ms to first feedback — which no test in this repository can measure. What is mechanically holdable stands under its own rows: table-window.test.ts and table-rendered.test.ts pin windowing against the row’s own 3,200-unit Vast fixture, and map-projection.test.ts holds the projection’s behaviour; what remains here are numbers to be measured on hardware, not behaviour CI can run.

This replaces a figure inherited from an earlier architecture note — a 1,000×1,000 "Large" map with 10,000 live units — that no longer corresponds to any tier the rules define. GR-230's largest shipped tier is 40,320 tiles, twenty-five times smaller in area, and GR-720's Surface stacking limit of 1 in the open bounds unit density by tiles rather than by ambition. The client is not being asked to hold less; it is being asked to hold the game that exists. See open question 1 for the reconciliation this still owes 03-architecture.md.

Culling bounds entity work; it does not bound tile work. At the low LOD tiers of UX-390 a large fraction of the map is on screen at once, so the budget MUST be stated per tier. At a reference viewport of 1920×1080 CSS px with a Close-tier tile size of 32 CSS px:

LOD tier (UX-390) Zoom Visible tiles Share of the Vast fixture Budget
Close 1.0× ~2,040 (60 × 34) 5% 60 fps, full sprites and animation
Mid 0.5× ~8,200 (120 × 68) 20% 60 fps
Chip 0.25× ~32,400 (240 × 135) 80% 60 fps
Strategic 0.125× all 40,320 100% 60 fps

Three budgets follow from the two-layer split, and one requirement follows from the table:

  1. Map layer — draw calls MUST be batched (tilemap batching plus one texture atlas per LOD tier), so frame cost scales with visible tiles and visible entities rather than with total entities.
  2. Entity aggregation — at the Chip and Strategic tiers, entities MUST be aggregated to one marker per occupied location, sized and counted (UX-390). This is a legibility requirement before it is a throughput one: forty thousand individual dots is not a picture of anything.
  3. Allocation — the render loop MUST hold per-frame allocation to a bounded, pooled working set. A garbage-collection pause is indistinguishable from a dropped frame, and this is the budget most easily lost to convenience code.
  4. DOM layer — HUD and panel updates MUST be driven by state change, never re-rendered per frame, and high-frequency values (hover readout contents, route ETA, minimap viewport, playback cursor) MUST be coalesced to at most one DOM update per frame.

A pre-reduced overview raster for terrain below the Chip tier is permitted and SHOULD be used, but is required only above GR-250's tested envelope (any axis beyond 1,024), where per-tile quads stop being affordable. Custom maps up to GR-120's limit of 2,048 per axis MUST still render under UX-400's fixed memory budget, without a frame-rate guarantee.

UX-055 The core MUST run off the render thread. @everylastcity/core MUST execute in a Web Worker (the same worker under Tauri), with the UI submitting order records and receiving events and fog-filtered views; the client MUST NOT call the core synchronously from the render loop. Cascade computation, Reckoning, replay scrubbing, AI thinking, and map generation therefore MUST NOT block the frame: while the worker is busy the player MUST still be able to pan, zoom, open panels, and read reports, and the client MUST show progress rather than freezing (UX-375).

Partially held 2026-09-27: the row’s named jobs stand where they are built; the id names no test of them, so it stays untitled. What is pinned: new-game generation in the worker (game-worker.test.ts, AR-780’s first slice), a turn closed in the worker to the same hashes and fields (close-worker.test.ts, its second slice), restore replayed through the worker while the page waits (restore-worker-page.test.ts, its third), replay scrubbing off the page (past-job.test.ts, under UX-365 — “the worker replays what the page would”), and the setup’s map preview generated in the simulation worker (map-preview.test.ts, from preview-job.ts). What is not: the frame-rate half of the clause — pan, zoom, panels and reports staying live while the worker is busy (UX-375) — needs a running render loop with timing, which no test here owns; SESSION.md’s unbuilt list records the remaining page-side work (“the page still applies orders and projects views itself”, measured at 0.2–4 ms on Vast: architecture owed, not cost a player feels), with AI thinking standing as the Staff inside packages/client.

Playback never blocks on simulation, because the Cascade is computed to completion before any frame of it is drawn (TM-2240) and the client animates a finished observation stream at its own pace (TM-2200).

UX-060 Disclosure profiles. On first run the player MUST be offered at least three named profiles, each of which sets a coherent bundle of disclosure and interface settings:

Profile Sets
Field Initiative Tier 1 (TM-1040); Sanctions shown as the three presets only (CM-1980); Formations, Groups, Postings and Requisitions hidden (CM-1990); overlays reduced to supply, intelligence and victory sites; the Board summarised
Campaign Initiative Tier 2; the four Sanction dials exposed (CM-570); Formations and Doctrines enabled; the full overlay set
Staff Initiative Tier 3; everything enabled, including Postings, Requisitions, Consignments and the command audit trail (UX-895)

Every individual setting in a profile MUST remain independently adjustable afterward, and a player MUST be able to enable any hidden system immediately rather than earning it (CM-1980).

The Initiative Tier is a rules setting, fixed at setup and unchangeable during a game (TM-1030, TM-1070). A profile therefore proposes a Tier in the setup wizard and MUST NOT be able to change it once play begins; the client MUST make that distinction plain at the point of choosing.

Built 2026-09-24 (client/src/settings/disclosure-profiles.ts, ProfileChooser.tsx). A first run is a settings store with no profile named; the chooser offers the three and says, beside them, that the Tier is proposed to the setup wizard and fixed once a game begins. Choosing writes the bundle as ordinary settings — Sanction dials, the four CM-1990 switches, the reduced overlay set, the summarised Board, the audit trail — each of which the Settings screen edits alone, so nothing is locked behind the profile. Two readings, recorded here and in the module: a profile enables what its row names, so Campaign turns Formations on and leaves Groups, Postings and Requisitions as Field left them; and "the Board summarised" is the matrix shown with every other section folded one click away. Doctrines have no switch of their own and ride Formations. Flagged for the spec owner.

Rationale: the specification set delivers escalating complexity through several independent disclosure mechanisms — the Initiative Tier, the command layer's presets and optional systems, and the Industry ladder's unlock cards. A player should meet one decision about how much game they want, not four. The profile is that one decision, and it is a starting point rather than a mode.


2. Application shell and screens

UX-070 The client MUST provide at least these top-level screens, reachable from a main menu:

Screen Contents Spec owner
Main Menu New Game, Load Game, Multiplayer, Editors, Encyclopedia, Settings, Profiles this doc
New Game the setup wizard and the Terms (§3) this doc + 14
Load Game saved and correspondence games, archives, branch restore this doc + 03/05
Multiplayer lobbies, correspondence games, invitations 05-multiplayer.md
Map/Scenario Editor map painting, scenario setup 07-modding-content.md
Content Editor unit set and ruleset editing 07-modding-content.md
Workshop / Sharing maps, scenarios, content packs 07/08
Settings gameplay, interface, key binds, audio, video, accessibility, profiles this doc
Encyclopedia the complete unit and terrain reference (UX-1290) this doc + 02
In-Game the phase surfaces (§4–§12) this doc

Built 2026-09-24 (client/src/shell/MainMenu.tsx): a Menu control on the bar opens the main menu, from which New Game, Load Game, Multiplayer, Encyclopedia, Settings and Profiles each open in one click. It is a menu rather than a landing screen: the client opens on a game, and a first run on the profile chooser (UX-060). The two editors and the workshop are named on it as not in this build, with what each waits for (07-modding-content.md, 08-services-platform.md).

UX-080 The Load Game screen MUST list games with display name, map thumbnail, turn number and phase, participating seats and their commit state, game mode, and last-played timestamp; games MUST be keyed by unique id, not by name. It MUST surface archive creation and branch restore per the save model of 03-architecture.md, presenting restore as non-destructive timeline branching (AR-600) rather than as an overwrite.

The map thumbnail since 2026-09-24 (client/src/save/thumbnail.ts): the seat's own minimap, fog and all, sampled to at most 48 columns and kept with the save as a small palette and one character a pixel, drawn in the Load Game list as a named image. A save from before it shows a dash.

UX-090 All settings MUST live in the unified Settings screen with per-profile scopes. Audio settings MUST all appear together in one panel; there MUST NOT be a second settings surface reachable only from outside a game.

Met 2026-09-24 for what this build has. Settings is on the bar during play and edits every kept preference: the options, the HUD layout, and the key bindings, which it now opens (the Key Bind editor had been reachable only from the bar). All of it is kept in the guest profile (UX-220), one scope. The build makes no sound, so there is no audio setting yet; the clause applies when there is.

UX-100 Player colour identity: each account or profile carries a colour used to tint chrome. In game the current seat's colour governs, out of game the profile default governs, and the player MUST be able to override it in settings. Ownership colour on the map is governed separately by UX-210 and UX-1310.

Built 2026-09-24 (client/src/shell/chrome-colour.ts). The tint is one custom property on the document root, drawn as the outer edge of the top and bottom bars. In a game it is the seat's field colour from the palette, remap included, so the chrome and the map agree; with no game on screen it is the profile's colour, a Settings colour picker with a default that is no seat's. The override is a second setting: with it on, the profile's colour governs in game too.

UX-110 The client MUST remain fully functional offline with a local guest profile; no screen in this document may hard-require a cloud account (accounts in 08-services-platform.md). In the browser build this obligation is concrete: the client MUST be installable (PWA) and MUST cache its application shell, the active content pack's data and assets, and the help and encyclopedia content, so a solo or hotseat game can be created and played with no network.

Local persistence in the browser is evictable, which the packaged builds' filesystem storage is not. The client therefore MUST request persistent storage on first save, MUST tell the player plainly when persistence is not granted, and MUST make export-to-file reachable in one step from the Load Game screen, so a cleared browser profile is never the only copy of a game. Per-browser quota and eviction behaviour against real save sizes is open question 5.


3. Setup: the wizard and the Terms

UX-120 Game setup MUST be presented as a guided wizard — Content → Map → Rules → the Terms → Players → Review — and MUST also offer a single-screen advanced view exposing every panel at once for a player who knows what they want.

UX-130 Content selection MUST come first: the ruleset version and the unit set in force (02-units-and-industry.md US-1340 ships exactly one, standard; further sets come from 07-modding-content.md). Selecting content MUST load that content's suggested rules and Terms into the later panels, which the setup player may then override individually.

UX-140 Where changing the content selection would overwrite choices the player has manually modified, the wizard MUST show a diff of the incoming suggestions against current choices and offer apply suggestions or keep my overrides. It MUST NOT silently reset.

Flagged gap, 2026-09-23: no diff or override machinery exists. Nothing in client/src/setup/ compares incoming content suggestions against choices the player has changed; Wizard.tsx says in prose that later panels may be overridden, and there is no record of which values were overridden, which is the state this requirement needs before it can show a diff at all.

The clause to build first is the last one, and it is a one-line invariant: "It MUST NOT silently reset." A wizard that simply refuses to overwrite a manually-set value satisfies the prohibition immediately and is a fraction of the work of the diff; the diff then improves it. Doing the diff first risks shipping the reset with a dialogue on top of it. This is a data-loss guard — the player’s work is the thing at stake — and those are usually either absent or silently wrong, which is why it is worth recording as absent rather than leaving it to read as covered by the section around it.

Built 2026-09-24 in the order this note asked for (client/src/setup/suggestions.ts). A Term set on the Rules step is recorded as the player's; choosing a victory preset clears the record, because that is the player asking for the preset's Terms. An upstream recompute — today the shape preset, the call site recorded at UX-260 — gives the untouched Terms the incoming values and leaves every override as it was, with nothing clicked; only then, where an override and the incoming value disagree, does the wizard show the diff (Term, yours, suggested) with Apply suggestions and Keep my overrides. The Content step selects a unit set, which suggests nothing downstream yet; it attaches to the same function when it does.

UX-150 Game naming: the wizard MUST auto-generate a name from a bundled name list, provide a Generate Name control to reroll, and allow manual entry. The name is a display label; identity is the game id (UX-080).

UX-160 The Map panel MUST expose exactly the setup parameters 01-game-rules.md GR-1480 defines — size, topology, landRatio, terrainMix, cityDensity, minCitySpacing, riverDensity, roadDensity, neutralDefence, startCities, minStartSeparation, fogEnabled, intelligenceDecay — each with its stated default, its legal range, and a one-line plain-language effect.

UX-165 Platform map-size ceiling. A large map costs the client memory that a browser tab or a mobile WebView may not have. Concretely:

  1. Advertisement. The client MUST determine the largest map it can hold and MUST expose that as a measured capability value through the adapter of UX-018, never as a guess inferred from the user agent — a capable desktop browser MUST NOT be refused for belonging to the browser class, and a memory-starved desktop MUST NOT be offered a size it cannot hold for belonging to the desktop class.
  2. Setup. The wizard MUST NOT offer, and MUST NOT accept, dimensions above the advertised ceiling by any route: the dimension fields, a tier button, a generator preset, a one-click preset, a loaded setup file, or the selection of a pre-made map or scenario.
  3. Explain, never clamp. A size above the ceiling MUST be shown disabled with the reason and a platform that can hold it named, never hidden and never silently reduced. Rewriting a player's typed dimensions is a defect under this requirement.
  4. Entry, not only creation. The same check MUST run wherever a game is entered rather than created — the Load Game list, an archive restore, an accepted invitation, a turn-bundle open — which MUST list the game and disable entry with the reason stated, rather than loading it and failing partway.
  5. Every shipped tier is non-negotiable. All five GR-230 tiers MUST be holdable on every supported platform, because cross-play across Windows, macOS, Linux, iOS, Android and the browser is a promise 05-multiplayer.md makes and this ceiling MUST NOT be used to break. A measured ceiling below Vast is a budget defect to be fixed, not a configuration this UI may normalise. The ceiling exists for custom maps beyond GR-250's tested envelope.

UX-170 Generator options: a selected map generator's options MUST be rendered from the plugin's declared schema (07-modding-content.md), and option configurations MUST be savable to and loadable from named presets. The wizard MUST show a live thumbnail preview of the generated map and MUST display the generation seed with copy and share support. Where the generator exhausts its retry budget without satisfying the fairness invariants (GR-1560), the wizard MUST say which invariant failed and offer the parameter that would relax it, rather than reporting a generic failure.

The live thumbnail, built 2026-09-25 (client/src/setup/preview.ts, MapPreviewPanel.tsx, held by client/test/map-preview.test.ts). The wizard's Map step draws the world the setup and seed will generate: every tile's terrain in its palette colour, and every city as a light mark. The world is generated in the simulation worker once the settings have been still for 400 ms, and a newer request abandons an older one. A map that cannot be made gives its reason instead of a picture. The whole map is shown before play: this is the map being chosen, and no seat has yet explored any of it. Until this, the step named the thumbnail as absent.

UX-180 The Terms. The victory setup panel MUST present each Track as a card carrying exactly the Track's name, one plain-language sentence of at most 140 characters, at most three parameters with their current values, and the three integer ratings Length, Aggression and Complexity, each in [1, 5] (VC-1910, VC-1920, VC-1940). A fourth or further parameter MUST live behind an advanced disclosure with a default the player never needs to change.

Enabling, disabling or re-parameterising any Track MUST immediately regenerate the paragraph (UX-185), the estimate (UX-192) and the lint results (UX-195), without a confirmation step and without navigating away from the cards (VC-1950).

UX-185 The generated Terms paragraph (VC-1960) MUST be shown below the cards, MUST state every quantity in natural units rather than per-mille (VC-1980), and MUST be presented to every player before play begins. It MUST be stored verbatim with the game and re-presented unchanged in the end-of-game summary (VC-2000, UX-1195).

Rationale: 14-victory.md calls this the single most valuable object on the setup screen and it is right. It is also the answer to the worst way a multiplayer game can end — an argument about what the rules were. The client's obligation is to make it impossible to start a game without having been shown it.

UX-190 The Rules panel MUST present every rule setting the specification set defines, grouped as common and advanced, with search, plain-language effect text per setting, and a live summary sidebar. The identical summary MUST be reviewable at any time in game. At minimum it MUST expose:

Setting Owner
Initiative Tier (1 Standard / 2 Veteran / 3 Campaign), fixed for the game TM-1030, TM-1040, TM-1070
Orders deadline and per-player reserve bank, or None TM-480, TM-490, TM-500
Fog and intelligence decay GR-1630
Progress Visibility — Banded / Public / Own Only VC-2190
Capitulation policy, the Verdict, the Rally, Defiance VC-2120, VC-2470, VC-2980, VC-3010
Per-player Industry cap US-900
Neutral city defence GR-1480

UX-192 Estimated length MUST be presented as an integer turn range from the shipped calibration table (VC-2030, VC-2050), never as a qualitative label (VC-2020). Where the table has no cell for the current configuration the estimate MUST be labelled as uncalibrated (VC-2060); where the table was generated against a different ruleset version it MUST be labelled stale (VC-2070).

UX-195 The setup linter MUST run continuously and MUST present every finding with its concrete numeric message naming the setting that would resolve it (VC-2080, VC-2100). A finding of severity Error MUST prevent the game from starting; a Warning MUST be dismissible and MUST be recorded as dismissed in the game record (VC-2090, VC-3050). The client MUST NOT summarise findings into a count — each is a sentence and MUST be shown as one.

UX-200 Player list: each row is a seat carrying its controller — a local human, an invited remote human, or an AI seat with its plugin choice and schema-driven options, savable as named presets. Two or more local humans constitutes hotseat (UX-275).

UX-210 Per-seat colours: each seat MUST carry a foreground, background and text colour, all changeable later in game, and all subject to the colourblind-safe and pattern requirements of UX-1310.

Partially held 2026-09-27: the triad stands as the measured palette under its own rows; no test names this id, so it stays untitled. Each seat’s three colours — FIELD the body mass, INK the contour and every interior mark, TEXT a label on the field — are SeatPalette in client/src/map/palette.ts, named after UX-210 in its own docblock, and they are held channel by channel where measured: UX-215’s separate-channel CIEDE2000 separation and UX-1310’s shape-before-hue clauses both stand under their own titled describes in map-colour.test.ts. The “changeable later” half is what the settings panel reads — per-seat overrides from settings.seatColours falling back to the shipped palette (SettingsPanel) — and no test exercises a player changing a colour mid-game; a third of a requirement is not the requirement.

UX-215 Seats MUST be separable on both ownership channels. For every unordered pair of shipped seats, foreground-to-foreground and background-to-background separation MUST be tested separately, as CIEDE2000 colour differences. A palette that separates on only one channel MUST be rejected however well it scores on a particular asset.

A provisional floor of ΔE₀₀ ≥ 10 per channel MAY be used during palette development. The final floor MUST be calibrated and locked with the Phase-2 palette, alongside the rendered-asset threshold of docs/art/02-terrain.md ART-1135.

Ruling, 2026-09-05, corrected the same day and then settled on the asset team's instrument. The two-channel rule is the part that was established by measurement and it holds: the shipped pairs separate by ΔE₀₀ 41.5 on foreground and only 13.1 on background — two tinted whites — so effectively only ink distinguished the seats. Candidate palettes built the other way round, with near-white foregrounds for contrast against their own backgrounds, failed just as hard for the mirror-image reason: ink-dominated assets made those seats identical. Different assets are dominated by different tones, so a single rendered score can pass while one channel carries nothing. This requirement is the guardrail that stops that, and it is deliberately independent of the rendered test rather than derived from it.

The rendered ownership score itself is ART-1135's and is not restated here. Its first form — RMSE in RGB over the whole cell, threshold 0.25 — was unreachable at any palette, and this document's own first ruling wrongly blamed the palette alone for it. Both were replaced by a support-normalised CIEDE2000 measure proposed by the asset team and verified independently before adoption.

UX-220 Local human seats MUST be bound to a local profile or account, which stores options, key bindings, and game history (08-services-platform.md); a guest profile satisfies this.

Met 2026-09-24 by the guest profile: this browser's local store keeps the options (elc.settings.v1), the key bindings (elc.keys.v1, client/src/keys/key-store.ts) and the game history (elc.saves.v1), and every local seat plays under it. Accounts are 08-services-platform.md's.

Flagged gap, 2026-09-23: there is no profile of any kind in the client. This requirement points at 08-services-platform.md, which stands at 0 of 126 — wholly unbuilt — so the dependency is real rather than an oversight.

The clause worth holding onto is the escape hatch: "a guest profile satisfies this." It means §3 does not have to wait for accounts, sessions or a service — a local, anonymous profile that stores options, key bindings and game history is enough, and it is buildable today with the localStorage the wizard already uses for map presets. Recorded so that this reads as a small local piece of work rather than as blocked on an unbuilt document.

UX-230 Per-seat settings MUST include starting cities (within GR-1500's range) and the seat's Industry cap (US-900), plus any handicap the ruleset defines. Every asymmetry between seats MUST be disclosed to every player, in setup and in the in-game rules review, with its direction stated — which seat it helps and by how much (06-ai.md AI-100). The client MUST NOT present a handicap control whose effect it cannot state in one sentence.

UX-240 Add Seat and per-row Remove Seat controls MUST exist, and MUST be disabled with the reason shown when a pre-made scenario fixes the seat list.

Built 2026-09-24 (client/src/setup/model.ts, the Players step). Add seat appends an AI seat; each row's Remove drops it, and the rows after it renumber carrying their own per-seat settings. The Players select's bounds (two to eight) disable the controls with a sentence. The scenario lock is a draft field the lock reads; no shipped preset fixes a seat list — UX-260's shape presets set a count and leave the list editable — so today it is reached only by a scenario that says so.

Flagged gap, 2026-09-23, and it has two halves with different blockers. There is no seat add or remove control anywhere in the client — seatsFor(players) derives the seat list from a player count, so seats are a consequence of a number rather than rows a player edits. That half waits on nothing.

The second half — "disabled with the reason shown when a pre-made scenario fixes the seat list" — additionally waits on scenarios, which 03-architecture.md’s AR-640 records as entirely absent (there is no scenario loader). Worth separating, because the first half is buildable now and the second is not, and a note that called the whole requirement blocked would be wrong about the part that is not.

UX-250 Complete setup configurations MUST be savable to and loadable from named files; the Play control MUST activate only when the setup is valid and complete and no linter Error stands; and the last-used setup MUST be auto-saved in full — rules, Terms and seats included — and auto-restored on next entry. The wizard SHOULD additionally offer a shareable setup code or URL.

Partially held 2026-09-23: the Play gate is built and is tested; the files and the auto-restore are not. canStart consults the Terms’ own verdict before anything else, so a standing Error can never start a game, and standingFindings keeps every Error regardless of what the player dismissed while dropping dismissed Warnings — which is what the gate rests on, since an Error that could be dismissed would unlock Play on a setup the linter had refused.

A note on how that row had to be written, because the first version was wrong in the passing direction. It passed null for the map ceiling in every case, so canStart refused at the ceiling check and never reached the Error at all — a mutation pass deleting the result.canStart line left every assertion green. The scene now supplies a ceiling large enough that it cannot be the thing refusing, and carries a control that the same draft with clean Terms does start. A scene that cannot say which condition refused is only testing that something did.

Flagged gap: savable setup files and the auto-restore. Nothing in the client saves or loads a whole setup — localStorage is used for map presets and the measured map ceiling, not for the draft. The clause to build first is the auto-save, not the named files: "the last-used setup MUST be auto-saved in full — rules, Terms and seats included — and auto-restored on next entry" is the one a player notices by losing twenty minutes of configuration to a refresh, and "in full" is the part a partial implementation gets wrong silently, by restoring the rules and quietly dropping the seats.

UX-260 The wizard MUST offer one-click presets. The victory presets MUST be exactly those of VC-2120, presented by name with their estimated length; a player who never opens the Terms MUST receive Standard War (VC-2130). Map and seat presets MUST additionally be offered for the common shapes — a two-player duel, a four-player standard, an eight-player Vast correspondence game.

*Held 2026-09-23 as packages/client/test/setup-presets.test.ts for the victory-preset clauses, and the table is not copied into the test — it is parsed out of 14-victory.md and compared to the shipped PRESETS. That is the only form that fails in both directions: a preset added to the wizard and not to the document fails, and a row edited in the document and not the wizard fails too. A hand-copied expectation drifts with whichever side somebody edited and then agrees with neither, which is exactly the failure this repository met on 2026-09-23 in test/combat/helpers.ts’s golden rows. Names, order and all eight advertised ranges match today. Six mutants killed, three on each side of the equality.*

The wizard already guards half of this itself — newDraft throws VC-2130 names a preset VC-2120 does not carry — but that catches only a deletion of the default. A rename, a ninth preset, or an estimate that moved all passed it, and are what the new rows add. VC-2130 is checked on the draft rather than on the constant, because the constant being right while the draft ignores it is the defect worth catching.

One methodological note, because the test is a scrape: the parse is asserted before it is compared — eight rows, each with a real range, exactly one marked default. A scrape that silently matched nothing would make every equality below it pass. The estimate cells use an en dash, which a naive - split misses entirely.

The third clause was recorded as a gap earlier on 2026-09-23 and built the same day (client/src/setup/shape-presets.ts, held by client/test/setup-shapes.test.ts). Three shapes, each a point where the map size and the seat count agree — which is the pair a new player has no way to reason about separately, and the reason "one-click" is worth having: GR-230’s tiers carry their own player ranges, so a Vast map with two players is a long walk to first contact and a Duel map with eight is a knife fight nobody chose.*

Every figure is read from a requirement rather than chosen, and the tests check it against that requirement rather than against itself. The sizes are GR-230’s tiers and their own playerMin/playerMax fix the counts — Duel is 2..2 so the duel is its only legal shape, Standard admits 3–6 so four sits strictly inside it, Vast admits 6–8 so eight is its ceiling. The deadlines are TM-490’s table, parsed out of 10-turn-model.md rather than copied, for the same reason VC-2120’s presets are parsed rather than copied: a hand-copied constant drifts with whichever side somebody edited and then agrees with neither.

The property that makes the table data rather than three typed numbers is shapeFitsTier: a shape whose seat count sat outside its tier’s range would be offered by a control and then refused by the engine at validateMapSetup — a player told yes and then no, in that order.

One shape deliberately sets no deadline, and that is the reading worth recording. TM-490’s defaults are a function of the mode, and only the third shape names one. So the Vast correspondence shape carries 172800 s (48 h) and the duel carries None — both TM-490’s own rows — while the four-player standard leaves the draft’s deadline untouched rather than guessing at a mode the player has not chosen. Guessing would produce a clock nobody asked for, which TM-500 forbids outright. The duel does set None rather than leaving a previous shape’s figure standing, which is the same clause read the other way.

And the design question the clause really turns on: what a shape does to the Terms. A victory configuration is a function of the player count — VC-2120’s Standard War carries "Dominion (auto by P)" — so a shape that set the map and left the Terms alone would leave them describing a different game than the one being set up. applyShape therefore recomputes the Terms from the player’s own selected preset, and does not reset the preset itself; the control says so in a line beneath it. This is a UX-140 call site: recomputing discards any hand-editing of the Terms, which is precisely the case that requirement governs, and its machinery does not exist (recorded as a gap at UX-140 the same day). Of the two available wrongs this takes the narrower one, and when UX-140 lands this is one of the places it attaches to.

UX-270 Setup help: the wizard MUST expose the manual, a walkthrough of the setup controls themselves, and a launchable interactive tutorial (§14).

Flagged gap, 2026-09-23 — and the pieces exist, which is what makes it worth recording precisely. The client ships help/HelpPanel.tsx with help/topics.ts, and tutorial/TutorialPanel.tsx with its model. What is missing is the exposure: nothing in client/src/setup/ references either, so the wizard does not offer the manual, a walkthrough of its own controls, or a way to launch the tutorial.

So this is not "build a tutorial" but "wire three existing surfaces into the setup screen", which is a much smaller piece of work than the requirement’s wording suggests — and the walkthrough of the setup controls themselves is the only one of the three that has no component yet. Recorded this way so nobody reads §3’s help clause as waiting on §14.

Closed 2026-09-24. The wizard's header carries all three: The manual opens the help document over the wizard, Play the tutorial closes setup and opens the tutorial, and Walk me through setup is the new piece (client/src/setup/walkthrough.ts) — one card per wizard step, in the wizard's order, each shown on its own step so the controls it names are on screen beside it. A step added to the wizard without a card fails setup-help.test.ts.

UX-275 Hotseat: where two or more seats are local humans, the client MUST take their Orders phases in sequence and MUST fully occlude the previous player's view before handing over (TM-460). The hand-off screen MUST name the incoming player and MUST require a deliberate action to reveal the board. The rules MUST NOT differ from concurrent play in any respect.


4. The Orders phase

This section replaces the unit-by-unit prompting loop that an earlier draft specified. There is no processing queue, no active unit, no Continue key, and no unit that must be dealt with before another can be touched.

UX-280 The Orders board. During an Orders phase the client MUST present a board that does not change (TM-340) and MUST let the player move freely among their units, cities and command objects in whatever order suits them (TM-330), with no forced sequence and no unit blocking access to another. The client MUST NOT present any "next unit needing orders" affordance; routing attention is the Dispatch's job (UX-810) and duplicating it here would reintroduce exactly the per-unit tax the turn model exists to remove.

*Held 2026-09-23 as packages/client/test/orders-phase.test.ts. The prohibition is the half that needed a test, and it is the half a functional suite cannot see: a "next unit needing orders" button is an affordance a helpful contributor adds without malice, every existing test still passes, and the per-unit tax is back. So it is scanned for across the whole client over comment- and string-stripped source — and separately in keys/catalogue.ts, because a navigator arrives as a hotkey before it arrives as a button, and the catalogue is a single enumerated list that can be checked exhaustively rather than by substring luck. A mutant of each form is killed.*

The freedom-of-movement clause is held positively: every own unit opens a panel, and the rows ask in forward and reverse order — if any unit gated another, the second pass would differ from the first.

UX-285 Revision is free, and it is free until the phase closes. Every order MUST be revisable any number of times, at no cost and with no record beyond the log (TM-360). The client:

  1. MUST NOT present a per-order confirmation step, a commit-per-unit affordance, or any language implying an order becomes fixed before the phase closes;
  2. MUST provide an undo and redo of the player's own recent order edits within the phase (Ctrl+Z / Ctrl+Y), bounded only by the phase;
  3. MUST state plainly, at the commit affordance, that after the phase closes there is no undo of any kind.

Rationale: this retires the retraction machinery an earlier draft specified. Under a sequential model, orders executed as they were issued, so undoing an applied order needed a whole mechanism with purity conditions about fog and randomness. Under Orders and the Cascade nothing executes until the phase closes, so the Orders phase is the undo window, and it is total. The client needs no core retraction support at all, which is a real simplification bought by the turn model and worth naming where a reader will look for the old feature.

UX-290 What changed while I was away. On entering an Orders phase the client MUST offer, in one action, a review of the previous Cascade (TM-2230) and the after-action report it produced (UX-355). It MUST additionally mark every unit whose situation changed during that Cascade — a stack level popped, a Fallback Posture adopted (CM-310), a Reflex still active (CM-1400), an activation that fell back to no action (TM-1400) — so that revision starts from what happened rather than from memory.

The projection half landed 2026-09-25 (Codex CLI): each mark rides a published surface, so the review can start from what the seat already holds — a Fallback adoption is named by PlayerView.fallbackAdoptions (VIEW_SCHEMA_VERSION 27), derived at read time from this seat’s recorded attention events of the previous Cascade in CM-1340’s two adoption classes, with no new stored state; a Reflex still active, and its cause beside it, ride the canonical own-unit order stack; an activation that fell back to no action is TM-1400’s NoOpActivation record (the unsatisfied ones) in the view stream, beside the stance-fallback rate TM-1440 instruments; a stack level popped is what two consecutive checkpoints disagree about. What remains for this row is its own screen: the one-action review with UX-355’s after-action report.

UX-295 Readiness and commit. A single always-reachable control MUST mark the player ready, submitting CommitOrders (TM-220), and MUST remain revocable with RevokeCommit while the phase is open (TM-420). The control MUST make three facts plain:

UX-300 Presence. The client MUST show, for every other seat, whether it has committed, whether it is currently connected, and how much time remains (TM-440). It MUST NOT show or imply anything about another player's orders, order count, unit selection, camera position, or edit activity (TM-450). Presence MUST be presented as what it is — ephemeral, non-authoritative, and carrying no fact about the game world.

Held 2026-09-23, and the prohibition is met at the type level, which is stronger than any component test could be: Presence (core/src/net/protocol.ts) carries exactly MP-186’s six fields — seat, connected, committed, deadlineSeconds, reserveSeconds, idleSeconds — and a field that does not exist cannot be rendered. The five facts TM-450 forbids (orders, order count, unit selection, camera position, edit activity) are each named individually as well as covered by the set equality, because an equality alone would still pass if the list were widened and the test updated to match without anyone re-reading TM-450.

A second row closes the other route: the type closes the wire, but a client that read an opponent’s selection from some other channel and drew it on the presence strip would satisfy the first row and violate the requirement. So every client module that touches Presence is scanned for that reach.

UX-305 Deadline and reserve. Where a deadline exists the client MUST show the remaining phase time and the player's remaining reserve bank (TM-510), MUST warn before the reserve begins to draw down, and MUST state that a timeout costs nothing but the opportunity to revise — no forfeited movement, no production penalty, no initiative penalty (TM-530, TM-540). In solo, two-player and hotseat games the client MUST NOT display a clock at all unless one was explicitly configured (TM-500).

Rationale: a visible countdown in a game that has no deadline is a source of anxiety with nothing behind it. TM-500 makes the common case clockless; the interface has to honour that rather than showing a disabled timer.

Held 2026-09-23 on the clause the rationale singles out: a disabled timer is the defect, not the fix. The shape that makes this cheap to honour is already in the protocol — deadlineSeconds and reserveSeconds are number | null, not numbers with a disabled flag — so the clockless case has no value to render rather than a value to grey out. Both the nullability and the absence of any disabled-clock state anywhere in the client are pinned, and a mutant that turns the deadline into a bare number is killed.

Flagged gap, 2026-09-23: the positive half is unbuilt. "Where a deadline exists the client MUST show the remaining phase time and the player’s remaining reserve bank, MUST warn before the reserve begins to draw down, and MUST state that a timeout costs nothing but the opportunity to revise." No deadline mode is built, so there is no countdown, no reserve readout and no draw-down warning to test. It waits on MP-990’s stall policy being configurable at setup; the sentence about a timeout costing nothing (TM-530, TM-540) is the row to write first, because it is the one a player reads under time pressure and the one most likely to be omitted as self-evident.

UX-310 The initiative readout. For every unit they own the client MUST show the exact integer Initiative and the integer contribution of every term active at the current Tier (TM-1090, TM-1080), MUST recompute and re-display it live as the player changes Stance or Posture (TM-370), and MUST name the terms that are inactive at this Tier so a player understands what the number is not counting

Held 2026-09-23. The third clause is the one with a real failure mode and it gets its own row: an inactive term is named as inactive, never shown as a zero. A bare 0 cannot distinguish "in command, contributing nothing" from "command is not in this game", and TM-1040/TM-1060 make exactly that difference the purpose of the tiers. The fixture is chosen so the distinction is provable rather than assumed: four terms read "not at Tier 1", three carry integers, and one of the three is legitimately 0 — without that unit the rows would pass on an implementation that used 0 to mean inactive. (TM-1060).

UX-315 Own activation order. The client MUST show the player's own units in their relative activation order before committing (TM-380). The order MUST NOT be directly reorderable — it is a consequence, not a control — but a Stance or Posture change MUST visibly move a unit within it, so the player learns the lever by using it. The client MUST NOT show the global activation order, any opponent's position in it, the size of the roster, or any opponent's exact Initiative (TM-1120, TM-2120).

UX-320 Enemy initiative MUST be shown only as the three coarse bands Quick / Steady / Slow of TM-1100, only for units the player currently observes, and a remembered contact's band MUST be shown as of the last observation and marked stale (TM-1110). No finer figure may be displayed, derived, or implied anywhere in the client.

Held 2026-09-23, and the finding is that this requirement cannot currently be satisfied as written.

The prohibition is met, and met by construction rather than by care. unitPanel resolves its unit from view.ownUnits and returns null otherwise, so the section that draws Initiative has no foreign unit to draw it for — the failure mode the requirement guards against is a component that renders both sides through one code path, which works, looks right, and leaks TM-1120’s exact integer. Deeper still: no PlayerView surface carries a foreign unit’s Initiative at all. The only two surfaces carrying one are relativeActivation and frozenActivation, both documented own-units-only (checked per field, not by counting occurrences), and a Contact — the register of what a player has seen of everyone else — carries a class and a count band and no initiative whatever.

Which is exactly why the positive clause is unbuilt. "Enemy initiative MUST be shown ... as the three coarse bands" requires a channel that does not exist. initiativeBand is implemented and exported from core/src/turn/initiative.ts (Quick ≥ 50, Steady ≥ 30, Slow below) and has no caller — the function is ready and nothing feeds it, because the band would have to ride on the Contact or on the visible-unit projection, and neither carries it.

The ruling this needs from the spec owner is where the band lives, and it is not a free choice: a Contact is fidelity-graded (GR-1200–GR-1260), so "the band of a unit you have seen" has to say at what fidelity — a Trace sighting that yields an initiative band would leak more than the sighting itself does. The cheap reading is that the band rides at the fidelity that already reveals the class, since the band is a function of the class base plus stance, and a viewer who knows the class already knows most of the band. Recorded rather than implemented, because putting a field on a hashed projection is a re-emission and the placement should be ruled before it is paid for.

UX-325 The commit gate. Before the phase closes the client MUST present a warning summary that never blocks: the count of unresolved Dispatch items above the urgency threshold, default 120, naming the threshold (CM-1820, CM-1830), suppressible for the remainder of the game in one action (CM-1840). It MUST additionally list, as warnings with deep links and never as blocks:

Warning Source
Units that are Idle — no Posture set CM-540
Cities with no eligible production order US-1250
Cities discarding Works because the Yard is capped US-1240
Postings whose Requisition shortfall is unmet CM-1250
Cities with a Stalled production order US-1040, US-1060

Each category MUST be individually configurable as warning or ignored. No category may be configurable as blocking.

Rationale: 13-command.md settles this — blocking is patronising, not warning is negligent — and the client should not relitigate it per category. Under persistent orders (TM-280) an unattended unit is not an emergency; it is doing what it was told to do last turn, which is usually right.

Held 2026-09-23 on the clause that carries the weight — "never blocks" — and held exhaustively: turnEndWarningGate takes one count and two booleans, so the input space is small enough to enumerate rather than sample, and across all of it the answers are exactly warn and close. The return type admits no third value, and a mutant that adds a block branch above a threshold is killed. The row also checks that warning is reachable, since "never blocks" would otherwise be satisfied by a gate that does nothing at all, and that a second press goes through — a warning is a speed bump, not a gate.

CM-1840’s suppression is held on all three of its clauses: one action, held for the game, and this seat’s alone — plus the clause that is easy to miss, that it dies with the game it was made in. Mutants for the cross-seat leak and the outliving game are both killed. CM-1830’s default of 120 is pinned.

Flagged gap, 2026-09-23: the five-category warning table is not built. The Dispatch half is — the count above the threshold, the threshold itself, the suppression — but the itemised warnings (Idle units with no Posture, cities with no eligible production order, cities discarding Works to a capped Yard, unmet Requisition shortfalls, Stalled production orders) are not assembled into the commit gate, and neither is the per-category warning/ignored configuration. The clause worth building first is the last one: "No category may be configurable as blocking." It is a constraint on the settings type, so it costs nothing while the categories are being written and is awkward to retrofit once a settings shape exists that can express a block.

Built 2026-09-25 (Claude Code): client/src/commit/gate.ts reads the five categories off the seat's view, the way the screens they link to read it. The Production Overview's rows give the city facts, the Posting list gives the shortfall, and a missing Posture marks an Idle unit. CommitWarnings.tsx lists them beside the commit control, each item a link: a unit or a city is selected on the map, and a Posting opens its panel. Each category is set in Settings under "Before you close the turn" to 'warning' | 'ignored', a type with no third value, and a stored "blocking" is dropped when the settings are read (commit/categories.ts). Three readings, recorded here for the spec owner. (1) The list is a standing summary rather than a second press. The Dispatch half keeps CM-1820's warn-then-close, but these five are usually non-empty, since a city waiting on its first order is listed from turn one. A close that took two presses every turn would teach a player to press twice without reading. (2) "No eligible production order" counts only a city governed by hand, because a Requisition or a Template fills its own queue. (3) "Discarding Works" is a Yard standing at its cap in force, whichever rule set the cap (US-1240 or EC-610). client/test/commit-warnings.test.ts holds each category on a real game, the setting's two values, and the links.

UX-330 Rolling planning. Where 10-turn-model.md §18 is enabled, the client MAY allow a player to begin staging a unit's orders for turn T+1 once that unit has activated during turn T's playback (TM-2550). Such staging MUST NOT reach the core before the next Orders phase opens (TM-2570), MUST remain fully revisable when it does (TM-2560), MUST NOT reveal anything the observation stream withholds or any activation index (TM-2580), and MUST be disableable with no rules effect (TM-2590). Where it is enabled the client MUST distinguish staged-early orders from submitted ones unmistakably, because the two look identical and behave differently. no-test-needed: a dormant MAY — 10-turn-model.md §18 is not enabled, so no staging surface exists in the client and the row’s four MUSTs bind nothing until it does; there is no behaviour to pin now, and when rolling planning lands this row stands for tests then. The flagged-gap note below already says as much: “the id is deliberately absent from the tests”.

Flagged gap, 2026-09-23: rolling planning is not built, and this is a MAY. 10-turn-model.md §18 is not enabled and the client stages nothing early, so every one of this requirement’s obligations is vacuously satisfied — which is worth saying plainly rather than counting as coverage, because the four MUSTs here only bite once the MAY is taken. The id is deliberately absent from the tests.

The clause to hold onto for whoever builds it: "the client MUST distinguish staged-early orders from submitted ones unmistakably, because the two look identical and behave differently." That is a presentation requirement protecting a correctness property, and it is the one that will be discovered late — the staging works, the orders are right, and a player commits a turn believing they already had. TM-2580’s clause (staging reveals nothing the observation stream withholds, and no activation index) is the one that needs a fog test rather than a UI one.


5. The Cascade

This is a first-class screen and the second half of the game's loop. The Orders phase is where a player thinks; the Cascade is where they find out. The design decision that produced this turn model argues that watching a plan execute is a different pleasure from poking a unit rather than a lesser one, and that claim is this section's responsibility to make true.

UX-335 Playback is presentation of a finished result. The Cascade MUST be computed to completion before any frame of it is drawn (TM-2240). The client MUST NOT present playback as a live simulation, MUST NOT offer any control that could alter it, and MUST NOT solicit input during it (TM-050, UX-030).

UX-340 Fog-filtered playback. The client MUST animate exactly the player's compacted observation stream (TM-2150), in the order it is given (TM-2190), and MUST NOT render, infer, or hint at anything the stream withholds — no gap markers, no "something happened" placeholders, no activation indices, no roster size, no count of activations (TM-2120, TM-2160).

Transits MUST be shown as transits. A unit that crossed an observed corridor and left it MUST be seen to cross it, not merely to have arrived somewhere (TM-2090). Rendering only end positions is the one failure that would make playback a lie, and it is also the cheapest corner to cut.

UX-345 Step-through and instant resolution. The client MUST offer:

Step-through MUST NOT be usable as a means of pausing a game with more than one human player (TM-2260). It is nevertheless available in every game shape, because in a game with two or more humans the whole observation stream has already been transmitted as one payload before playback begins (TM-2170): stepping through a recording pauses nothing, blocks nobody, and reveals nothing the player does not already hold. In solo, two-player and hotseat games instant resolution and step-through are additionally required by TM-2250 as the amendment that protects the common case.

Rationale: step-through is what gives back the tactile pleasure of seeing one result land, and the outcome is fixed before the first frame, so it costs the model nothing. The only thing that must be guarded is the wall clock of other people, and the single-payload rule already guards it.

UX-350 Pacing and clustering. Playback duration MUST NOT scale linearly with the number of observed events (TM-2280). The client MUST hold playback within a bounded budget by clustering — one marker per engagement, one arrow per column, one summary per contested area — with drill-down into any cluster, and MUST always produce the written report of UX-355 regardless of how much was animated. The clustering threshold and the total budget MUST be player-adjustable, including "never cluster" and "always summarise".

The client MUST instrument the skip rate: if more than half of players are skipping playback by turn 100, the answer is more clustering and a better report, never a longer animation.

UX-355 The after-action report. After playback the client MUST present a written report, navigable and copyable (UX-045), containing at least:

Content Source
Every observed engagement, with its reconciliation line — predicted band against actual result CB-2440
Every observed city or work that changed hands TM-2110
The count of the player's own activations that fell back to no action, with a one-action route to those units TM-2320, TM-1400
Every own unit whose activation slot was skipped, and which reason applied — destroyed, spent after landing, changed owner TM-2350
The frozen activation order for the player's own units, with each unit's Initiative and every term that produced it TM-2340
Every observed reaction, naming the reactor where observed TM-1710, UX-360
Every Attention Event raised by the Cascade, as Dispatch items CM-1610

Every entry MUST jump the map to its location without stealing keyboard focus from the report, so a player can walk a long report entry by entry.

Rationale: the fallback count is the model's key diagnostic (TM-1440) and the player is the person best placed to fix their own instance of it. "Four of your units had nothing they could do" turns an invisible waste into a specific, actionable list, and it teaches the conditional vocabulary faster than any tutorial.

UX-360 Attribution. Every observed event MUST name the unit that acted. Where the actor is not itself observed — a reaction fired from a tile the player cannot see, which 11-combat.md may still make observable — the actor MUST be shown as unknown rather than the event being omitted (TM-2310).

UX-365 Replay. The client MUST be able to replay any past Cascade from the log for the seat being viewed, reproducing that seat's observation stream exactly (TM-2290), with the same step-through, clustering and report. A spectator seat MUST receive a stream constructed by the same rules, for whatever view the game's spectator policy grants (TM-2300).

Built 2026-09-24 for the seat on screen (client/src/turn/replay.ts, Replay turn beside Last turn). A past turn is rebuilt by opening the game's own setup and seed on the journal up to that turn's CloseOrders, applying it, and taking the same compactedStream a live close takes — one computation, not a second one — and the stream goes to the same playback and report. Measured equal, value for value, to the live streams over three turns and both seats. The spectator clause waits on a spectator seat, which this build does not grant (TM-2300).

UX-370 Playback state — pacing, clustering, skip position, step cursor, camera — MUST be client state and MUST NOT contribute to any hash (TM-2330). The client MUST animate at its own pace and MUST NOT be able to affect the simulation by doing so (TM-2200).

UX-375 Waiting. While the Cascade is being computed, or its stream transmitted, the client MUST show progress and MUST remain fully usable: panels, reports, the encyclopedia, the Board and the previous turn's report all reachable (UX-055). A spinner over an inert screen is not acceptable at any phase boundary.

Implementation status (2026-09-23). The local close is met. The turn resolves in the worker (AR-780), and the wait is a status line with an indeterminate <progress> (shell/ResolvingStatus.tsx) in place of the modal that made the page inert until this date; panels, reports and command keys stay live. What the modal also did, keeping an order off a state the worker's answer is about to replace, is now done at the one door every player order passes: setClosing in orders/submit.ts refuses a player's order, undo and redo for the length of the close, each with a sentence saying why, and lets the close's own orders through. A close that answers after the player has opened another game is dropped whole, by the journal's epoch, so neither its state nor its orders reach the new game. The progress is indeterminate because the close is one call and reports none. Not met: the stream-transmission half, which waits on the live authority's close; and opening a save still holds the page (HoldDialog), which this requirement's "any phase boundary" does not obviously cover — a save opening is not a phase boundary.


6. Map rendering

UX-380 The map MUST be rendered by the Phaser 3 WebGL layer (UX-010) as a square-grid tilemap with 8-way adjacency (GR-180), supporting continuous smooth pan and zoom with inertial scrolling. Wheel zooms and drag pans, captured per UX-014 so the page never scrolls or browser-zooms underneath the player.

Because every distance in the specification set is Chebyshev (GR-170), every radius the client draws MUST be drawn as the square it is — a vision radius of 4 is a 9×9 box, a Leash of 4 is a 9×9 box, a reaction reach of 1 is a 3×3 box. Drawing a circle for a Chebyshev radius is a defect: it misstates the rule at exactly the corners where a player is deciding.

UX-385 Renderer robustness. The map layer runs on a resource the browser may take away, and the client MUST survive that:

Implementation status (2026-09-24), the context-loss bullet only. Phaser's renderer handles the GL half and announces both edges; the client shows a role="status" sentence beside the canvas layer (never inside it, UX-012) while the context is gone, keeps every DOM surface usable, and on restore rebuilds the scene's ground from the payload it holds without touching the camera. Checked in the running client through WEBGL_lose_context. Backgrounding was added the same day: the map's one elapsed-time animation, the glide, counts a resumed tab's first frame as one slow frame (100 ms) rather than the whole gap, and nothing on the turn's path holds a timer, so a backgrounded close is waited for, never abandoned (client/test/backgrounding.test.ts). Degradation is told, the same day: the fallbacks themselves are Phaser's under AUTO (its WebGL renderer asks for a WebGL1 context; where none can be made it starts the Canvas renderer), and the client now checks which renderer it got and, on Canvas, says so beside the map (client/test/renderer-fallback.test.ts). The declared reduced art tier is not built, so the sentence promises none. Not met until 2026-09-25: the environment-change bullet. The backing store was CSS-sized, so a high-DPR screen drew soft, and fixing it moves the camera zoom the LOD tiers key on. Built that day (Claude Code): MapCanvas sizes the game in device pixels and shows it at CSS size (the scale manager's zoom is 1 / DPR). The scene reads its zoom in CSS pixels through cssZoom wherever a player's eye is the measure: the LOD tiers, the limits of the wheel, pinch, keys and opening frame, and the screen-sized marks. It keeps the camera's own zoom where device pixels meet device pixels: a drag's delta, the seam's viewport, and the fit's ratio. Checked at 1× and 2× in a headless browser: at 2× the backing store is 2132 × 950 behind the same 1066 × 475 box, the framing and tier match, and a click lands on the same tile (client/test/high-dpi.test.ts). The density is read once, so a window moved to another screen keeps correct geometry at a resolution a little off.

UX-390 Level of detail: the renderer MUST implement at least these zoom tiers, transitioning smoothly:

Tier Approx. zoom Unit presentation Terrain presentation
Close ≥ 1.0× full sprites, state variants, animation, condition badge (UX-1125), Stance and Posture glyphs full terrain art, rivers, installations
Mid ~0.5× simplified sprites, no idle animation, ownership tint, condition badge full terrain art, simplified decals
Chip ~0.25× flat chips in the owner's colours bearing the unit's short class label; one marker per occupied location, sized and counted flat terrain colours, roads and rivers as lines
Strategic ≤ 0.125× owner-coloured markers only; one marker per occupied location with a count; Formation and Group markers under UX-395 flat terrain colours only

UX-395 Command View. The client MUST provide a zoom level at which individual unit icons are replaced by Formation and Group markers, each rendering a Posture glyph, a strength bar, an arrow showing current intent, and any Dispatch pins in that area (CM-1880). Every Formation, Group and Posting order MUST be issuable without leaving it (CM-1900). Dispatch items MUST pin to map locations; hovering an item MUST move the camera to it and resolving an item MUST return the camera to where it was (CM-1890).

Rationale: 13-command.md is right that on a large map individual unit icons stop meaning anything, and that this is not a nicety — it is the only zoom level at which a late-game empire is legible. It is also what keeps the Dispatch from becoming a list a player reads instead of a map they look at.

UX-400 Large maps: map data MUST be streamed and culled in chunks so that any map across GR-120's full range renders within a fixed memory budget that does not grow with map size. Scrolling across unloaded chunks MUST show a neutral placeholder and MUST NOT stall the frame. The budget MUST cover GPU texture memory as well as heap, MUST be sized for the mobile WebView case, which is the tightest of the six targets, and exceeding it MUST evict distant chunks and never crash the context (UX-385).

UX-410 Wrap rendering: on a cylinder or torus map (GR-190) panning MUST be seamless across each wrapped edge, with entities drawn on both sides of the seam, and the minimap MUST indicate the wrap. The polar impassable strip a cylinder map carries (GR-220) MUST be visually distinct from ordinary terrain so a player does not plan a march into it.

Built 2026-09-24 for the east-west seam (client/src/map/wrap.ts, the scene, the shell). Found the same day: a default game is generated as a cylinder, GR-190's default, while every client surface was told flat, so the seam was an edge on screen and a neighbour in the rules. The shell now reads the topology off the world; the scene keeps its camera inside one period of the world and draws what lies past the east edge with a second camera looking at the west edge, so panning never meets an edge and entities on both sides are drawn; clicks fold their column; the minimap and the map-anchored DOM are told. The polar strip is hatched on a cylinder. Checked in the running client: a lap east brings the explored ground back in from the west. Not built: a torus's north-south seam.

UX-420 Fog and the Intelligence Picture. The client MUST distinguish the three tile states of GR-1120 — Unexplored (opaque), Explored (terrain known, contents from memory), Observed (current and complete) — and MUST discharge GR-1190 and GR-1870 in full:

  1. Remembered is never mistakable for observed. Every remembered unit, garrison, installation and ownership MUST be rendered distinctly from a current observation, and MUST carry its observation age (GR-1170) and its fidelity grade.
  2. Fidelity is shown, not flattened. A Contact MUST be rendered at exactly the detail its current grade admits (GR-1260): Trace as an unattributed marker with a domain only; Sighting as an owner-coloured marker with a count band — one, a few, many — and no identity; Identification with exact count and classes; Assessment with strength, Stance and condition. Rendering a Sighting as a typed unit icon is a defect under this requirement.
  3. Position is a region. A Contact whose uncertainty radius is non-zero MUST be drawn as the region GR-1330 and GR-1340 define — the projected centre and its Chebyshev box, minus exclusions — and MUST NOT be drawn as an icon on a single tile. A tracked Contact's region MUST be visually distinguishable from an untracked one, because the difference between the two is the single largest payoff of keeping observers on the map.
  4. Exclusions are visible. Tiles excluded from a region by later observation (GR-1350) MUST be shown as excluded, so that a patrol which found nothing has visibly narrowed the search rather than appearing to have wasted its turn.
  5. Decay is legible. A Contact approaching the grade drop or the drop threshold (GR-1300, GR-1320) MUST show that it is about to, so re-scouting is a decision a player can schedule.

UX-425 Perceptible events. A Trace created by a perceptible event (GR-1390, GR-1400) — a battle heard beyond sight, a bombardment, an expendable unit resolving — MUST be rendered as an unattributed, dateable marker that reveals owner, count, class and strength to nobody (GR-1410). The client MUST NOT decorate it with an inference, a guess, or a probable owner.

Pinned 2026-09-24 (client/test/perceptible-marker.test.ts). Built with the Contact model: a Trace is drawn as a small hollow grey diamond and described as "Something on the ground — seen now", with no owner, count, class or strength in the mark. The pin drives a real event through the engine's applyPerceptibleEvent into a live game's register and reads it back through the model the map and the panel draw from, and scans the scene's Trace branch for any seat colour.

UX-430 Unit state visuals. A unit MUST be visually distinguishable across every state the rules give it, and the following MUST each be readable at the Close and Mid tiers:

State Presentation Source
Condition — strength and disorder one composite badge, never two raw numbers CB-190, UX-1125
Grade (veterancy) a rank mark CB-170
Dug in (digIn 0–4) and established entrenchment a works mark that grows CB-100, TM-700
Shaken a distinct overlay for its duration CB-100
Stance — March / Advance / Hold / Entrench a glyph, with the derived value shown where an override replaced it TM-630, TM-730
Posture — Hold / Garrison / Screen / Survey / March / Attach, or Idle a glyph; Idle MUST be conspicuous CM-330, CM-540
Forming Up (age 0–3) a mark that visibly expires TM-950, TM-960
Supply grade a mark in the economy's three-grade vocabulary EC §13.5
Encircled — no retreat candidate marked for both owners whenever the unit is visible to the viewer CB-1410
Reaction capacity armed / spent a mark; own units always, enemy units only at the disclosure TM-1740 permits TM-1530, TM-1740
Banked movement shown as movement points on the panel and marked on the map CM-1550, CM-1590
Carried cargo a loaded mark; contents only at the fidelity GR-1060 admits GR-1060
Air endurance remaining a countdown mark US-2000, US-2040

Where several marks would collide, the client MUST prioritise by severity — encircled, out of supply, condition, Stance, Posture — and MUST make the remainder reachable from the unit panel rather than dropping them silently.

UX-440 Ownership rendering. Unit art MUST carry a team-colour channel — a per-pixel mask, authored alongside the art, marking which pixels take the owner's colours — and the renderer MUST substitute the owner's foreground and background into the masked region. Art outside the mask renders as authored and MUST be free to carry shading, material and mechanical detail.

The mask is constrained by an outcome, not by a technique or a colour count: ownership MUST be unmistakable from the silhouette alone at the Chip tier (UX-390, ~0.25× zoom, an 8 CSS px sprite), without reference to insignia, trim, or any detail that survives only at higher zoom. A design that tints only a unit's markings fails this and MUST be rejected. Ownership MUST NOT be carried by hue alone at any tier (UX-1310), so the mask works alongside the shape and pattern encodings that requirement demands.

The technique is not mandated. A WebGL shader reading the mask, and pre-tinted atlases baked per seat colour at load time under the Canvas fallback (UX-385), are both conforming. The number of tones in the authored art is likewise not mandated: two is permitted and may be the right answer for small units, but it is a choice the art direction makes per asset, not a rule.

Rationale, recorded because this requirement previously had none and was wrong for it. An earlier draft mandated two-tone art tinted by palette substitution. That is a 1993 technique: it exists because palette-indexed graphics gave you 256 colours and recolouring meant swapping reserved palette slots. On a GPU it buys nothing, and it discards shading, material and depth for no benefit — the exact kind of inherited constraint docs/design/00-direction.md §1 exists to reject, and the more dangerous kind, because it cites no source and so cannot be found by searching for one.

What that requirement was protecting is nevertheless real, and this one keeps it. At a 32 CSS px tile with enemy and friendly units adjacent constantly, ownership has to read from the whole shape at a glance; a shaded illustration whose only team colour is a shoulder flash is unreadable at speed and invisible at the Chip tier. The mask preserves that guarantee while removing the arbitrary limit — it can cover most of a sprite where legibility demands it, and leave tracks, barrels, canopies and cast shadow untinted where they add information. It is strictly more expressive than the rule it replaces and can still produce a flat two-tone counter wherever that is the better design.

UX-450 City rendering MUST reflect: the owner's colours and, for a city the player has ever explored, the owner as a public fact that fidelity never withholds (GR-1290, GR-1430); occupancy; Industry level 1–4 where observed, with its observation age (US-810, US-970); Integration and Unrest bands where the city is the player's own; and badges for a Seat, a Landmark, and any city currently published as a Bellwether (VC-2280, VC-3110). A city in the Unsettled state, or holding a Stalled production order, MUST say so on the map.

Partially held 2026-09-27: the clauses stand apart under their own rows, and no single test holds all six, so the id stays untitled. Verified pieces: the Unrest/Unsettled clause stands in city-bands.test.ts — every band begins exactly at the threshold the economy gives an effect to, and a city in the Unsettled state says so; owner-colour channels stand measured in map-colour.test.ts and chrome-colour.test.ts under UX-215’s and UX-1310’s names. The remaining clauses — occupancy, Industry level with its observation age, and the Seat/Landmark/Bellwether badges — each stand where they are built; none is held together under this id.

UX-460 Map overlays. The client MUST provide individually toggleable overlays, each with a control and a bindable key, and each drawn from the player's filtered view only:

Overlay Content Source
Supply (Reach) the three supply states across the whole map EC-1880
Projected supply the network as it would be with a Depot on a chosen tile, or a chosen city at Integration 50 EC-1880
Attack odds every enemy-occupied tile adjacent to the player's units, coloured by the break probability of the best available attack CB-2430
Intelligence Contact regions, fidelity grades, observation age, exclusions GR-1170, GR-1330, UX-420
Industry the Industry level of every observed city US-970
Integration and Unrest per-city bands across the player's own cities EC-1810, EC-1820
Victory sites Landmarks, Region boundaries, Seats, and any published Bellwethers VC-2280, VC-3110
Formations Formation and Group membership, the Command Reference Point, and the cohesion radius CM-950, CM-1010
Postures every own unit's assigned geometry — Screen lines, Survey regions, March routes — and its Leash radius CM-330, CM-630
Reaction cover tiles covered by the player's own units holding reaction capacity TM-1570, TM-1740
Endurance range and return envelope for the selected air unit US-2000, US-2040
Grid cell boundaries ours

The reaction-cover overlay MUST show the player's own units only. An enemy unit's reach MUST appear only where TM-1740 permits it — a visible enemy whose class the player has observed — and MUST NOT be aggregated into a coverage field, which would disclose a map of enemy Stance the rules do not publish.

UX-470 A minimap MUST be available showing explored terrain, ownership, the current viewport, and waypoint bookmarks; clicking it centres the main map. It MUST be collapsible, and it MUST indicate wrap (UX-410).

Implementation status (2026-09-24). Met, bookmarks included since the same day (UX-490's note). map/MinimapPanel.tsx sits in the strip over the map's lower-left corner (beneath the map below 900 pixels, since 2026-09-25) and draws one pixel per tile from the seat's PlayerView (map/minimap.ts, reading the same explored section as the scene through map/explored.ts): fog where unexplored, terrain or the city owner's colour where explored, and the remembered treatment where not seen now (UX-420). The viewport rectangle is drawn from the canvas layer's frame (map/frame-feed.ts, published from the same post-render as UX-016's anchors), a click centres the main map, a disclosure button collapses it, and a sentence says whether the map wraps. Bookmarks are drawn as marks on it, from the list UX-490's surface keeps.

UX-480 All animation MUST be skippable and MUST respect a global animation-speed setting including instant (UX-345). Outcomes MUST be identical regardless of animation settings, because by construction they were computed before the first frame (TM-2240).

Implementation status (2026-09-24). One setting, animationSpeed (slow, normal, fast, instant), paces both animations the client has: the Cascade's auto-play (its step interval scaled 2000‰ / 1000‰ / 400‰) and the map's inertial glide. Instant opens the Cascade on its report at the last step, exactly as UX-345's instant mode does, and a released drag stops where it was let go. A reduced-motion preference starts it at instant (UX-1350). Both animations were already skippable: the playback by its Skip control, the glide by the next drag. No outcome can depend on it, because the Cascade is computed before its first frame.

UX-490 Camera. Centring on the selection MUST be idempotent: repeated invocation MUST NOT zoom, cycle, or otherwise change behaviour on the second press. The client MUST provide camera bookmarks, a waypoint jump list (UX-870), and a jump-to-location that never steals keyboard focus from the list or report that invoked it.

Bookmarks, 2026-09-24. The minimap panel (UX-470) carries them: Bookmark this view saves the main view's centre to the lowest of nine free slots, each saved place has a Go to that centres the camera and leaves keyboard focus on the button that asked, and a Forget. They are kept per game in client storage (elc.bookmarks.<game>), versioned, and a corrupt record costs the bookmarks and never the game. No default keys: the key table (UX-1220) names none, and none is invented here. The waypoint jump list is UX-870's and is not this.

Rationale: this replaces an inherited quirk in which repeated centring progressively zoomed and then bounced back out. It was a curiosity of another program, nobody asked for it, and a control that does a different thing on the second press is a control players stop trusting.

UX-495 Terrain legibility. The eleven terrain types (GR-260), River edges and Fords (GR-1550), and every installation (Road, Fort, Airfield, Bridge) MUST be visually distinguishable at the Close and Mid tiers, and the hover readout (UX-520) MUST state, for the hovered tile: its terrain, the movement cost for the selected unit's movement class, whether it is enterable at all, its defensive value as the rules compose it, its concealment, and any installation. A player MUST never have to consult the encyclopedia to learn what a tile in front of them costs.


7. The persistent HUD

UX-500 The client MUST provide these HUD regions, each dockable and collapsible except where stated:

Region Default position Content
Phase indicator top centre current phase and turn number (TM-090); where a deadline exists, time and reserve remaining (UX-305)
The Watch docked, never dismissible one row per player (UX-1150; VC-2340)
Empire strip top left Works income, Manpower pool and levy rate, next Cadre price, with a route to the Empire Ledger (UX-510)
Selection panel lower left the selected unit, Formation or city (UX-530)
The Dispatch right the ranked attention list (UX-810)
Location readout lower right the hovered tile (UX-520)
Commit control bottom readiness and commit (UX-295)
Command bar bottom commands applicable to the current selection (UX-560)
Menu top right save, load, settings, exit

UX-510 The empire strip MUST show the core-computed values only (UX-040): Works accrued this phase, the Manpower pool against its cap, the current levy rate, and the price of the player's next Administrative Cadre — which 12-economy.md requires to be permanently visible as one of its six named brakes (EC-030). Each figure MUST link to its decomposition in the Empire Ledger (UX-1070). No figure here may be a bar without a number (EC-050).

UX-520 The location readout MUST populate on hover with the hovered tile's terrain and its derived costs (UX-495), its contents as fogged with age and fidelity (UX-420), the owner of any city present, and the Chebyshev distance from the current selection. On touch devices, long-press inspect substitutes for hover (UX-1390).

UX-530 The selection panel MUST show, for a selected unit, at all times and without a click: its identity and class; its full order stack in plain language (UX-740); its effective Stance and the derived value it overrode (TM-730); its four Sanction values and the level that supplied each (CM-760); its Fallback Posture (CM-310); its exact Initiative with every active term (UX-310); its condition, supply state and endurance; its reaction capacity, spent count and reach (TM-1740); its banked movement (CM-1590); and its next intended action as one plain-language line (CM-1910), or an explicit statement that the next step depends on a contact not yet made (CM-1930).

For a selected Formation the panel MUST additionally show membership and roles, Pace and cohesion radius, the commander, and whether the Formation is Responsive or One turn behind, in words, with the reason (CM-990, CM-1080).

UX-540 The phase indicator MUST name what the current phase permits. Entering the Cascade MUST visibly disable every order-authoring control rather than leaving it live and rejecting the order (UX-030, UX-645).

UX-550 Layout: every HUD region except the Watch MUST be dockable, collapsible and scalable, and side placement MUST be flippable left/right. Layouts MUST persist per profile and MUST be resettable in one action.

Built 2026-09-23 (client/src/hud/layout.ts and HudLayoutPanel.tsx, held by client/test/hud-layout.test.ts). The requirement quantifies over "every HUD region", so the region list is UX-500's table parsed out of this document rather than transcribed — the same arrangement VC-2120's presets and TM-490's deadlines use, and for the same reason: a hand-copied table drifts with whichever side was edited and then agrees with neither. The parse caught its first disagreement immediately: the module had the Watch's cell as "docked" where this document says "docked, never dismissible", and the document won.

Three clauses are pinned as prohibitions, because that is where each fails quietly. The Watch is excluded by carrying no layout at all — every mutator returns the arrangement unchanged when handed it, which is stronger than a comment saying not to. A stored layout cannot produce a region with no arrangement: the load heals in both directions against the current table, so a region the store has never heard of takes its default and one the table no longer names is dropped, rather than the interface rendering a control with nothing behind it. And the scale band is imported from UX-1330's settings rather than restated, so the HUD cannot come to admit a scale the settings screen refuses.

A reading recorded for the spec owner. "Side placement MUST be flippable left/right" is read as applying to a region that has a side. UX-500 places five that way — the empire strip, the selection panel, the Dispatch, the location readout and the Menu — and those flip. The phase indicator is top centre and the commit control and command bar are at the bottom: they have no side, and giving them one would put a control where UX-500 does not. The reading is derived from the table's own position cell rather than listed a second time, so a change to the table moves it.

A second reading, about a region this client does not have. UX-500 places a Menu region — save, load, settings, exit — at the top right. This client has no such region: those controls sit among the others in the header, which is where the layout's join points, so customising the Menu customises the header containing it. That is more than this row asks for and less precise than it deserves; separating the Menu is §2's work, and the flag stands at both requirements.

One limit of the tests, stated rather than left to be discovered. The model, the controls and the attribute-and-variable join are all held by the suite, but jsdom loads no stylesheet, so nothing in the suite can prove a token the shell emits actually moves anything. The tests pin that every variable and every token the model emits is one app.css names; the effect itself was verified in the running client. That gap bit once in the building: the first flip rule used auto margins alone, and a grid item that is not stretching has nothing for an auto margin to distribute — so the flip was recorded, stored and visibly did nothing. The rule now carries justify-self beside the margin.

UX-560 The command bar MUST present the commands applicable to the current selection, each contextually enabled, and each disabled control MUST state why in one sentence — for a build option the failing clause of US-360 (US-370); for an order the named rejection the core would return (TM-250, AR-320); for an automation decision the numbers, in the form "stopped because sanctioned at ≥700‰; predicted 610‰" (CM-2000). A greyed control with no explanation is a defect under this requirement.

UX-570 Section customisation: each button section MUST support expand and reduce, drag reordering by exchange, and hiding individual commands. A hidden command's bound key MUST continue to function, and the command MUST remain reachable from the palette (UX-610).

Built 2026-09-23 (client/src/hud/sections.ts, held by client/test/command-surfaces.test.ts). The second half is the load-bearing one and it is built as a negative: no module under src/keys imports the layout, so hiding a control cannot reach what a chord dispatches or what UX-610 lists. That is pinned as an import boundary rather than as a rule two modules must remember — and the first attempt pinned it as a scan for the word "hidden", which failed on three comments each saying a control's reason is never hidden. "Reordering by exchange" is read literally: the dragged item and its target swap and everything between them stands still, which differs from a splice on any non-adjacent pair, and that pair is the row that decides it.

A join recorded for the spec owner. A layout is over catalogue commands, because "a hidden command's bound key" is only meaningful for something with a key. The bar's rows are over the selection, and several are not commands at all — a build: row is a queue item, the levy and the raze are one-shot acts, and none has a binding or a palette row. Those gather under one section and cannot be hidden, on the reading that this requirement's clause does not reach a control with nothing to preserve. The "persist per profile" half of the bar's arrangement is UX-550's clause, with the docking and the one-action reset; this slice holds the arrangement in session state and UX-550 is where it learns to survive a reload.

UX-580 A single control and key MUST toggle the whole non-essential interface for near-full-map play. The Watch and the phase indicator MUST remain visible.

UX-590 Tooltips: hovering any control MUST show its description and its current key binding. Tooltips MUST deep-link to the relevant help or encyclopedia topic.

UX-600 A context menu listing the selection's currently applicable commands MUST be available on secondary click (desktop) and long press (touch, UX-1400). It is a DOM menu anchored by UX-016 — focusable, arrow-key navigable, and announced by screen readers like any other menu — and the browser's own context menu MUST be suppressed over the map surface only (UX-014.3).

Built 2026-09-23 (client/src/hud/context-menu.ts and ContextMenu.tsx, the suppression at the canvas seam in map/MapCanvas.tsx, held by client/test/command-surfaces.test.ts). "Currently applicable" is the difference from UX-560: the bar shows every command the selection has and says why each inert one is inert, and the menu is that list filtered to what can run — so the two share one applicability answer rather than each computing its own, and the test pins the agreement across selections and phases rather than asserting a list, which would pass if both sides drifted together. The suppression and the replacement are one handler on the canvas element, never on the document: a document-level handler would take the native menu away from the text fields UX-045 requires it in, and suppressing without replacing would leave a secondary click doing nothing at all.

One thing the rendered accessibility pass caught that the behavioural tests could not: an empty role="menu" is an ARIA violation — the role has required children — so the empty case is deliberately not a menu, just a labelled focusable box carrying the sentence UX-630 owes. A menu that declined to open would have been the worse answer.

Anchored, 2026-09-23. This row's "anchored by UX-016" was owed while the projection did not exist, and the menu took a screen position from the gesture rather than invent a second projection. It is now met: the gesture's point is turned into a cell by the projection's inverse, the menu's anchor is that cell, and the registry places the menu and re-places it on every frame — dismissing it when its cell scrolls off the map (UX-016). The touch half (long press, UX-1400) reuses map/long-press.ts's existing fire, wired 2026-09-24 (see UX-1400).

UX-610 Command palette: a searchable palette (default Ctrl+K) MUST list every command in the game, including keyboard-only ones, with descriptions, current bindings, and applicability; invoking an applicable command executes it. Nothing in the client may be reachable only through an undiscoverable binding.

UX-620 Chat: in multiplayer games a chat surface MUST be present; behaviour, retention and moderation are specified in 05-multiplayer.md.

Implementation status (2026-09-23). Met for live games: hud/ChatPanel.tsx is drawn whenever the page is in a live session, with the game-wide channel and a private recipient per other seat, each line turn-annotated (net/chat.ts). LiveGame.sendChat sends MP-270's Chat and every ChatRelay is shown and nothing else, so chat reaches no order, state or hash (MP-185). Retention across a reload and correspondence delivery wait on the authority's record and on correspondence play. A reading recorded for the spec owner: ChatRelay does not carry the recipient, so a player who receives a private line cannot tell it from a game-wide one; the sender keeps its own private lines marked as private. Marking them for the recipient needs a field on MP-270's relay.

UX-630 No dead states. Whenever nothing is selected and no dialog is open, the client MUST state what the current phase expects of the player — "issue or revise orders; commit when ready", "watching turn 41", "waiting for two players". A screen that appears to be doing nothing, with no explanation, MUST NOT be reachable at any phase boundary.

UX-640 Dialog framework: floating panels MUST be movable and resizable; all dialogs MUST support arrow-key and Return navigation of controls and lists and full focus traversal (UX-1325). Modal dialogs MUST use native top-layer modal semantics (background inert, Escape closing per UX-014.5, focus trapped and restored per UX-1325), and moving and resizing MUST use pointer capture (UX-014.2) so a drag that crosses the canvas does not detach. Size and position MUST persist per profile and MUST be re-clamped into the viewport on resize, browser zoom, and UI-scale change (UX-1330) — a panel may never end up unreachable off-screen.

UX-645 The client MUST NOT present a control that is inert in the current phase. A control requiring the Orders phase MUST be disabled during Cascade and Reckoning with the phase named, and MUST re-enable at the next Orders phase without the player having to re-select anything.


8. Selection, targeting, and issuing orders

UX-650 The selection and targeting model MUST be exactly one model:

Input Effect
Primary click or tap on a location or unit select
Secondary click, drag from the selection, or an explicit order control on touch target the location or unit with the currently armed order
Primary click on empty space clear the selection (UX-680)
Drag on the map away from the selection pan

There MUST NOT be a family of alternative click semantics. A single precision setting (UX-700) MAY make targeting require an explicit armed order rather than a secondary click, for players who mis-click; it MUST NOT change what any order means.

Rationale: an earlier draft carried four mutually exclusive click regimes inherited from another program, and named "check your interface mode" as the usual cause of players finding the game unresponsive. A single model that separates selection from ordering removes the whole category, and under Orders and the Cascade the cost of a mis-click is a wrong order rather than a wasted move — because everything is revisable until the phase closes (UX-285).

UX-660 Every order-authoring gesture MUST have a visible in-progress state naming what it is waiting for — "pick the second corner", "place waypoint 3 of 16", "choose the attack target" — and MUST be cancellable by Escape (UX-014.5), by the same control that started it, and by a visible cancel affordance on touch.

UX-670 Route preview. Before an order is submitted the client MUST show, live, the route the order would take: the projected path, its per-tile movement cost, the turn on which the unit would arrive, and the portion planned through unrevealed terrain visually distinguished as provisional (CM-1540), because that portion is planned optimistically (CM-140) and may not survive contact. The preview MUST come from the core's pathfinder, not from a client re-implementation (UX-040).

UX-680 Clearing the selection MUST be possible three ways: primary click on empty space, a control, and Escape (UX-014.5). Clearing the selection MUST NOT change any order.

UX-690 Navigating among own units MUST be possible by: selecting on the map; the Roster (UX-800); the Dispatch (UX-810); cycling by filter — by Posture, by Stance, by condition, by Formation, by Posting, by "changed since last turn" (UX-290); and cycling in the player's own activation order (UX-315). There MUST NOT be a "next unit awaiting orders" cycle, because under persistent orders (TM-280) no unit awaits orders.

UX-700 Precision setting: an option MUST exist under which a primary click on a friendly unit always changes the selection to it and never targets, for players for whom targeting-by-proximity misfires. This is a comfort setting and MUST NOT alter any order's meaning.

Built 2026-09-24 (client/src/settings/precision.ts). The pointer's primary click in this client only ever selects — targeting is UX-650's secondary click — so it meets the clause with or without the option. The gesture that does both is the keyboard's activate (UX-730), which targets while an order is armed; under the setting, activating a tile holding one of the player's own units selects it and drops the arm, and every other tile targets as before. Off by default, in Settings under Interface.

UX-710 Order authoring and revision. Every order the player authors MUST be previewed on the map before submission and MUST be submitted only by a deliberate action. A submitted order remains fully editable until the Orders phase closes (TM-360, UX-285), and the client MUST show, per unit, whether its current stack differs from what it carried at the start of the phase.

There is no post-close undo, and the client MUST NOT offer, imply or reserve one. The client requires no core retraction mechanism.

Partially held 2026-09-27: the preview half stands under UX-670, and the deliberate-action half has no pin of its own. “Previewed on the map before submission” is held where previews are tested — march-preview.test.ts pins the March’s route, per-tile cost and arrival turn “before the March is submitted (UX-670)”, which owns previewing as a row; this id is not named there. The deliberate-action half stands unheld: in this client the pointer’s primary click only ever selects — targeting is UX-650’s secondary click, and committing rides the keyboard’s activate (UX-730), each under its own rows — while selection.test.ts disclaims UX-710 rather than claiming it. Left untitled on that account.

UX-720 Multi-unit ordering. The player MUST be able to apply, in one action, to a multi-unit selection: a Posture with its geometry where the geometry is shared; a Stance; any individual Sanction; a whole Doctrine (CM-780); a Formation membership change; or a Task. The client MUST:

  1. show exactly which units in the selection the action will affect and which it cannot, with the reason for each exclusion;
  2. state the count before the action is taken;
  3. emit one order record per affected unit (TM-240 keeps records from different players commutative and records from one player last-write-wins);
  4. make the whole action undoable as one edit within the phase (UX-285).

Applying an edited Doctrine to units already created under it MUST be an explicit, counted action naming N (CM-800), never a silent retroactive change.

UX-730 Keyboard targeting. Directional single-step targeting across all eight adjacent cells, keyboard route entry by direction, map scrolling keys, and the accessible map cursor (UX-1320.4) MUST all be supported, and every geometry-authoring interaction of UX-735 MUST be completable from the keyboard alone. Eight directions are required rather than four because the map is a square grid with 8-way adjacency under Chebyshev distance (UX-380, GR-180, GR-170): a four-way mapping cannot express the move set.

Two constraints bind the shipped default for directional targeting:

  1. It MUST be pressable on a keyboard with no numeric keypad, and more generally MUST NOT require a key absent from the keyboards of a supported platform (UX-1225.6).
  2. It MUST keep its spatial arrangement across keyboard layouts. A directional cluster is a set of positions on the board, not a set of characters, so it MUST be bound by physical key position (KeyboardEvent.code in the browser) rather than by produced character. Every surface that displays the binding — the tooltip (UX-590), the Key Bind dialog (UX-1200), the palette (UX-610) — MUST show the characters those positions produce under the player's active layout, so the label never contradicts the key.

Where a numeric keypad is present its 3×3 block is a genuinely good fit for this interaction, and the client MUST ship that mapping as a named binding profile (UX-1200) selectable in one action.

Rationale: the outcome this requirement protects is that eight directions are reachable from the keyboard on every supported platform — which is what UX-1340's "operable without a pointer" guarantee rests on for map targeting. An earlier wording spelled that outcome as "numeric keypad by default" and carried no reason. The keypad's 3×3 block was the standard idiom for grid movement on the desktop PCs this genre grew up on, when every keyboard had one. Under UX-018 the browser is the primary platform and iOS and Android ship: laptops, tenkeyless boards and Apple wireless keyboards have no keypad at all, so a keypad default leaves this interaction unpressable on a large share of supported hardware. UX-1225's audit of shipped defaults could not catch that, because every clause of it was about keys the browser reserves rather than keys the host keyboard lacks — hence UX-1225.6. The keypad mapping is kept for the boards that have one; only its status as the default is withdrawn.

UX-735 Geometry authoring. The client MUST provide a direct-manipulation editor for each Posture parameter, with live legality feedback against the stated bounds:

Geometry Bounds Source
Screen line — an ordered list of tiles 2–64 tiles, need not be contiguous or passable CM-440
Survey region — a set of tiles 1–4,096 tiles CM-490
March route — ordered waypoints, plus loop mode and follow-on order 1–16 waypoints CM-530
Garrison target a city or work CM-330
Leash radius Chebyshev 0–64, drawn as a square (UX-380) CM-630
Formation facing one of the eight directions CM-930

While authoring a Screen line the client MUST show which line tiles are currently covered and by which friendly unit (CM-450), and the coverage a candidate position would add. While authoring a Survey region it MUST show the currently unrevealed share.

UX-738 Rejection presentation. An order record the core rejects (TM-250, CM-350, AR-320) MUST be presented at the point of authoring with its named, localised reason and a route to the fix. It MUST NOT be silently dropped, silently queued, or deferred to the next turn (TM-260). Where the rejection is a static illegality — a Task with no end condition (CM-250), a Stance the unit may not take (TM-750), a Screen line shorter than two tiles — the client SHOULD prevent it at authoring time and MUST still handle the rejection correctly if it occurs.

Route half built 2026-09-24 (client/src/orders/submit.ts: REJECTION_ROUTES, refused). The reason was already the client's sentence for every refusal it can cause; each refusal a control can fix now also names that control — the Stance, the Posture, Production, Postings, revoking the commit, or clearing a selection that no longer holds a unit of yours — and the refusal notice carries it as its one action. A refusal with no route (a closed phase, a timed-out seat, a vote already cast) is one the player cannot change now, and its sentence says when that changes.


9. Command at scale

This section implements the presentation obligations 13-command.md places on this document (CM-2200). Its subject is a player with three hundred units and thirteen decisions.

UX-740 The order stack. Every unit's complete order stack MUST be inspectable on the unit panel in plain language at all times, and editable at any level without disturbing the others (CM-320). The three levels MUST be presented as what they are (CM-200):

[2] Reflex    pushed by the engine — Withdraw, Replenish, Regroup, Return to Post, Take Cover
[1] Task      one instruction with an end condition
[0] Posture   the standing job

A Reflex MUST show its cause and its pop condition (CM-1400). A Task MUST show its end condition (CM-260) and its pressHome state (CM-280). A Posture MUST show its parameters and its four Sanctions. A unit with an empty Posture level is Idle and MUST be conspicuous everywhere it appears (CM-540).

The complete command inventory the client MUST expose, each as a control, a context-menu entry, a palette entry and a bindable key:

Group Commands Owner
Posture Hold, Garrison, Screen, Survey, March (loop mode Once / Shuttle / Circuit), Attach CM-330, CM-530
Sanctions Engagement (Never / Defend / Favourable(N) / Free), Leash 0–64, Risk (Expendable / Prudent(N) / Cautious), Seize (Never / Undefended / Free) CM-580–CM-710
Stance March, Advance, Hold, Entrench, and clear-to-Derived TM-630, TM-220
Task move to, attack, garrison, load, unload, join formation, execute a Posture once — each with an end condition CM-250, CM-260
Doctrine create, edit, apply to a selection or a Formation, set as a type default CM-770–CM-830
Formation and Group create, assign roles, set facing, Pace, cohesion radius, commander, remove member, disband CM-840–CM-1120
Posting create, anchor, weight, Requisition rows, surplus rule; set a city's Consignment CM-1130–CM-1330
Production enqueue a unit, industry or works order, reorder, remove, apply a Template US-1150–US-1210, CM-1310
Victory DesignateSeat, DesignateWorkSite, SelectCharter, CastVerdictVote, Concede, ClaimWarrant VC-320

UX-745 Stance control. The client MUST present the four Stances with their exact effects — the initiative modifier, the movement allowance change, whether the unit may initiate combat, its reaction capacity, and its defensive treatment in words — drawn from TM-640 rather than restated. Setting a Stance submits SetStance; it MUST persist until cleared and MUST NOT expire at end of turn (TM-720), and the panel MUST always show both the effective Stance and the derived Stance it overrode (TM-730). An illegal Stance MUST be refused with its reason (TM-750).

UX-747 Reactions. The unit panel MUST show reaction capacity, spent count, and reach (TM-1740). The reach of a visible enemy unit MUST be shown only where the player has observed that unit's class (TM-1740). The client MUST make plain the two facts a player most often gets wrong: that a unit in March or Advance has no reaction capacity at all (TM-1700), and that a mover halted by a reaction forfeits the rest of its movement and cannot bank it (TM-1640).

UX-750 Carriage. Loading and unloading along a stated route is automatic and requires no input (CM-535); the client MUST NOT present it as a decision. It MUST show a carrier's manifest, its remaining capacity, and each cargo unit's destination, and MUST let the player author an explicit load or unload Task where they want one (CM-250). Cargo contents of a foreign carrier MUST be shown only at the fidelity GR-1060 admits.

UX-760 Navigating a carrier's cargo MUST be possible from the panel and from the map: select the carrier, select any cargo unit, and step between siblings, without losing the carrier's selection context.

UX-770 A stack and cargo widget MUST show the host and child tree for a selected location or unit, allowing selection of any member and drag-to-move between hosts where the rules permit it. It MUST show the tile's stacking limit and current occupancy (GR-720, GR-780), because a limit of 1 in the open is load-bearing and invisible otherwise.

UX-780 Landing legibility. A unit unloaded during a Cascade is spent for that turn and will not activate again (TM-1840). The client MUST state this at the point the order is authored, not after the fact.

UX-790 The client MUST NOT invent automation. Every automated behaviour the interface depicts MUST be one the core actually performs (CM-100); the client MUST NOT add a convenience behaviour, a smart default that issues orders, or an inferred action of its own. A player who hand-orders every unit MUST get identical outcomes to one who automates identically (CM-050), and the interface must not quietly break that by helping.

UX-800 The Roster. The client MUST provide an empire-wide unit list meeting UX-045 in full: virtualised, filterable and groupable by class, Posture, Stance, condition, supply state, Formation, Posting, progress state and "changed this turn", sortable on every column, selectable in bulk and commandable from the selection (UX-720), and navigable and selectable entirely from the keyboard. Its default grouping MUST be Posting, then Formation, then Posture, which is the order in which a player thinks about an army; every other grouping MUST be one action away.

UX-810 The Dispatch. The client owns the presentation and the ranking; the core owns the events (CM-1610, CM-1620). The client MUST:

  1. present at most 7 items, player-settable in [3, 15] (CM-1640), with everything below the cut collapsed into a one-line digest carrying per-class counts;
  2. rank by the integer urgency U = S + T + A + P, ties broken by lowest affected-unit creation sequence number (CM-1660), where S, T and A come from the core and P is the client's adaptive per-class weight (CM-1770);
  3. give every presented item at least one one-action resolution and a snooze (CM-1650); an item with no available action MUST be a digest line, not an item; *(Partially met, 2026-09-12, and the gap is stated rather than implied. Found reading the panel for an unrelated item: the engine computes CM-1650's resolution list at eleven emission sites and the panel showed it at none of them — resolutions was consumed only by the presentability test, which decides whether an item appears at all. So CM-1760 priced an item's urgency by how much agency it carried, an item could rank 40 for agency, and a player had nothing on screen that explained the
    1. The three buttons that were there do not satisfy this clause: Done marks an item resolved without doing anything and Show me navigates, so neither of them is a resolution the core named.* What landed: every resolution renders as its own control, labelled with the action, whose accessible name carries the action and the item — because "Commission a replacement" alone is four identical buttons on a bad turn — and which takes the player to the cause. What did not: a control that issues the order its label names. Measured over every literal the engine emits — 'Commission a replacement', 'Raise the Industry', 'Remove the order', 'Replace the Depot', 'Review the queue', 'Flag for priority levy', 'Scrap the hull', 'Review production' — none is an OrderInput kind: they are sentences written for a person, which is right for a label and means the mapping is a design step rather than a lookup. So this is one click to the place the action lives, not one click to the action, and the remaining half is on the work list rather than described here as done.)
  4. keep items sticky across turns until resolved, snoozed, or their condition ceases (CM-1670), and show a retired item once in the digest naming what happened (CM-1680);
  5. snooze by cause key for a player-set number of turns, default 5, range [1, 20], re-raising immediately if the item's Stake rises by 30 or more (CM-1690);
  6. adapt P per CM-1780, default on, disableable with one setting, with every current P visible in a settings panel and a per-class and global reset (CM-1800);
  7. never demote a non-adaptable class (CM-1790) — which includes CityThreatened, the economy's RevoltImminent (EC-1910), and victory's VictoryAlarm and VerdictOffered (VC-3150);
  8. pin every item to a map location, move the camera on hover, and restore the camera on resolve (CM-1890);
  9. rank and present within 16 ms for up to 200 concurrent live events (CM-2130);
  10. emit no network traffic whose timing or volume varies with Dispatch content (CM-1860).

The Dispatch MUST be the client's only by-exception surface. No subsystem may add a competing notification feed (EC-1900); toasts, banners and badges MUST all be renderings of Dispatch items or of phase transitions, and MUST be announced through the live regions of UX-1320 at the politeness their severity implies, coalesced so that a large Cascade produces a summary rather than a flood.

Rationale: the whole value of this design is that a player with three hundred units reads five items. A second feed alongside it does not add information, it subtracts the guarantee.

UX-820 Screen and Survey authoring MUST show coverage as the thing being decided: for a Screen, which line tiles are covered now and by which unit, and what a candidate position would add (CM-450, CM-460); for a Survey, the unrevealed share of the region and the reveal a candidate position would produce. An under-strength Screen MUST be visible as such on the map and on the Formation panel (CM-1050, CM-1060), together with the Requisition entry it raised.

UX-830 March routes and Ferry: waypoint editing (UX-735), loop mode, and the follow-on order MUST all be editable in one panel, and the cached route MUST be displayed including its provisional portion (CM-1540, UX-670). A route that has been suspended by a Blocked interruption MUST be shown as suspended, not cancelled (CM-1390), with the obstruction named.

UX-840 Multi-select MUST support drag-box selection with live highlight, additive and subtractive modifier clicks, and selection from the Roster and from any map overlay. Selection MUST be representable in the accessible map view (UX-1320.4).

Built 2026-09-24 (client/src/shell/multi-select.ts, the map scene, the shell). The primary selection stays what every panel acts on (UX-650's one model); the set is picked beside it. A plain click replaces the set, Shift+click adds, Ctrl/Cmd+click removes; Shift-drag draws a box that rings the own units it would pick while it is drawn and adds them on release, and never pans. The Roster's Select these on the map hands its ticked rows over, and every overlay selects through the same map click. The set is shown in words and as controls in a selected-units strip above the unit panel, which is how it is representable without the canvas. UX-850's control groups assign the set and recall the whole group. Orders still go to the primary unit; issuing one order to the whole set is not built.

UX-850 Persistent control groups MUST exist (Ctrl+1–9 assign, 1–9 recall), MUST survive save and load as client state, and MUST NOT be confused with Formations, which are game state and confer different behaviour. The client MUST make the distinction plain the first time a player creates one.

UX-860 Formations and Groups panel. The client MUST show and allow editing of: membership against the span of 8 (CM-850), with a rejected ninth member offered a split or a promotion to a Group; role assignment against the slot caps of CM-900; the facing (CM-930); Pace and cohesion radius (CM-1000–CM-1010); the commander and the succession rule (CM-940); and Group composition up to five Formations across exactly two echelons (CM-860).

It MUST state at all times, in words and with the reason, whether the Formation is Responsive or One turn behind (CM-990), and for a Group member whether its order is up to two commander activations stale (CM-1080). It MUST show role overrides on the affected units' panels — in particular the Van's forced Defend and Cautious (CM-910).

Disbanding a Formation, or removing a unit from one, MUST show what Posture each affected unit will be left holding before the action is taken (CM-1090, CM-1100); it MUST never leave a unit Idle.

UX-870 Map annotation: players MUST be able to place free-text labels on the map, managed from a labels panel; a label MAY be designated a waypoint and bound to a key that centres the map on it (UX-1210). Labels are private to the authoring player unless 05-multiplayer.md specifies shared annotations.

Implementation status (2026-09-24). Built, the key included. The labels panel, in the strip under the map, places a free-text label on the selected tile (or the view's centre), renames it through a native text input (so an IME works, UX-890), flags it as a waypoint, jumps to it without taking focus (UX-490), and removes it. Labels are drawn on the map as UX-016-anchored text, dismissed when their tile scrolls off. They are private in the simplest way: kept in this browser, per game, and sent nowhere. 05-multiplayer.md specifies no shared annotations. The key (UX-1210): Shift+1 to Shift+9 centre the map on the first to ninth waypoint, in the panel's order. They are listed in the Key Bind dialog and the palette like every command, and rebindable. A reading, recorded: UX-1220's table names no default for these, and the catalogue ships one for every command, so Shift with the digit was chosen to sit beside the control groups' plain and Ctrl digits.

UX-880 Endurance. Air endurance MUST be shown as turns remaining on every air unit at all times, and the client MUST warn before committing an order that would leave a unit unable to reach any base (US-2040). The range and return envelope MUST be available as an overlay (UX-460), computed from enduranceRemaining and the basing sites the player can see (GR-770).

UX-890 Naming: units MUST display their names, and cities, Formations, Groups, Doctrines and Postings MUST support inline renaming. Inline renaming MUST use a native text input with full IME support (UX-045) — a player naming a city in Japanese, Chinese, or Korean gets their system's candidate window, not a keystroke-swallowing custom field.

Partially held 2026-09-27: the IME-input pattern is pinned under this id; the renaming it would serve waits on core, and the pin’s own file disclaims the row — a recorded self-collision left in place. map-labels.test.ts holds that the label panel renames through a native text input (“so an IME’s candidate window is the system’s (UX-890)”) — this clause’s mechanism, on one surface — while its own header disclaims UX-890, saying what it does not claim. That collision is recorded rather than fixed: it says exactly where the row’s evidence stands and where it refuses to stand. The rest of the requirement — units displaying their names; cities, Formations, Groups, Doctrines and Postings supporting inline renaming — sits in SESSION.md’s blocked-on-core list: “UX-890’s renaming, which needs rename orders and unit names”. Nothing is built there yet to pin.

UX-895 The audit trail. For any automated action the client MUST be able to retrieve and render the core's CommandDecisionRecord (CM-1940) for at least the 20 most recent turns (CM-1950), rendered as one sentence naming the clause and the numbers — "Attacked at 743‰ predicted; sanctioned at ≥700‰" (CM-600, CM-1960) — with the runner-up option and its score available. Where automation declined to act, the same surface MUST state the reason in numbers (CM-2000).

Rationale: this is the feature that converts a bad automated outcome from "the automation is broken, I will never use it again" into "the threshold was too low, I will raise it". It is also the best debugging tool the team will have, and it costs the client a table and a sentence template.

Implementation status (2026-09-24). The unit panel carries Why it did that: the selected unit's CommandDecisionRecords, newest first, over the core's twenty-turn window (trimDecisionRecords). Each is the core's clause as the sentence, with the prediction and the threshold added where the clause did not already carry them, and the runner-up tile and its score. A decline under CM-2000 reads as the core wrote it ("stopped because …; predicted …‰"). Only the seat's own units: another seat's records would be a view through the fog. The whole-game trail a replay keeps (CM-1950's second clause) is the replay's, not this panel's.

UX-898 Progressive disclosure. On first exposure the client MUST present Sanctions as the three named presets only — Careful, Balanced, Aggressive — and MUST expose the four individual dials at a tier the player can also enable immediately (CM-1980). Formations, Groups, Postings and Requisitions MUST each be independently disclosable, and the game MUST be fully playable with none of them enabled (CM-1990). Every command surface in this section MUST remain fully functional for a player who automates nothing (CM-070).


10. Industry, production, and the economy

12-economy.md makes visibility a requirement rather than a courtesy: a brake that satisfies its Visibility Test on paper and not in the client has failed the direction document's test just as completely as an invisible multiplier would (EC-020, EC-1790). This section is where that debt is paid.

UX-900 The city panel MUST open automatically when a city is captured and on demand for any owned city, and MUST be presented as a non-modal dockable panel rather than a modal that blocks the map. A captured city requires no player input to become productive (US-1030, EC-680) and the panel MUST NOT imply otherwise.

UX-910 The panel's identity area MUST show: location, name (editable inline), city class, traits, Industry level, Integration, Unrest, Loyalty, supply state, and any Seat, Landmark, Unsettled or Bellwether status.

UX-920 The production list MUST show one selectable line per unit definition with: name, class, role sentence, Works price and Manpower price as the ordered pair the economy defines, the turns to completion from the current Yard, and — for any definition not currently buildable — which clause of US-360 fails, in one sentence (US-370). Naval definitions in a non-coastal city MUST be listed and explained rather than hidden.

Implementation status (2026-09-24). Met. Every unit definition has a line in the city panel's build list, buildable or refused: name, class and role sentence, the Works and Manpower pair, and the turns to completion. That count is the price less the current Yard over the city's Effective Works (EC-640), for this order funded next. A refused line carries the engine's sentence (US-370), so a naval class in an inland city is listed with the coast it lacks.

UX-930 The queue MUST be presented as what it is: an ordered list of at most 8 orders (US-1150) of three kinds — unit, industry, works (US-1160). The client MUST show that removal is free and alters nothing (US-1200); that at most one industry order may be queued (US-1180); that at most one order completes per turn regardless of Yard size (US-1290); and that an order which has begun funding is not automatically retargeted (US-1210, CM-1270). A Stalled order MUST be shown in place with its reason and MUST NOT look cancelled (US-1040, US-1060).

UX-940 The Yard MUST be shown as a single integer with its cap and what the cap is derived from (US-1220, US-1240). On any turn a city discards Works because the Yard is capped, the client MUST say so plainly, once, with a route to fix it. A silent discard is exactly the hidden tax the economy forbids.

UX-950 The Industry ladder panel. For any owned city the client MUST show all four levels with, per level: the name, the Works multiplier, the upgrade cost, the Works gain for that specific city, the payback in turns, and the complete set of unit definitions the level unlocks — all inspectable before any Works are spent (US-940, US-820, US-890). Where the player's Industry cap binds (US-900) the panel MUST say so and MUST state that a captured city above the cap keeps its Works multiplier and loses only the buildable set (US-920).

UX-960 Unlock cards and contact briefings. On the turn a city's Industry upgrade completes the client MUST present one unlock card per newly available unit, each carrying the unit's name, role sentence, full stat line, the two units it counters and the two that counter it (US-950). The first time a player observes an enemy unit of a class they have never produced the client MUST present that unit's card unprompted as a contact briefing (US-960). Both MUST be dismissible and both MUST be re-openable from the encyclopedia (UX-1290).

UX-970 Governance: for every city the client MUST state, in one word, whether a Template, a Requisition, or a manual queue is currently governing its production (CM-1330), with one action to change it, and MUST show the city's Consignment — a Posting, Auto, or Manual (CM-1160) — and its surplus rule (CM-1220).

UX-980 A centre and close action MUST close the panel, centre the city on the map, and save pending edits. Any active Dispatch item on the city MUST remain active.

UX-990 A minimise action MUST hide the panel to expose the map and MUST restore it from a persistent affordance or by selecting the city. Because the panel is non-modal (UX-900), minimising MUST NOT lock selection.

UX-1000 Closing a city panel MUST return focus where it came from and MUST NOT clear the map selection (UX-1325).

UX-1010 Reach. The client MUST render the three supply states across the whole map as an overlay (EC-1880), MUST show each unit's grade on its panel and its map badge (UX-430), and MUST show each source's reach as the cost-limited region it is — the fill's own shape (12-economy.md EC-1680), never a Chebyshev box drawn around the source. The consequences of each grade — the movement and combat effects, and attrition where isolated — MUST be stated on the unit panel, not left to the encyclopedia.

Ruling, 2026-09-10. A Reach is not a radius, so UX-380's rule never applied to it. This clause read "as the Chebyshev region it is (UX-380)" and was recorded as a defect in the requirement on 2026-09-10 while the overlay was built. A supply Reach is a cost budget, not a distance: EC-1675 converts the Reach in tiles to a budget by multiplying by the foot cost of Plains, and EC-1680 spends it as a Dijkstra expansion over the terrain table with EC-1700's water discounts, so a source's Supplied region is a Chebyshev square only on uniform open ground. Through rough ground it is an irregular shape, and that shape is the whole mechanic: economy-supply.test.ts has pinned it green all along — the same Town reaches 8 tiles over plains and 2 tiles into marsh — under a comment naming it "the reason the fill stays cost-based rather than becoming a Chebyshev radius". Nothing here weakens UX-380, which is correct and stays: every real radius the client draws — vision, Leash, reaction reach — is a square, because those are Chebyshev distances. UX-380's own reason is what condemns the old phrase: drawing a circle for a Chebyshev radius "misstates the rule at exactly the corners where a player is deciding", and drawing a square for a cost-limited fill misstates it in exactly the same way, telling a player that marsh costs what plains costs at the moment they are deciding whether to cross it. The direction was never open: making the fill a Chebyshev radius was rejected outright when EC-1600 was ruled, because it deletes EC-1580's canonical foot class and EC-1700's water discounts and terrain stops mattering to supply, which is the mechanic's only purpose. No client code changes — map/supply-overlay.ts runs the core's own supplyNetwork and draws its result, which is what this clause now asks for. The word "grade" in the second clause reads as supplyState per 12-economy.md's binding naming note, which names this document among those citing the concept under its former name.

UX-1012 Depot siting MUST show the projected network before the order is committed: the supply overlay as it would be with a Depot on the chosen tile, or with a chosen city crossing Integration 50 (EC-1880). Placing a Depot without seeing what it would connect is exactly the guess this overlay exists to remove.

UX-1015 The garrison burden MUST be shown on the city panel as points required against points present, with each contributing unit and its rating, the Integration at which the requirement next drops, and the requirement that will apply as Integration rises (EC-1830). Where the requirement is unmet, the item MUST appear in the Dispatch with its one-action resolution — raise the Requisition target (EC-1920).

UX-1020 The Administrative Cadre. The client MUST show the player's Cadre count, each Cadre's assignment, and the price of the next three Cadres (EC-030, EC-1870), and MUST present the Digestion Forecast (UX-1080) as the answer to what buying one more would do.

UX-1030 Air basing: a city bases up to 4 air units and an Airfield up to 3 (GR-770), and units with the airbase capability base to their own capacity. The client MUST show basing occupancy against capacity wherever an air unit could land, and MUST fold endurance state (UX-880) into the same readout, so "can I get home from there" is answered in one place.

UX-1040 Construction: with a construction-capable unit selected, the client MUST list the installations it can build on its current tile with the exact build time for that terrain, and MUST support authoring an ordered construction path of build and remove locations, rendered as an overlay. Removing an installation that a March route or a supply link crosses MUST require confirmation naming what is affected. no-test-needed: it binds a construction UI the engine cannot yet show — the core has no construction orders, as SESSION.md’s blocked-on-core list records (“UX-1040’s construction, which has no construction orders”) — so there is nothing to list, author or confirm against in the current build. When the construction order lands and the client gets its panel, this row stands for tests; until then it is unexercisable rather than untested.

UX-1045 Works strikes. Where a unit may strike a city's industry rather than attack a unit (US-1090), the client MUST show, before the order is committed: the works defence terms and their integer contributions (US-1100), the effect table for 0, 1, 2 and 3+ hits (US-1110), and whether the target has already lost an Industry level within the five-turn floor (US-1120). It MUST also state plainly that a strike may be ordered against a city the player cannot currently see and will resolve against the true state (US-1140).

UX-1050 Production Overview. The client MUST provide an empire-wide, sortable, filterable table of every owned city and producer showing at least: name, location, class, traits, Industry level, Yard against its cap, head order and its ETA, Stalled state, governing source (UX-970), Consignment, Integration, Unrest, garrison state, supply state, and live Dispatch items. It MUST support multi-select bulk assignment, emitting one order per affected location exactly as the per-city panel would (UX-020, UX-720), and it MUST meet UX-045 in full. It complements and MUST NOT replace the per-city panel.

UX-1060 The Cost of Conquest. For any enemy or neutral city in the player's knowledge view the client MUST present the panel 12-economy.md specifies, with every line it names: immediate yield, starting Unrest, garrison required and the unit count that satisfies it, turns to digest, Cadre cost, Levy earned and when the first point arrives, and War Damage after the capture (EC-1850). Every line MUST be computed from the player's knowledge view only and every line derived from a stale observation MUST be marked with its observation age (EC-1860, GR-1170).

Rationale: 12-economy.md calls this the single most important piece of interface in that document, because it is where "visible and plannable" stops being an adjective and becomes a screen. It is the anti-snowball design presented as an offer rather than administered as a penalty, and a player who takes the city anyway has taken a mortgage with the terms in front of them.

UX-1070 The Empire Ledger. The client MUST provide the screen EC-1870 specifies: Works accrued this phase with a per-city breakdown sortable on every column; the Manpower pool, cap, levy rate and the Manpower committed to hulls awaiting men; the per-city Levy breakdown with zero-yield cities listed explicitly; the Cadre count, assignments, and the prices of the next three; every city's Integration and Unrest with its delta; and total garrison points required and present across the empire.

UX-1080 The Digestion Forecast. The client MUST present, for the whole empire, the turn at which every held city reaches Integration 100 under the current Cadre assignment, and the marginal effect of one more Cadre — which city it would take, how many turns it would save, and what it would cost (EC-1890).

UX-1090 The brake audit. For each of the six named brakes of EC-030 — Integration, the Cadre price, the garrison burden, Reach, War Damage, and Manpower not transferring — the client MUST satisfy all four clauses of the Visibility Test at the place the brake applies (EC-020): the brake named in one sentence without the words penalty, scaling or balance; every integer term it consumes displayed before it applies; a forecast of when it will bind and what it will cost; and the counterplay named as an action available this turn.

Additionally, and bindingly: no economic quantity may be rendered as a bar without its exact integer (EC-050), and the client MUST be able to answer "why is this number what it is" for Effective Works, Levy, Integration delta, Unrest delta, garrison requirement and supply state by naming every contributing term and its value (EC-040, EC-1790 through EC-1840).

UX-1100 Manpower and Awaiting Levy: the client MUST show the pool against its cap, the levy rate and its per-city composition, every hull awaiting men and the Manpower each owes (EC-1840), the Emergency Levy with its cooldown, and any Manpower discarded by the cap on the turn it is discarded.

UX-1105 Economic events reach the player through the Dispatch and nowhere else (EC-1900, UX-810). The client MUST NOT build a second by-exception economic surface, and MUST honour the one-action resolutions EC-1920 requires for GarrisonShort, CadreUnassigned, AwaitingLevy and SupplyCut.

Built 2026-09-24 (client/src/dispatch/resolve.ts). Until then CadreUnassigned's assign to the auto-selected city fell to the resolution map's default and showed the Cadre, leaving the player to author the order by hand; it now issues EC-1390's assignment order to the city the label names, and refuses in a sentence if that city is no longer the player's. The Requisition target, the priority levy and the scrap open the Production screen, where the choice is; the broken link is a look, and shows the cut unit. packages/client/test/dispatch-economy.test.ts reads the classes and their resolutions out of EC-1920 rather than copying them, and scans the client's code for the four class names outside the Dispatch and the AI — the "nowhere else" clause.


11. Combat: the preview and the record

UX-1110 The attack preview MUST present its content in exactly the order 11-combat.md CB-2390 fixes:

  1. The verdict, in plain language, first — "They break — certain."
  2. Both loss bands as min ── median ── max, with the resulting strength values.
  3. Odds and reliability as two separate figures.
  4. Every contributing line item, with its source named.
  5. The consequence — where the defender would retreat to, whether they are cut off, whether the attacker takes the tile.
  6. The alternative — a one-click preview of the same attack without each supporter.

Because support and flanking are automatic, the panel MUST enumerate every unit it counted and every modifier it applied (CB-2410). It MUST name the retreat destination and MUST state explicitly when the defender has none (CB-2420) — which is the difference between a fight and a rout.

UX-1112 A single expected value MUST NOT be shown in place of a band, and the falter probability MUST always be shown even when it is 0‰ (CB-2400).

Rationale: an expected value is the specific lie that makes players distrust odds displays, and a risk that appears only when non-zero is a risk players never learn exists until it happens to them.

UX-1115 Prediction under fog. The preview MUST show the target's observation level — L1 Sighted, L2 Adjacent, L3 Identified (CB-2310) — and what each level does and does not tell the player. Where an input is unobserved the preview MUST show the resulting range of each output and name every unobserved field (CB-2330). It MUST display at most two pivots, ordered by the span each induces, largest first (CB-2340, CB-2350), each phrased as the threshold and the fix — "they break unless they are Veteran or better; grade unobserved; a Recon unit adjacent would tell you". The panel MUST make explicit that the attacker's own losses are well known even when the outcome is not (CB-2370).

UX-1118 The client MUST NOT compute any combat number itself; every figure MUST come from a core export (CB-2470), and every per-mille figure MUST be shown in per-mille or as a percentage derived from it (CB-2480).

UX-1120 The resolution trace. The core emits a combat event carrying the Clash Specification, the draw, every intermediate quantity and the resulting state of every participant (CB-2450, CB-2460). The client MUST expose that trace in full in solo and hotseat games, and MUST NOT expose it in a game with two or more human players.

Rationale, stated because this is a design decision rather than an inherited rule: the trace names fields — the defender's exact grade, disorder and dig-in — that the viewer's fidelity grade may withhold (GR-1260), so publishing it to an opponent would reveal through the combat log what observation was supposed to cost. In a solo or hotseat game there is no opponent to leak to, and the trace is the best teaching object the combat model has.

UX-1125 The condition badge. strength and disorder MUST be rendered as a single composite badge, always available together and always meaning the same thing across every unit in the game (CB-190, CB-110). Exposing them as two raw numeric columns is a failure of this requirement. The badge MUST be legible at the Close and Mid tiers (UX-390) and MUST have a non-colour encoding (UX-1310).

UX-1130 The hostile-tile overlay MUST colour every enemy-occupied tile adjacent to the player's units by the break probability of the best available attack, computed by the same function as the preview (CB-2430).

UX-1135 Encirclement MUST be visible before it is fatal: the client MUST mark any unit with no retreat candidate, for both owners, whenever the marked unit is visible to the viewing player (CB-1410).

UX-1140 After every resolution the client MUST show a reconciliation line stating the prediction and the actual result — "predicted 26–41, actual 35" (CB-2440) — in playback, in the after-action report (UX-355), and in the combat history.

Rationale: this builds trust in the readout faster than any tutorial, because it is evidence rather than a claim, and it makes the invariant that the outcome always lies within the displayed bands visible to the person it protects.

UX-1145 History and graphs. The client MUST provide per-turn charts covering at least: Standing and Tenure (VC-2390), city count, unit count, Works income, Manpower pool and levy rate, and combat losses. Charts MUST be DOM/SVG rather than canvas, MUST be rendered from core-recorded history so that a replay reproduces them, and every chart MUST offer an equivalent data-table view of the same series (UX-045) — which also satisfies UX-1320 without a parallel implementation. Series MUST follow the colourblind-safe palette rules of UX-1310 and MUST be distinguishable by line style as well as hue.

UX-1148 The combat record MUST be filterable by turn, by location, by participant and by outcome, MUST meet UX-045, and MUST jump to a location without stealing focus.


12. Victory: the Watch, the Board, and leaving the game

14-victory.md makes legibility a requirement of the rules rather than a courtesy of the interface (VC-030), and delegates the pixels to this document (VC-3170). It also imposes a discipline this section must not break: the client MUST NOT compute any victory value, and MUST NOT present one at a precision finer than the band published for it.

UX-1150 The Watch. The client MUST present a compact, always-visible, never dismissible strip carrying exactly one row per player (VC-2340). A row MUST carry exactly the six fields of VC-2350 and no more:

# Field
1 Identity — colour, name, and live / capitulated / Reduced / Out of Contention status
2 Nearest Track — the Track that player is nearest completing, by highest ratio
3 Bar — drawn from the published value at that player's band
4 Value — a Rumour word, a quantised Report figure, or an exact Alarm figure
5 Vigil — VIGIL n/V where one is running; blank otherwise
6 Tenure — the value and a rising / falling / flat indicator against the previous Seal

Tenure MUST be shown at full precision to every player at all times and MUST NOT be banded (VC-2360). The Watch MUST carry a one-line footer stating the current Ebb value with its next scheduled change, and the current turn against the Horizon (VC-2370), and MUST answer without any click: who is closest to what, by how much, whether they are rising or falling, and how long the game has left (VC-2380).

UX-1155 Precision discipline. The client MUST render each opponent value at exactly its published band and no finer (VC-2160, VC-3170): Rumour as one of the five words of VC-2210 with a stub bar and no number; Report as the quantised figure, with the report quantum itself shown so the player can see the precision of what they are reading (VC-2260); Alarm at full precision. The client MUST NOT infer, interpolate, animate between, or trend-fit banded values, because a smooth line through two quantised readings reconstructs precision the band exists to remove. Band hysteresis (VC-2220) MUST be respected in presentation as well as in value: a row MUST NOT flicker.

UX-1160 The Board. The full victory panel MUST be reachable from a single keypress (VC-2390) and MUST contain at least: the full matrix of players against Tracks with published value, per-Seal delta and Vigil state; the Ebb schedule drawn as a descending line; each player's count-backwards markers; the Warrant board; the Ascendancy Tally calendar; each player's Charter and Sealed Order counts; Standing and Tenure history; and the Chronicle. It MUST be redrawable for any past turn from the retained Seal records without recomputation (VC-2400), and MUST mark every Track proved DECIDED and every player Out of Contention, naming the turn each was established (VC-2410).

Partially held 2026-09-27: the Board’s clauses stand under their VC names in board-complete.test.ts; the id stays untitled while its own note below records what remains open. The Ebb schedule over the whole game (VC-830), each Out-of-Contention mark with the turn it was established, and the DECIDED half as “the projection’s mark with its run’s first Seal” all stand there under 14-victory.md’s own rows — board-complete.test.ts describes them one by one. The implementation note below this requirement records the piece that does not yet: core stores no establishing turn for the Horizon’s mark, pending the latch ruling in core/src/victory/elimination.ts.

Nine of nine sections since 2026-09-24 (client/src/victory/board.ts): the Ebb schedule is drawn as a line from the Terms' own threshold(t), said in cities for a share of the map by the Terms paragraph's conversion (so the Board and the Terms give the same numbers), with the turn shown marked. VC-2410's Out of Contention half is drawn too, read off the Seal records up to the turn shown with the turn each mark was established; a revoked mark (VC-2740) leaves none. Its DECIDED half is not: core recomputes the Horizon's mark where it is read and stores no establishing turn, pending the latch ruling flagged in core/src/victory/elimination.ts, and deriving one here would be the client computing a victory value (VC-3170). So this row stands open on that half alone.

UX-1165 The Alarm. When a player enters the Alarm band the client MUST present the banner 14-victory.md requires, naming the player, the Track, and the number of turns to completion, and MUST publish the Bellwether sites as named, locatable map markers with the live vision granted for the duration and the two-turn grace (VC-2280, VC-2290, VC-2310). Bellwether vision MUST render exactly current occupancy of the named sites and nothing else — no unexplored terrain, no other tile, no past state (VC-2300).

The alarmed player MUST be told, at full precision, exactly what has been published about them and which of their sites were revealed (VC-2230). An Alarm that discloses three of a player's cities without telling them which three converts a fair fight into an ambush.

The Alarm MUST additionally appear as a non-adaptable Dispatch item on every other player's Dispatch (VC-3150, UX-810.7) and MUST remain in force under every Progress Visibility setting, including Own Only (VC-2190).

UX-1170 The Chronicle. The client MUST present the permanent, one-line-per-event victory log, rendering each entry's turn number, class and plain-language text (VC-2420, VC-2430). Visibility MUST follow VC-2440, and the complete Chronicle MUST be presented to every player without redaction at the end of the game (VC-2450). The rendering MUST be terse enough that the endgame is followable from the Chronicle alone; VC-2460's worked form is the normative target for the classes it exercises.

Implementation status (2026-09-24). The Board draws the Chronicle during play, in VC-2460's line form through the core's own chronicleEntries and renderChronicleLines, and the end screen publishes the complete one (VC-2450). A reading, recorded: VC-2440 makes a class visible to a player "only where the event it describes was published to that player under §13.2", and a Chronicle entry does not record whom it was published to. So during play the Board shows exactly the classes VC-2440 names as visible to everyone, parsed from its sentence by the test. That is a strict subset, which can hide a line a player was entitled to but never shows one they were not. Showing the rest per player needs the entry to carry its publication.

UX-1175 A player MUST always see their own Track values at full precision, unbanded, including their own Vigil counters and their own sites (VC-2170) — on the Watch, on the Board, and on the map.

UX-1180 Reduced status and the Rally. Where a player is Reduced the status MUST be shown publicly on their Watch row, naming the leader it is measured against (VC-2920). The one-time Survey MUST be presented as what it is — current ownership of every city, and nothing else: no units, no strengths, no terrain reveal, no ongoing vision (VC-2970) — and the client MUST say so, so that the player does not plan an attack on a map that shows no defenders because there is no data rather than because there are none. Rally Warrants MUST be public, including target and holder, from the moment they are issued (VC-2960).

Implementation status (2026-09-24). Met. The Watch row carries Reduced with the leader it is measured against. The Board lists every open Rally Warrant by target, holder and deadline, and each Survey's frozen ownership, under a sentence saying what a Survey is not: no units, strengths, terrain or vision after its turn, so a city with nothing on it has no data rather than no defenders.

UX-1185 The Verdict. Where a Verdict is offered the client MUST raise it as a non-adaptable Dispatch item on every eligible voter's Dispatch, persisting until the window closes (VC-2780, VC-3150), stating the window in turns and stating plainly that abstention counts as "play on" (VC-2770). Casting a vote submits CastVerdictVote (VC-320).

Built 2026-09-24 (client/src/dispatch/verdict.ts, victory/VerdictPanel.tsx, castVerdictVote in orders/submit.ts; held by client/test/verdict-dispatch.test.ts). Two gaps, both confirmed before the slice was claimed. A human player could not vote at all: the Dispatch keeps both votes off its one-click list — a vote can end the game for every player, so it belongs where its consequences are written — and there was no such place; CastVerdictVote existed in the client only for the AI and the wire. And the item lasted one turn of a window that may run several, because the Dispatch is built from the turn's Attention Events and the core raises the Verdict's event once, at the offer.

The item is now built from the offer itself, which is in the player's view on every turn it stands (the view's own docblock said the Dispatch line was to be built from it, and only the AI read it). On the offer turn the core's event is replaced rather than joined, so the item never appears twice, and once the window closes no stale event can bring it back. The sentence gives the window as the turns it has left, from the view's own countdown, and says in so many words that not voting counts as a vote to play on — checked against the resolution, which records every silent voter as "continue". Both votes open one panel at the same cost; its two buttons are the same control in every respect but their words, with no default, no emphasis and no focus, because a panel that leans has an opinion about the outcome. The vote is journalled, so UX-1190's listener has it on disk before the call returns — and a vote cannot be changed, which makes it the worst order to lose to a closed tab.

A disagreement inside the core, recorded for the owner and filed on the victory lane. The electorate the core offers the Verdict to and counts at resolution excludes every member of the leader (VC-2750 over VC-1570's team subjects). The vote order's door refuses only the leader's own id, and a team leader's id is negative, so it matches no player: the door accepts a vote from a member of the leading team, and the resolution then silently discards it. Not an exploit — the vote never counts — but a false confirmation, and an applied order with no effect. The client follows the electorate the resolution counts, and is held against the engine's door seat by seat wherever the two are defined alike (player leaders).

A limit, stated rather than left to be found: an open Verdict needs one player holding twice second place's Standing for twelve Seals, which no short session in the running client reaches, so the offer path is verified by the rendered tests, the accessibility pass and the mutation pass rather than by a browser. The running client was checked for regressions only.

UX-1190 Exit paths. Save-and-exit MUST be the normal egress. A force quit at any moment MUST never lose submitted orders or corrupt state; the client MUST persist submitted orders at submission time rather than at unload time, because closing a tab, navigating away, and being killed by a mobile WebView under memory pressure are all force quits that arrive with no guaranteed shutdown hook. Unload handlers are a courtesy, never the mechanism. A confirm-on-leave prompt MUST be raised only when unsubmitted staging would be lost, and MUST NOT be used routinely — browsers show generic wording the client cannot control.

Built 2026-09-23 (client/src/save/resume.ts, the journal listener in orders/submit.ts, the shell's adoption and guard; held by client/test/exit-paths.test.ts). The mechanism is one synchronous listener on the order journal, called from every site that changes it — a player's order, the close's orders, the worker's close, undo, redo, a new or loaded game — and it writes the game as it stands to a reserved key before the order is routed anywhere else. No shutdown hook is part of it: the central test gives an order and then reads the store cold, with no unload, no save and no cleanup having run, and replays what is there into the same game. A structural row requires the listener call beside every journal mutation, so a sixth mutation site cannot be added silently.

The resume record is a save's record — composed by the same function a save uses and restored by the same replay — and it is kept under its own key rather than in the saves' store, because every save lives in one object and writing there would re-serialise every save on every order. At start, an unclean record is filed as an ordinary save first and the player told second: an offer held in memory is a record the new session's first order would overwrite, so a second force quit before the player answered would have lost the first one's orders. Filed first, there is no window at all. Saving marks the record clean, so the normal egress — save and exit — leaves nothing to recover.

Two defects the running client found and the suite could not have. The recovered game's Open it was refused by the map-ceiling gate, because the offer appears at start and UX-165.4's measurement is lazy — only the wizard and the Load list start it — so the one-click recovery failed on the very page load that offered it, having already dismissed its own banner. The offer now starts the measurement itself, and the button waits with its reason. Both are pinned.

A reading recorded for the spec owner. "Unsubmitted staging" is read as work the player composed and has not submitted — a geometry draft with tiles in it (UX-735), a Posture and its Sanctions being edited (UX-1295), an order held for UX-880's stranding confirmation. Arming an order is not staging: it is a mode, one click to enter and one to re-enter, and a prompt for it would be the routine use this row forbids. The guard is registered only while staging exists, writes nothing, and is the only unload-family listener in the client.

UX-1195 Leaving the game. Concession MUST require an explicit confirmation and MUST be presented as irreversible (VC-2840), stating its consequences — the disposition of the player's cities, the frozen Tenure, and the final placing — before it is taken. A capitulated, conceded or eliminated player MUST retain spectator access to their own past Seal records and to the public Chronicle (VC-3090). Where a seat is substituted by AI on departure the client MUST say so plainly and MUST show that Tenure continues to accrue (VC-3060, VC-3070).

A reading recorded for the spec owner, 2026-09-24. "The frozen Tenure" cannot be stated before the concession is taken. A concession settles at S13 of this turn's Seal (runCapitulation), and the same Seal's Tenure accrual runs earlier — so the record freezes at today's figure plus this Seal's share, and that share depends on the Standing the Cascade has not yet produced. The first confirmation stated today's figure as the frozen one: conceding a live Standard game at turn 25, it said 72 and the Watch read 75 from that Reckoning on. The confirmation now gives today's figure labelled as today's and says in words that this turn's Seal still adds its share (victory/concede.ts's tenureText, held by client/test/concede.test.ts on a scene where the Seal really does add). The same slice stopped the idle line (UX-630) inviting a conceded seat to "commit the turn as it stands": a seat whose concession is pending, or has settled, is told so, and a settled one that its Seal records and the Chronicle stay open to it.

UX-1198 The end-of-game screen MUST present the record VC-3050 defines: the winner and the route by which they won; every player's placing, final Standing, final Tenure and the turn they left; the stored Terms paragraph unchanged (VC-2000, UX-185); the complete Chronicle; and every dismissed linter warning (UX-195). It MUST be exportable and MUST be reachable again from the Load Game screen.


13. Keybindings and input maps

UX-1200 Key Bind dialog: a settings screen MUST enumerate every command in the game — it is the authoritative command list — show current bindings, support rebinding with conflict detection, and support multiple named binding profiles per account. Defaults for new profiles come from the account defaults; profiles sync via cloud accounts where available (08-services-platform.md).

Fixed 2026-09-24: the dialog rebound keys in its own state and nothing else read it — the dispatcher, the command bar and the context menu kept the catalogue's defaults — so a rebinding was accepted, did nothing, and was lost when the dialog closed. The shell now owns the profiles, dispatches with the active one, and keeps them in the guest profile (UX-220).

UX-1210 Discoverability: commands without a visible control — directional targeting, map scrolling, playback controls — MUST appear in the Key Bind dialog and the command palette (UX-610). Waypoint labels MUST be bindable to keys that centre the map on them (UX-870).

UX-1220 Default keyboard and mouse map. All bindings are rebindable. Defaults MUST satisfy UX-1225 — the browser reserves keys a native client owns outright, and the shipped defaults must be ones that survive in a tab.

Command Default binding Notes
Commit orders / revoke commit Ctrl+Return / Ctrl+Shift+Return UX-295
Undo / redo order edit Ctrl+Z / Ctrl+Y within the Orders phase only (UX-285)
Clear selection Escape reached only after higher layers decline it (UX-014.5)
Select next / previous own unit in initiative order ] / [ UX-315
Cycle by filter Shift+] / Shift+[ UX-690
Stance: March / Advance / Hold / Entrench Alt+1 … Alt+4 UX-745
Clear Stance override (Derived) Alt+0 TM-720
Posture menu P six Postures plus Idle (CM-330)
Sanctions panel Alt+S four dials or three presets (UX-898)
Apply Doctrine Alt+D CM-780
Formation panel F UX-860
Attach / detach from Formation Shift+F CM-330
Task: move to / attack M / A attack opens the preview (UX-1110)
Directional step target (8 ways) the 3×3 cluster Q W E / A S D / Z X C (S = the cell itself), while an order is armed or the map cursor holds focus UX-730 — bound by physical position, not by character; the keypad mapping Numpad 1/2/3/4/6/7/8/9 ships as a named alternate profile
Dispatch: focus / next item / resolve / snooze D / Shift+D / Return / S UX-810
The Board B UX-1160
The Roster R UX-800
Production Overview Y UX-1050
Empire Ledger L UX-1070
Cost of Conquest for the hovered city Shift+C UX-1060
Command View Shift+V UX-395
Overlay menu O UX-460
After-action report Shift+A UX-355
Playback: play/pause, step, skip Space / Period / Escape UX-345
Map scroll Arrow keys
Zoom in / out Mouse wheel, + / −
Centre on selection Home idempotent (UX-490)
Multi-select box drag with primary UX-840
Control groups assign / recall Ctrl+1–9 / 1–9 UX-850
Toggle interface F9 Tab is reserved for focus traversal and MUST NOT be bound (UX-1225.3)
Encyclopedia / help F1 browser-claimed; UX-1225 guard
Command palette Ctrl+K browser-claimed in some browsers; UX-1225 guard
Chat T multiplayer
Quick save Ctrl+S browser-claimed; UX-1225 guard

This table is our design, not a reconstruction of anything. Where a binding is unfamiliar the palette (UX-610) and the tooltip (UX-590) are the discovery path, and every one of them is rebindable.

Two properties of the table are requirements rather than accidents. No default here requires a numeric keypad (UX-730, UX-1225.6). And the directional cluster is modal — it applies only while an order-authoring interaction is in progress (which UX-660 already requires be visible and Escapable) or while the accessible map cursor holds focus (UX-1320.4). It therefore reuses A, S and D, which carry unrelated commands outside that mode; the Key Bind dialog's conflict detection (UX-1200) MUST treat a modal binding and a global one as conflicting only where their modes overlap, and MUST NOT report a false conflict for this row.

UX-1225 Keys the client cannot count on. Running in a tab costs the client keys a native application owns, and the hardware it runs on may not have every key a desktop keyboard has. Both MUST be handled explicitly rather than discovered by players:

  1. Unavailable keys — combinations the browser does not surrender to the page (new/close/switch tab and window, quit, developer tools, full-screen toggle, and the platform's window-management chords) MUST NOT ship as defaults, and the Key Bind dialog MUST refuse to bind them in the browser build while flagging, not hiding, that they remain bindable in Tauri builds (UX-018).
  2. Claimable keys — combinations the page can take by suppressing the browser default (the function-key row, Ctrl/Cmd+S, Ctrl/Cmd+K, Backspace, and similar) MAY be bound, but MUST be suppressed only while focus is on the game surface and never while a text field has focus (UX-014.4), so the browser's own save, find, and text editing keep working where a player expects them.
  3. Tab MUST remain focus traversal (UX-1340) and MUST NOT be bindable to a game command. This is a deliberate divergence from a native client's freedom, and it is why UX-580's default is F9.
  4. Per-platform conflict detection — the Key Bind dialog MUST validate a binding against the host it will run on and warn per browser where behaviour differs, rather than silently producing a binding that works on one player's machine and not another's.
  5. A player MUST be able to see, in one place, every command whose default differs between the browser and packaged builds.
  6. Keys the host keyboard may not have. Browser reservation is not the only way a shipped default can be unpressable. No default may require a key that the keyboards of a supported platform do not have, or that the active layout does not produce. Concretely, the whole shipped set MUST be operable on a keyboard with no numeric keypad — the case that motivates this clause is directional targeting (UX-730) — and any default whose meaning is spatial MUST be bound by physical key position so it survives a change of layout. A key that is present but reachable only through a system modifier (the function-key row on Apple laptops, which defaults to media control) is not absent and MAY ship as a default, because the palette (UX-610) reaches its command without it. The Key Bind dialog MUST check a candidate binding against physical availability and layout as well as against browser reservation; where availability is a property of the player's own keyboard rather than of the platform it MUST warn rather than refuse, and a binding profile imported or synced from another device (UX-1200) MUST be flagged where it names a key this host has never produced.

The final reserved-key sets differ between Chromium, Firefox, and Safari and change between releases, so this is a policy to be owned and re-tested, not a fixed table (open question 7).

Rationale for clause 6, added because its absence hid a real defect: clauses 1, 2 and 4 audit the shipped defaults against what the browser takes away, which made this requirement look like a complete check on defaults when it was a check on one failure mode out of two. A default can be equally unpressable because the keyboard in front of the player has no such key — and UX-1340 promises that every function is operable without a pointer, a promise that fails silently on the hardware the key is missing from. The two failures deserve one audit because they are both "this default does not work here", and because whichever one is unaudited is the one that ships.

UX-1230 Touch input map:

Gesture Action
Tap select (UX-650)
Tap with an armed order target
Long-press location inspect — the location readout (UX-520)
Long-press selection radial command menu (UX-1400)
Drag from the selection route preview and order on release (UX-670)
One-finger drag on the map pan
Pinch zoom
Two-finger tap cancel the current in-progress interaction (UX-660)
Persistent on-screen controls commit (UX-295) and the Dispatch (UX-810) always reachable

Touch in a WebView carries host gestures the client does not own. Over the map surface the client MUST suppress pull-to-refresh, double-tap zoom, and the long-press selection callout (UX-014.3), and MUST NOT suppress them over DOM panels. Edge swipes MUST NOT be the only way to reach any surface: on iOS a swipe from the screen edge is system navigation and cannot be reliably captured, so every drawer MUST additionally have a visible handle (UX-1370), and on hosts where an edge gesture is unavailable the client MUST simply not offer it rather than binding a gesture that half-works. Per-host edge-gesture behaviour is open question 2.

Three rows built 2026-09-24 (client/src/map/touch.ts, the scene): a tap with an order armed targets — a touch has no secondary click, so before this a phone could arm an order and never aim it — a pinch zooms within the wheel's band, and a brief, still two-finger tap cancels what is in progress. The fingers lifting from a pinch are not taps. Long-press on the selection (the radial menu, UX-1400) and drag from the selection are not built.

UX-1240 Gamepad input map (full playability required on gamepad):

Control Action
Left stick move the map cursor
Right stick pan; click centres on the selection
D-pad directional step targeting
A / Cross select or target at the cursor
B / Circle cancel / clear selection
X / Square radial command menu for the selection
Y / Triangle Dispatch: next item
LB / RB previous / next own unit in initiative order
LT / RT zoom out / in
Start game menu
Select/Back toggle interface (UX-580)
Start + A commit orders, with confirmation

In the browser, controllers are exposed through the Gamepad API, which does not report a connected pad until the player presses a button and offers no guaranteed haptics. The client MUST therefore show a "press any button to connect your controller" affordance rather than reporting no gamepad, MUST treat rumble as optional decoration and never as a required signal, and MUST route gamepad input through the command dispatcher (UX-014.6). Tauri builds MAY use native gamepad APIs for parity, but the browser build MUST remain fully playable on a pad without them.

Built 2026-09-24 (client/src/keys/gamepad.ts, useGamepad.ts). The pad is polled once a frame through the Gamepad API's standard mapping and every row of the table runs a catalogue command through the shell's handler map, or acts through the map cursor the keyboard uses: the left stick moves it, A acts at it, the D-pad steps the targeting cluster, B is the Escape stack itself, X opens the context menu, and Start + A asks once and commits on the second press. Settings says "press any button to connect" until the browser reports a pad. No rumble is used. Not checked on physical hardware from this machine.

UX-1250 Input coexistence: keyboard, mouse, touch, and gamepad MUST be hot-swappable at any time without a mode switch; on-screen hints MUST follow the most recently used device. External keyboard, mouse and gamepad MUST work on mobile platforms.

UX-1255 The command reference — every command, its binding, its applicability and its availability per platform — MUST be generated from the command catalogue rather than maintained by hand, and MUST be viewable and exportable from the Key Bind dialog. A hand-maintained list is a list that will be wrong by the second release.


14. Onboarding and the encyclopedia

UX-1260 The tutorial system MUST be activatable from setup and from within a running game, and MUST cover at minimum, in this order: the three phases and what each permits; issuing a Posture with its Sanctions; reading the initiative readout and changing it with Stance; reading the combat preview; watching a Cascade and reading the after-action report; using the Dispatch; and reading the Watch.

Rationale: these are the seven things a player cannot work out by clicking, and the first two are the ones that decide whether the turn model feels like a plan or a gamble.

UX-1270 The tutorial ladder MUST follow the specification set's own disclosure structure rather than inventing a parallel one: Initiative Tier 1 → 2 → 3 (TM-1040), Sanction presets → the four dials → Formations → Postings and Requisitions (CM-1980, CM-1990), and the Industry ladder's four levels with their unlock cards (US-950). Each stage MUST be a playable scenario authored per 07-modding-content.md, and each MUST be skippable.

UX-1280 Contextual help: every tooltip (UX-590) MUST link to the relevant help topic; the help system MUST be searchable and hyperlinked, with the full content available offline (UX-110). Help and encyclopedia content are DOM documents, so browser find-in-page, text selection, copy, and deep-linkable topic URLs MUST all work rather than being reimplemented (UX-045).

UX-1290 The encyclopedia MUST be complete from turn one for every player, regardless of their Industry level, of what they have built, and of what they have seen (US-930). It MUST present, per unit definition: the full stat line, capability flags, the one-sentence role, the two units it counters and the two that counter it, its build requirements and the Industry level that unlocks it, and its carriage arithmetic. It MUST present the terrain set with movement costs per movement class, passability, defensive values and concealment. It MUST present every unlock card and contact briefing already seen (UX-960).

It is a reference view only: it MUST NOT expose any fact the player's fog-filtered view withholds about a specific unit or city on the map (UX-020).

Rationale: there is fog over the map, not over the rules. A player who is out-teched should be beaten by tempo and by worse decisions, never by not knowing what the thing that killed them was.

UX-1295 Live derived values. The numbers a player acts on MUST be shown as core-computed live values at the place they act on them, and the client MUST compute none of them (UX-040):

Derived value Where it MUST appear Source
Effective Works, decomposed into every step city panel, Production Overview EC-1790
Levy, decomposed, with the Integration at which the next point arrives city panel, Empire Ledger EC-1800
Integration and Unrest, their deltas, every contributing term, and the projected turn city panel EC-1810, EC-1820
Garrison requirement, points present, and the next threshold city panel EC-1830
Turns to completion for the head queue item production list, Production Overview US-1250, US-1270
Industry upgrade cost, Works gain for this city, and payback Industry ladder panel US-890, US-940
Initiative and every active term selection panel, during Orders TM-1080, TM-1090
Win probability, loss bands, pivots attack preview CB-2390, CB-2340
Screen coverage, present and candidate Screen authoring CM-450, CM-460
Track progress, threshold, ratio, Vigil remaining the Watch, the Board VC-030, VC-490
Cost of Conquest, every line the Cost of Conquest panel EC-1850
Endurance remaining and the return envelope unit panel, endurance overlay US-2040

UX-1300 First run: after profile selection (UX-060) the client SHOULD offer a solo game at the Field disclosure profile, on a Small map, under the Standard War Terms, as the way to learn the game — and MUST make clear that every system hidden by that profile can be enabled immediately.

Built 2026-09-24: the chooser's second step, after a profile is picked. "Set up the learning game" opens the setup wizard at Review on one local seat against one AI, a Small map and the Standard War preset at Field's Tier, so it is one click from play and every step is still reachable; the same step says nothing hidden has to be earned. The offer is made whichever profile was picked, since the learning game is the same game either way.

Ruled 2026-09-24, against VC-960. Standard War's Dominion runs a Vigil of 3, and VC-960 makes that the setup linter's L3 on any map under 24 cities. The Small map has 19, so the learning game this row asks for opened on its own warning ("Vigils will thrash"), in orange, on the Terms tab of the first game a newcomer is offered. The learning game is therefore Standard War with Dominion's Vigil at 2, which is under L3's threshold and is the figure Skirmish gives Dominion for a short game. That is the one change. It is carried as the player's override, so the Terms step shows it rather than hiding it in the preset. Measured over every preset on the learning map: Standard War, Long War and Tournament raise L3; Skirmish raises L2; The Keys cannot be scored (L8); Every Last City raises L10. Only Blitz and Intrigue are clean, and neither is a first game. Standard War at Vigil 2 is clean (client/test/disclosure-profiles.test.ts, with the unchanged preset's L3 pinned beside it).

UX-1305 Unlock cards (US-950) and contact briefings (US-960) are onboarding surfaces: each MUST be dismissible in one action, MUST NOT interrupt the Orders phase more than once per turn in aggregate, and MUST be re-openable from the encyclopedia.

Built 2026-09-24 (client/src/production/onboarding.ts). Until then each surface was its own overlay — one per city whose upgrade landed, shown one after another, and the briefing beside them — so a turn with three upgrades and a first sighting interrupted four times. Every card owed on a turn now comes as one sequence behind one dismissal, and the turn is recorded per seat; a briefing that becomes owed after that waits for the next turn, which loses nothing because it stays owed until shown. Unlocks arise only at the Ledger phase, so all of a turn's are in its one sequence. Every card is the encyclopedia's own entry, which is the re-open clause.


15. Accessibility

Because every surface except the tile map is real DOM (UX-010), accessibility here is not a compatibility layer bolted onto an engine's control toolkit — it is the platform's own semantics, assistive-technology stack, focus model, and zoom. The requirements below take that for granted and spend the effort where it is genuinely still hard: the canvas.

UX-1310 Colour. The per-seat foreground/background/text trio (UX-210) MUST be preserved, and the client MUST ship colourblind-safe default palettes plus optional pattern and shape overlays so that ownership is never encoded by hue alone. The same applies to every graded encoding this document introduces: fidelity grades (UX-420), supply states (UX-1010), condition (UX-1125), victory bands (UX-1155), and chart series (UX-1145) MUST each be distinguishable without colour vision.

A player MUST be able to re-map any seat's colours for their own view in game; the preference is saved with that player's seat and affects only their view. The DOM layer MUST honour the platform's contrast preferences, including forced-colours modes, without losing information, which means no control may carry its meaning solely in a background image or a CSS-only colour. The canvas cannot participate in forced colours; on the map, the palettes and pattern overlays of this requirement are the substitute, and the client MUST enable them by default when a high-contrast preference is detected.

UX-1320 Screen-reader support MUST be delivered through native DOM semantics, not through a parallel description tree:

  1. Semantic markup first. Every non-map surface MUST be built from real elements — buttons, links, headings, lists, tables, form controls, native modal dialogs — with ARIA only where no native element exists. Every interactive control MUST have an accessible name and MUST expose its state (pressed, expanded, disabled, selected, current). A disabled command MUST expose why (UX-560), so a screen-reader user learns what a sighted player learns from the tooltip.
  2. Live regions for changing values. The phase indicator, the Watch, the Dispatch, the empire strip and the location readout MUST be announced through live regions at severity-appropriate politeness: routine changes polite, combat results and Alarms assertive. Announcements MUST be coalesced and rate-limited so that a large Cascade produces a readable summary rather than an unfollowable stream — the same obligation the clustering requirement of UX-350 places on the visual channel.
  3. Tables are tables. The data-dense surfaces of UX-045 MUST expose row and column headers so a screen reader can announce "Nyborg, Industry 2, Yard 14 of 21, producing Armour, 3 turns" while traversing — which is what makes UX-1050 usable non-visually at all.
  4. The map MUST have a DOM equivalent. A WebGL canvas is opaque to assistive technology, so the client MUST provide an accessible map view: a focusable DOM grid built strictly from the player's fog-filtered view (UX-020), exposing a keyboard-driven map cursor that reports each cell's terrain, movement cost and defensive value, contents with fidelity grade and observation age (UX-420), ownership, and Chebyshev distance from the selection — the same payload the location readout shows (UX-520). Every location-targeting interaction MUST be issuable from it, including all of UX-735's geometry authoring. It MUST be rendered as a bounded window of DOM cells around the cursor rather than as a grid of the whole map, and MUST stay within UX-050's budgets. It MUST stay in sync with the canvas camera in both directions: moving the cursor scrolls the map, and centring the map moves the cursor.
  5. Playback MUST be followable non-visually. Step-through (UX-345) MUST announce each observed event as a sentence naming actor, action and location, and the after-action report (UX-355) MUST be the complete non-visual equivalent of the animation — a player who never watches playback must lose nothing but the animation.
  6. Verification is a gate, not a checklist. Automated accessibility checks MUST run in CI over every screen, and the client MUST be manually verified each release against a named matrix of at least NVDA on Windows, VoiceOver on macOS and iOS, and TalkBack on Android. The granularity that actually reads well in those three is open question 8.

UX-1325 Focus management MUST be explicit and centrally owned:

UX-1330 Zoom and scaling. Two independent mechanisms MUST both work:

The canvas MUST render at the current device-pixel ratio and re-render when it changes, including when it changes as a side effect of browser zoom (UX-385). Touch targets MUST be at least 44×44 CSS px at 100% scale.

Rationale: browser zoom is the single most-used accessibility feature on the web and the one an engine-rendered UI cannot offer at all; making it a first-class requirement is a direct dividend of the DOM decision.

UX-1340 Full keyboard operability: every function in this document MUST be operable without a pointer — panels and menus by focus traversal, map targeting via keyboard targeting (UX-730) and the accessible map view (UX-1320.4), every map-anchored menu (UX-016), and every geometry-authoring interaction (UX-735). Tab order MUST follow visual order, Tab MUST NOT be rebound (UX-1225.3), and no command may be reachable only by pointer.

Partially held 2026-09-27: the Tab clause is pinned; full pointer-free operability has no single pin, so the id stays untitled. “Tab MUST NOT be rebound” (UX-1225.3) stands in foundations.test.ts, where Tab and Ctrl+Tab are reserved for focus traversal across every build — “moves focus”, with no build that frees them. Keyboard targeting (UX-730), the accessible map view (UX-1320.4) and the map-anchored menus stand under their own rows where built; the universal half — every function in this document operable without a pointer, Tab order following visual order — is a property of the whole application no single test holds.

UX-1350 Reduced motion and audio cues: a reduced-motion setting MUST disable non-essential animation and MUST default to on when the platform reports a reduced-motion preference rather than waiting to be found in settings. With reduced motion on, Cascade playback MUST default to instant with the after-action report (UX-345, UX-355) rather than being removed. All information conveyed by sound MUST have a visual counterpart, and every per-unit state of UX-430 MUST be readable on the unit panel and not solely as a map badge.

UX-1360 Localisation: every UI string MUST come from the localisation system (07-modding-content.md); layouts MUST tolerate 40% string expansion and RTL scripts. In the DOM layer this is largely the platform's job and MUST be left to it — text renders as real text nodes so the browser performs line breaking, bidi resolution, and font fallback, the document root carries the active language and direction, and RTL is achieved with logical layout properties rather than a mirrored stylesheet.

⚖ Ruled 2026-09-26 (owner-delegated, R2): requirement ids are not player text. A string a player reads in play — a control, a notice, a panel line, a tooltip, the report, the Dispatch, the playback, the setup wizard — carries no requirement id ("(GR-290)", "(AR-600)", "TM-220"). The id belongs in the code, its docblock and its test, where it has always been the evidence; in front of a first-time player it is noise that reads as a fault code (found by an exploratory tester, 2026-09-25). Two places keep them on purpose: Help topics and Encyclopedia entries may carry a folded "Where this is in the rules" line, for playtesters who report against the specification; and developer surfaces behind their own setting (the resolution trace of UX-1120) keep the core's names and ids whole. This is the same rule localisation will impose anyway — a translated string has no business carrying a specification's cross-reference — stated now so the strings are right before they are extracted. A client test holds it over the rendered surfaces.

Two obligations do not come free:


16. Mobile and touch adaptation

UX-1370 Responsive layouts: the interface MUST adapt by breakpoint — phone (< 600 CSS px width): the command bar becomes a horizontally scrollable strip, the Dispatch and menus collapse into drawers, the phase indicator and Watch merge into a compact top strip that keeps one row per player; tablet (≥ 600 CSS px): near-desktop layout with docking intact (UX-550). No layout may horizontally clip a command — everything remains reachable by scroll or drawer, and every drawer MUST have a visible handle rather than depending on an edge gesture (UX-1230). Breakpoints MUST be evaluated against the surface the interface actually occupies, so the same logic serves a narrow browser window, a phone, and a Tauri window (UX-018); layouts MUST respect display safe-area insets. Because these are DOM layouts they MUST hold at every browser-zoom step of UX-1330, not only at device widths.

The Watch MUST remain visible at every breakpoint (UX-1150). Where six fields per row do not fit, the client MUST reduce fields by the priority identity, value, Vigil, nearest Track, Tenure, bar and MUST make the full row reachable in one action — it MUST NOT drop a player's row.

*Built 2026-09-24 in part (the max-width: 599px block in client/src/shell/app.css and panel/FloatingPanel.tsx's handle, held by client/test/phone-breakpoint.test.ts). At the phone breakpoint:

The breakpoint is a width media query, recorded as a reading. The shell is the whole surface the interface occupies, in a tab and in a Tauri window alike. A container query on the shell would make it the containing block for the fixed sheet. Checked at 375 × 812, this found a defect older than the slice: below 900 px the shell kept the viewport's height while the stage grew past it. The grid squeezed every row whose item scrolls down to its padding, putting the toolbar in 17 px and the empire strip and command bar in 13 px under the map canvas. The shell now grows with the stage. Still owed: nothing further was identified. The menus became drawers on 2026-09-25: below 600 px the main menu is a sheet from the reading-start edge, 85 % of the width to at most 340 px and full height, with the game in sight beside it and "Back to the game" as its handle. The Dispatch has been in the panel drawer since the first phone slice.

The compact top strip, built 2026-09-25. Below 600 px the Watch dock takes the stage's first row, above the map, with the phase and turn at its head ("Orders · Turn 1"). It keeps one row per player and sheds only its footer, whose Ebb sentence the Board carries. The strip's phase line is hidden from assistive technology, because the facts bar's badge is the live region that announces the phase. Measured at 375 × 812: the strip spans 61–149 px, under the toolbar.*

UX-1380 Touch-first fidelity: the selection and targeting model of UX-650 is already touch-phrased and MUST work identically on touch; the drag-to-order path (UX-670) is the touch default. Every geometry-authoring interaction (UX-735) MUST have a touch-complete form, including multi-tile Survey painting and 16-waypoint March routes.

Survey painting built 2026-09-24 (client/src/map/paint.ts, the scene's primary-pointer stroke, held by client/test/survey-painting.test.ts). While a Survey region is being collected, a one-finger drag paints every tile it crosses, and a mouse drag does the same. Before this the region was collected one tap at a time. A second finger ends the stroke and becomes UX-1230's pinch, so the map can be moved mid-region. Screen lines and March routes stay one tap per point: they are ordered, and a sweep would spend a 16-waypoint route in one gesture. Taps already make those complete on touch. Read, not built: "the drag-to-order path (UX-670) is the touch default" is read as UX-670's route preview following the touch target. UX-1230's tap-to-aim already delivers that: the tapped tile is the cursor the preview reads. The stroke is exercised as a machine and pinned in the scene; it has not yet been driven by a real finger in the browser.

UX-1390 Hover replacements: everything the desktop client delivers on hover — the location readout, control tooltips with bindings, route ETA — MUST be reachable on touch via long press (UX-1230) or an inspect toggle.

UX-1400 Radial context menu: long press on the selection MUST open a radial or sheet menu of its applicable commands, equivalent to the desktop context menu (UX-600), with the same disabled-with-a- reason behaviour (UX-560).

Built 2026-09-24 (client/src/map/long-press.ts's heldMeans, the scene's fire, held by client/test/touch-command-surfaces.test.ts). A held press on the selected tile opens the desktop menu itself — the scene hands the tile to the same openMenu the secondary click calls — so the list and its answers cannot drift from UX-600's; held anywhere else it is still UX-1390's inspect. The menu is UX-600's sheet-shaped DOM list, not a radial: the row admits either, and one menu is one thing to keep accessible.

UX-1410 Virtual directional pad: an optional on-screen 8-way pad MUST provide directional step targeting on touch devices (UX-730).

Built 2026-09-24 (client/src/hud/direction-pad.ts and DirectionPad.tsx, a Settings switch, held by client/test/touch-command-surfaces.test.ts). Off until turned on, and drawn only while an order waits for a target; its eight steps and aiming centre call the direction and aim handlers the Q W E / A S D / Z X C cluster runs (UX-014.6), naming each step by its place in the cluster so a rebound cluster keeps the pad. Building it found those handlers read the chord under the default profile, so a rebound cluster (UX-1200) moved nothing; they now read the active one, and the gamepad's default-cluster chords are carried to it.

UX-1420 Panels on phones: modal and large panels — the city panel, the attack preview, the Board, the Production Overview — MUST present as full-screen or bottom-sheet views on phone-class devices, preserving every control. When the on-screen keyboard opens for a text field (UX-045) the client MUST track the visual viewport so the focused field and its confirm control remain visible, and MUST restore scroll position when the keyboard closes.

Built 2026-09-24 (the phone block at the end of client/src/shell/app.css, shell/keyboard-viewport.ts, held by client/test/phone-panels.test.ts). "Phone-class" is read as at most 640 CSS pixels wide, the breakpoint the client already used for its one-column forms. At that width every open dialog is full-screen: that covers the Board, the Production Overview and every other modal panel. The floating selection panel, where the city panel and the attack preview are drawn, becomes a bottom sheet capped at 60 % of the height. Each keeps every control and scrolls inside itself. The keyboard is read as the visual viewport falling at least 120 px short of the layout viewport; a collapsing address bar is about 60 px. While it is up, its height is published as --keyboard-inset, so the full-screen dialog and the sheet end above it. The focused field, and then its confirm control, are scrolled into view when the keyboard opens and when focus moves to another field; the page's scroll is restored when it closes. The confirm control is the form's submit button, or else the next button in the same panel. Checked at 375 × 812: until the sheet gave its panels their content's size, the narrow-container rule's zero flex basis collapsed it to its grip.

UX-1430 Mobile performance and battery: the client MUST support a frame-rate cap, a reduced-effects mode, and background suspension that preserves all unsubmitted state; resuming MUST restore the exact interface state. Suspension MUST be driven by page visibility, and unsubmitted state MUST be written to local storage on backgrounding rather than on termination, because a mobile WebView may be killed without notice (UX-1190). On resume the client MUST assume the WebGL context may have been discarded and recover per UX-385. The memory ceiling of the mobile WebView, not the device's RAM, is the budget UX-400 must fit inside.

*Built 2026-09-24 in part (client/src/map/frame-cap.ts, a Performance section in Settings, held by client/test/mobile-performance.test.ts). The cap offers 20, 30 and 60 frames a second — 30 is the rate the loop always had — and re-times the running loop live. Reduced effects stops the map's glide and the interface's animations, shadows and blurs. Suspension is driven by page visibility: hidden sleeps the loop and visible wakes it seamlessly. Building it found that Phaser's own response to a hidden page only records the time, and because the build forces a timer rather than requestAnimationFrame, the loop had gone on drawing a canvas nobody could see. Submitted orders reach storage at submission (UX-1190), before any backgrounding.

The persistence half, built the same day (client/src/save/staging.ts, held by client/test/background-staging.test.ts). This covers the work in hand: a geometry draft, a Posture being edited, and an order held for the stranding confirmation. It also covers where the player was: the selection and the map cursor. All of it is written to local storage when the page is hidden, and cleared when the page is shown again, since a page that came back alive still has it all in memory. The record names the game position it was composed against: seed, order count and turn. When a save at exactly that position is opened, the record is put back and consumed. That is either the game UX-1190's adoption filed after a kill, or one the player had saved. One order later a half-drawn route may no longer be legal, so the record is not restored there. The Posture panel takes a resumed edit ahead of the live posture.

UX-1440 Correspondence hand-off. On mobile, an incoming your-turn notification (05-multiplayer.md, 08-services-platform.md) MUST deep-link into the correct game and MUST land on the previous Cascade's review (UX-290) rather than on a bare map, because the first question a returning correspondence player has is what happened while they were gone. Deep links and notification delivery go through the adapter of UX-018, so the same link resolves whether it is opened as a URL in a browser tab or handed to a Tauri build by the OS.

*Receiving half built 2026-09-24 (client/src/shell/deep-link.ts, the adapter's deepLink, onDeepLink and clearDeepLink in platform/capability.ts, held by client/test/correspondence-link.test.ts). A link is the page address with the fragment #game=<save id>. The fragment is the part a static host never sees, and a Tauri deep-link handler can pass it to the same webview unchanged. The link is read at start and whenever one arrives, and consumed at once, so a reload does not follow it again.

The game is opened by the ordinary load path, once the map ceiling that path gates on is measured. It then lands on the previous Cascade: UX-365's replay of the last closed turn, which gives the playback and the report a live close does. A link to a game the device does not hold says so.

Checked in the browser: a turn was closed and saved, then the page was loaded cold with the link. It opened at turn 2 and was playing turn 1's Cascade. Read, not built: no sender exists. A correspondence game here is a local save, and the notification is 05-multiplayer.md's and 08-services-platform.md's to deliver; gameLinkOf is what that sender will write. The Tauri implementation of the three adapter members is the same seam, and lands with the Tauri build.*

UX-1445 Correspondence is a first-class shape, not a degraded one. On any platform the client MUST make a whole turn completable in one sitting with no live opponent present: enter, review the last Cascade, revise orders across the whole empire, commit, leave. Nothing in this document may require a second visit within a turn, and no surface may require a live connection to another player to be usable.

*Held 2026-09-24 (client/test/correspondence-sitting.test.ts). The test runs the whole shell with WebSocket, fetch and EventSource replaced by tripwires that refuse and record any call. One sitting then runs end to end:

No tripwire is touched. A structural half holds that the only code opening a connection is the multiplayer session in src/net/, plus the service worker's cache, which is what lets the client start with no connection at all.*


17. Contracts on other documents

Obligations this document places on subsystems it does not own, and the obligations it discharges, recorded so that a change on either side is detectable.

*Made detectable 2026-09-24 (client/test/ui-contracts.test.ts). Each row below is parsed out of this document and held against the documents it names:

A renumbered or moved row on either side fails the test. UX-1495 is a rule for this document's authors, not a contract a test can hold.*

UX-1450 10-turn-model.md MUST provide: the explicit phase value (TM-090); each unit's exact Initiative with its per-term breakdown and the active tier (TM-1080, TM-1090); the player's own relative activation order (TM-380); the coarse enemy band (TM-1100, TM-1110); effective and derived Stance (TM-730); the compacted per-player observation stream (TM-2150) delivered as one payload in multi-human games (TM-2170); the count of own activations that fell back to no action (TM-2320); the per-unit skip reason (TM-2350); and the frozen own-unit activation order after a Cascade (TM-2340). This document discharges TM-2720.

UX-1455 13-command.md MUST provide: Attention Events carrying class, cause key, affected entities, the integer terms S, T and A, a plain-language summary and an ordered list of offered resolutions (CM-1610), with the client owning P, ranking, deduplication and presentation (CM-1620); the next-intended-action line as a pure function of committed state and the knowledge view (CM-1910, CM-1920, CM-1930); and CommandDecisionRecords retrievable for at least 20 turns (CM-1940, CM-1950). This document discharges CM-2200 and presents the Command View of CM-1880 in UX-395.

UX-1460 11-combat.md MUST provide the preview through the single resolution function (CB-2140) with the content and ordering of CB-2390, the fog-limited prediction of §17 with its pivot analysis (CB-2330, CB-2340), and a combat event sufficient to reconstruct a resolution (CB-2450, CB-2460). This document discharges CB-2830, and adds one decision of its own: the resolution trace is exposed in solo and hotseat games only (UX-1120).

UX-1465 12-economy.md MUST provide the decompositions of its §14.1, the Cost of Conquest lines (EC-1850), the Empire Ledger content (EC-1870), the supply and projected overlays (EC-1880), the Digestion Forecast (EC-1890), and the economic Attention Events (EC-1900). This document discharges EC-2350, and additionally binds itself to EC-020 and EC-050 in UX-1090: no economic quantity rendered as a bar without its integer, and every brake named, numbered, forecast and counterable at the place it applies.

UX-1470 14-victory.md MUST provide the Seal record sufficient to redraw the Board for any past turn without recomputation (VC-250, VC-2400), the published band per player per Track (VC-2160), the report quantum (VC-2250, VC-2260), the Bellwether set (VC-2280), the generated Terms paragraph (VC-1960), the calibration estimate (VC-2030) and the lint findings (VC-2100). This document discharges VC-3170 and computes no victory value.

UX-1475 01-game-rules.md requires this document to render remembered contents distinctly from observed contents with age and fidelity, and to draw Contact uncertainty as regions rather than icons (GR-1190, GR-1870). UX-420 discharges both, and extends them with exclusion rendering (GR-1350) and decay warning (GR-1300).

UX-1480 02-units-and-industry.md MUST provide, in addition to what it already specifies: a short display label per unit definition for the Chip tier (UX-390) and an icon key, neither of which the current schema (US-070) carries; the greyed-build explanation naming the failing clause (US-370); the unlock card and contact briefing content (US-950, US-960); and the endurance warning (US-2040). This document discharges US-2190.

Note on citations: US-2120 declares every US-NNN identifier from the withdrawn 02-unit-sets-content.md stale, including the ones an earlier revision of this document cited. Every US- citation in this revision has been re-resolved against the current 02-units-and-industry.md.

UX-1485 03-architecture.md owes this document two reconciliations:

  1. Map tiers and the reference fixture. AR-690's tiers (Standard 200×200, Large 1,000×1,000, Extreme 4,096×4,096) and AR-760's reference game (1,000×1,000, 2,000 cities, 10,000 units) do not correspond to any size 01-game-rules.md defines. GR-230's shipped tiers top out at 240×168 with 269 cities, and GR-120 caps any map at 2,048 per axis. UX-050 states this document's budgets against GR-230 and will adopt whatever single fixture 01 and 03 agree; the client MUST NOT be gated on two different fixtures.
  2. Retraction is no longer required by the client. AR-330's RetractOrder, and the retraction soundness property AR-950(f) tests, exist to serve an undo affordance this document no longer has. Under TM-360 revision is free until CloseOrders and after that there is no undo (UX-285, UX-710). 03-architecture.md may retire the mechanism as far as this document is concerned; the citations to UX-710 and UX-720 it currently carries should be re-read against those requirements' new content.

03-architecture.md MUST continue to provide: the host-agnostic core (AR-010) and its worker execution (AR-780) for UX-055; typed order records and typed rejections (AR-245, AR-260, AR-320) for UX-738; structural view filtering (AR-420) and incremental fog (AR-480) for UX-020; view serialisation (AR-500) for the client's transport; and deterministic pathfinding (AR-750) for UX-670.

UX-1490 05-multiplayer.md owns transport of order records and observation streams, lobbies and invitations, the presence channel this document renders in UX-300, single-payload stream delivery (TM-2170), correspondence hand-off and turn notifications (UX-1440), and spectator policy (UX-365). 08-services-platform.md owns accounts, profiles and binding sync (UX-220, UX-1200). 07-modding-content.md owns localisation (UX-1360), generator option schemas (UX-170), content editing (UX-070) and tutorial scenario authoring (UX-1270). 06-ai.md owns the AI seat contract and the disclosure of any AI advantage this document renders in UX-230.

UX-1495 No document may require this one to present a value at a precision, or from a source, that its owning document does not publish. Where a presentation requirement here appears to exceed what a document publishes, this document is wrong and MUST be corrected here rather than by widening disclosure there. no-test-needed: an editorial meta-rule that binds whoever edits a presentation requirement outrunning its owning document’s publication, not any running behaviour; its enforcement is the spec review this note participates in, and no test can pin it.


Open questions

Every question below is one we own. There is no external artefact to check any of them against.

  1. The reference fixture. UX-050 gates the client on a Vast game — 240×168, 8 players, 269 cities, 3,200 live units — chosen to match GR-230's largest shipped tier and the largest unit population any neighbouring document budgets against (VC-310). 03-architecture.md's AR-690 tiers and AR-760 reference game describe a much larger world that no rules document defines. One fixture has to win, and it should be the one the rules produce. Until 01 and 03 agree, the client's measured budgets and the core's are not comparable (UX-1485).

  2. iOS. Tauri 2 mobile maturity for App Store distribution, WKWebView performance headroom for both layers, and empirical edge-gesture capture behaviour (UX-018, UX-1230). This is the platform where the canvas budget and the DOM budget compete for the same ceiling.

  3. How much of a Cascade should be animated? TM-2280 requires that playback not scale linearly with observed events and delegates the presentation here. UX-350 sets clustering as the mechanism but not the threshold. The right number is almost certainly a function of how much the player observed rather than how large the Cascade was, and it needs playtest at turn 20 and at turn 150, because a sixty-second animation is a pleasure at one and an obstacle at the other. The signal to watch is the skip rate.

  4. Which overlays default on? Eleven overlays (UX-460) is more than any player will discover by toggling. The disclosure profiles (UX-060) reduce the set, but the choice of which three are on for a new player is a real design decision that has not been made. Supply, intelligence and victory sites is the current guess.

  5. Browser storage. Per-browser quota and eviction behaviour for event-sourced saves (UX-110), and whether a first save should require an explicit export or cloud-sync opt-in rather than merely offering one.

  6. The Canvas fallback. How often does WebGL context loss actually occur on target mobile GPUs, and is the reduced Canvas tier of UX-385 worth maintaining, or is it dead weight guarding a case that does not happen?

  7. Browser-reserved keys. The per-browser, per-platform reserved sets and the final default binding table (UX-1220, UX-1225). The sets differ across Chromium, Firefox and Safari and move between releases, so this needs an owned, re-tested policy rather than a one-time survey.

  8. Accessible map granularity. What cursor-window size and announcement format actually read well in NVDA, VoiceOver and TalkBack (UX-1320.4)? A DOM grid is necessary and not sufficient, and this needs testing with screen-reader users before the design is fixed. The harder half is UX-1320.5: what a Cascade sounds like when it is announced rather than watched.

  9. Localisation budget. What per-locale download size is acceptable for CJK coverage in a browser-first product, and does the Chip-tier short-label atlas need per-locale generation at load time or can it be pre-built per shipped unit set (UX-1360)?

  10. Map performance at the Strategic tier. UX-050 asserts 60 fps with the whole Vast map on screen using entity aggregation and, above GR-250's tested envelope, an overview raster. Whether Phaser 3's tilemap can host those two mechanisms or whether they need a bespoke instanced render path is the largest technical unknown the stack choice creates. It is a much smaller unknown than it was when the target was a million tiles.

  11. Worker transfer. Whether whole fog-filtered view snapshots for the UX-050 fixture cross the worker boundary fast enough for interactive play, or whether the incremental ViewDelta form is required on the client too (UX-055). At 40,320 tiles this is probably a non-question; it is recorded because the answer changes if custom maps near GR-120's ceiling become common.

  12. Does rolling planning help or confuse? UX-330 permits staging next-turn orders during playback. It removes dead time in correspondence play and it introduces a state where two kinds of order look identical and behave differently, which is the sort of thing that produces bug reports about orders that "did not save". The safe answer is to ship it off by default and measure.

  13. The Dispatch on a phone. Seven items with one-action resolutions is calibrated for a desktop panel. On a phone the same seven items are the whole screen. Whether the phone cut should be lower, or whether the digest should lead and the items follow, is unresolved (UX-810, UX-1370).

  14. Does the two-pivot limit read? CB-2350 caps the preview at two pivots and this document presents them (UX-1115). Two thresholds plus two loss bands plus a verdict is already a dense panel, and on a phone it may be too dense to act on quickly. The alternative — one pivot on small screens — loses exactly the case where the second pivot was the one that mattered.

  15. Shared annotations. Should multiplayer-visible map annotations and pings ship, or do labels remain private (UX-870)? It depends on 05-multiplayer.md's scope and on whether a game with no diplomacy wants a channel for coordination that is not chat.

Decisions recorded here because an earlier revision left them open


Built from source 47fa2827eff0 · VERSION.json