UX Spec: Title Screen
Status: Reviewed — APPROVED (
/ux-reviewre-pass 2026-09-01: 0 blocking, 4 advisory — all applied; pass 1 3 B / 8 A applied same day) · v0.3
Author: Jeroen + ux-designer
Last Updated: 2026-08-31
Journey Phase(s): unknown — no player journey map yet (see Open Questions)
Template: UX Spec
Variants: none at the screen level — the third-party differences live in the Host / Join sub-flows
Requirements input:design/ux/settings.md(Settings entry; Back returns here) ·design/ux/pause.md(this screen is the session exit target and owns the session-ended notice) · ADR-0007 §4 (the reset floor: hold default Esc / Start 3 s wipes binding overrides) ·design/accessibility-requirements.md(the pre-launch photosensitivity notice lives here; reduced-motion keep-row added for the tide loop 2026-09-01) · art bible §2 row 6 (Menus/Title mood: a frozen mid-transition gradient, held static, low energy — the ≈ 40 s loop is the near-still reading of it; art-director to confirm, OQ 8) · netcode quick-spec Rules 1/3/4 (host-and-play, whoever owns the save hosts, capacity 2 at MVP, transport undecided until ADR-0008) · save quick-spec Rules 1/4 (one world = one host-owned file; loading an older copy forks permanently — a fact, not an error; version gate refuses loudly)
Patterns used: #1 Button · #2 List Picker (no-wrap deviation, declared) · #3 Panel/Modal · #4 Focus & Nav · #6 Reason String · #27 Option Cycler (stretched — the join-code steppers run well outside #27's 2–6-named-options envelope and wrap; declared as a deviation + a Gaps row, not a clean reuse)
Related specs:design/ux/settings.md,design/ux/pause.md(both APPROVED; both name this spec as their missing dependency),design/ux/hud.md(not present here — no HUD at title)
Purpose & Player Need
The title screen is the front porch of the shared world: pick it up and host, walk over to a friend's (join), make the game fit (Settings), or leave. The player arrives wanting to be in the world with their friends in as few presses as possible — for the host that's one press on the world they always play; for a friend, one press on Join. It also carries the three things no other screen can: the session-ended notice (the world behind it just closed — this is where that fact lands, tone-safe), the binding reset floor (the hold that works even when every binding is wrecked), and the one-time photosensitivity notice. Nothing here may manufacture ceremony: no splash chain, no "press any key" gate, no news panel.
Player Context on Arrival
- First encounter: first launch — the photosensitivity notice shows once; otherwise the same screen as always. Calm, curious.
- Typical: (a) evening start — the host continues the world, friends join; both want speed; (b) after a session ended — the host pressed End, or the connection dropped: the notice states what happened as a world fact ("the session has ended — everything is saved" / "the connection was lost — you can rejoin when it's back"), never as failure theatre; (c) after a version-gate refusal — the world list is where the loud, clear refusal message surfaces (#6).
- Emotional assumption: anticipation on the way in; mild "the evening's over" on the way back — the screen must never sharpen that into loss or blame (tone rule).
- Voluntary: always — it is also where the game puts you when a session ends, but never mid-anything.
Navigation Position
Root. Everything starts here and every session ends here. Children: Host (world list sub-panel, #2), Join (mechanism stub — ADR-0008 owns the flow; provisional direct-connect field for the playtest build), Settings (design/ux/settings.md — its Back returns here). Hidden sub-flow: the reset floor — holding the default Menu control (Esc / Start) for 3 s anywhere on this screen resets all binding overrides (ADR-0007 §4; deliberately not a visible button — settings' Reset is the visible one; this is the recovery of last resort). Exits: into a session (host / join), or to the desktop (Quit).
Entry & Exit Points
| Entry Source | Trigger | Player carries this context |
|---|---|---|
| App launch | — | settings.json + bindings.json applied; first launch adds the photosensitivity notice |
| Session ended (the host pressed End, or you left) | automatic | notice per cause; everything already saved / persisted host-side |
| Connection lost / host quit (client) | automatic | tone-safe notice; "rejoin" = just Join again (a rejoin is a late join — netcode Rule 4) |
| Settings (child) | its Back / UI.Cancel | focus restored to the Settings button |
| Version-gate refusal | attempted Host on an incompatible save | you stay here; the world row carries the refusal reason (#6) |
| Exit Destination | Trigger | Notes |
|---|---|---|
| Session (as host) | world row → load | starts hosting; loading an older copy of a save forks the world permanently — a fact the row states via its timestamp, never a warning dialog |
| Session (as host, fresh) | New world row | seed rolled; one file created host-side |
| Session (as client) | Join | flow owned by ADR-0008 (Steam expected); provisional direct-connect field in the playtest build |
| Desktop | Quit | nothing to flush — settings and bindings were written per change; the save belongs to sessions |
| This screen | the reset floor completes | overrides wiped + one confirmation line (a world fact); no navigation |
No one-way exits from the screen itself; the only permanent act reachable from here is the fork-by-older-copy, which is a filesystem fact the screen reports honestly rather than gates.
Layout Specification
Information Hierarchy
- The world itself — the Tideline art loop (amber ↔ violet) is the first read; the screen's identity is the game's one rule, ambient.
- The notice line (only when present: session ended / connection lost / controls reset) — the first text read on a return.
- The button column — Continue → Host… → Join → Settings → Quit.
- Version string, bottom-right, dim — playtest bug reports name a build.
Absent by design: news, splash chain, a press-any-key gate, profiles / accounts, save thumbnails, music-credit ticker — the porch has nothing to sell.
Layout Zones
The only screen with fixed world-art behind it, so px values are anchors, not a panel box (1080p @100 %; anchors scale with the UI-scale option).
- Z1 — Wordmark: top-left, 48 px safe margin (art asset, not text).
- Z2 — Button column: left-anchored at x = 96 px, bottom-aligned 120 px from the bottom edge; five buttons 320 × 48 px, 12 px gaps.
- Z3 — Notice line: 560 px wide (wider than the column, left-anchored with it at x = 96, ending at x = 656 — clear of the art's focal band, Z-clearance table below), up to 2 lines of Medium 16–18 px; 24 px above the column, and the column's bottom anchor is fixed so a second line grows upward into empty sky. Persists until the next input, then clears. Longest EN string (52 chars, "the connection was lost — you can rejoin when it's back") measures ≈ 470 px — one line at 100 %, two at 150 % or under 40 % expansion, both budgeted.
- Z4 — Version string: bottom-right, Small 13–14 px, dimmed:
driftlands <version>+<commit>. - Z5 — Sub-panels (world list · provisional join · photosensitivity notice): centred #3 panels, 420 px wide, height to content, over the art; one at a time (#3).
- Background: the tide loop, unobstructed to the right of the column. Reduced motion: the loop becomes a still frame with only the slow tint crossfade retained (luminance-stable, no drift parallax) — flagged into the a11y keep/remove lists (Accessibility).
Component Inventory
| Zone | Component | Pattern | Interactive | Notes |
|---|---|---|---|---|
| Z2 | Continue — [world] | #1 Primary | yes | the one-press path: loads the most-recent world and hosts. Hidden when no world exists. This screen has a Primary — Continue is a commit into a session, exactly #1's class; the no-Primary-for-dismissal rule (settings Z4 / pause) does not apply |
| Z2 | Host… | #1 Secondary | yes | opens the world list (Z5) |
| Z2 | Join | #1 Secondary | yes | flow owned by ADR-0008; playtest build: opens the provisional direct-connect panel |
| Z2 | Settings | #1 Secondary | yes | settings.md; its Back restores focus here |
| Z2 | Quit | #1 Secondary | yes | to the desktop |
| Z3 | notice line | #6-tone text | no | per-cause strings (States); cleared on the next input. Carries the slate fill behind it (like the button column) so contrast never depends on the tide-loop phase |
| Z5 | World list | #3 + #2 | yes | rows: auto-name ("World of {0}" with the locale short date — "World of 8/29/2026" in en-US; no text input / no rename at MVP, ruled 2026-08-31) · last-played timestamp; New world as the last row; a version-refused save = dashed row (#1 disabled grammar) with its refusal reason on focus (#6) — refused worlds stay listed, honestly; Back (Secondary) |
| Z5 | Join panel (provisional — playtest build only) | #3 + #27 | yes | a 6-character join code entered on six character steppers (◂ A ▸ per character; Navigate ←/→ steps a character, ↑/↓ moves between them) — ruled 2026-09-01: fully gamepad-operable, so the project keeps zero parity exceptions and needs no text-input pattern; on KB/M the six steppers also accept direct typing and paste as a convenience, never as the only route · Connect (Primary) · Back. |
Deviation from #27, declared (2026-09-01): #27 is specified for "2–6 discrete named options … no wrap". A character stepper carries ~32 glyphs (A–Z 0–9 minus the ambiguity-free exclusions) and wraps — a character ring has no meaningful end, and without wrap reaching Z from A costs 31 presses. Submit on a focused character does not step it (that is ←/→ only) — Submit belongs to Connect, so the panel's commit affordance is never ambiguous. Tracked as a "character stepper" Gaps row in the pattern library; the widget geometry (six steppers in a 420 px panel) is likewise title-screen-local. What is provisional here, and what is not (2026-09-01): the six-stepper mechanism is what delivers gamepad parity and it survives any ADR-0008 outcome — only the alphabet and payload are provisional. A join code presupposes a code-issuing lobby / relay layer (Steam lobby ids, a relay service); an address does not. ADR-0008 owns whether such a layer exists (netcode Rule 7); if it lands on a bare IP transport, the same six steppers become numeric-address entry and the parity guarantee is untouched (Open Question 2) |
| Z5 | Photosensitivity notice | #3 | yes | one-time on first launch; short body + OK (Primary); photosensitivityNoticeShown stored in settings.json (a11y doc: pre-launch notice) |
| — | reset floor | ADR-0007 §4 | hold | invisible by design; holding the default Menu control (Esc / Start) 3 s anywhere on this screen wipes all binding overrides → Z3: "controls were reset to defaults" |
| Z4 | version string | text | no | build id for playtest bug reports. Dimmed, but on the slate fill and verified ≥ 4.5:1 like every other text on this screen (dimming reduces contrast against the art, so the fill is required — Accessibility) |
ASCII Wireframe
DRIFTLANDS░
(wordmark) ~ tide loop: a land silhouette drifting
in on amber, out on violet ~
the session has ended — everything is saved
┌──────────────────────────────┐
│ Continue — World of 8/29/2026 ─▸*
├──────────────────────────────┤
│ Host… │
│ Join │
│ Settings │
│ Quit │
└──────────────────────────────┘
driftlands 0.1.0+2e8cfe3
* = focused (3 px outline + inset tick). The world list, join panel and photosensitivity notice are centred 420 px #3 panels over this scene.
Z-clearance (measured, not asserted — 1920 × 1080; the art's focal band is defined as the centre 40 % of width, x = 576–1344, where the drifting land silhouette sits — art-director to confirm, OQ 8):
| Scale | Column x-range | Z3 x-range | Centred panel x-range | Column → focal band | Column → panel |
|---|---|---|---|---|---|
| 100 % | 96 – 416 | 96 – 656 | 750 – 1170 | 160 px clear | 334 px |
| 125 % | 120 – 520 | 120 – 820 | 697 – 1222 | 56 px clear | 177 px |
| 150 % | 144 – 624 | 144 – 984 | 645 – 1275 | overlaps by 48 px | 21 px |
At 150 % the column's right edge crosses into the focal band by 48 px and sits 21 px from the centred panels — both stated as accepted, not clearance: the column carries its own slate fill so legibility never depends on the art (Accessibility), the art's subject is a silhouette whose read survives partial occlusion (art bible §1 strip-to-black), and the panels are modal — nothing needs to be read beside them. Z3 at 125 % / 150 % extends across the focal band and is the one element that must never be occluded by the art: it carries the slate fill too (Component Inventory). If art-director sets a narrower focal band, only the 150 % row changes.
States & Variants
| State / Variant | Trigger | What Changes |
|---|---|---|
| Default | launch, ≥ 1 world exists | full column; first focus Continue |
| Empty / first run | no save files on this machine | Continue hidden; first focus Host…, whose list holds only the New world row; nothing else changes (a brand-new friend most often presses Join anyway) |
| First-launch notice | photosensitivityNoticeShown false | the one-time photosensitivity panel (#3) over everything; OK dismisses and writes the flag; shown before any other interaction |
| Return notice — host ended | arrived from a session the host ended | Z3: "the session has ended — everything is saved" |
| Return notice — you left | arrived via pause → Leave | Z3: "you left the session — rejoin any time" |
| Return notice — connection lost | disconnect / host vanished | Z3: "the connection was lost — you can rejoin when it's back" |
| Controls reset | the reset floor completed | Z3: "controls were reset to defaults" |
| all Z3 notices | — | one line, #6 tone, cleared on the next input; only one shows at a time (latest wins) |
| World list open | Host… | centred #2-in-#3 panel; focus on the most recent world; rows show auto-name + last-played; a version-refused save = dashed row, its refusal reason (#6) shown on focus in the panel footer — refused worlds stay listed |
| Opening world | Continue / a world row / New world | column and panel inert (dashed); Z3: "opening the world…"; no spinner (seconds-scale, and a bar would imply a promise); then into the session as host |
| Recovered save | ADR-0004's load fell back to the previous good file / a backup | Z3 on arrival in the list: "an older good copy of this world was opened — recent moments may be missing" (a filesystem fact, stated once) |
| Join panel (provisional) | Join, playtest build | centred #3 panel: six join-code steppers (#27), Connect, Back; first focus = the first character |
| Connecting | Connect | panel inert; "connecting…"; failure → tone-safe reason ("no session answered that code"), back to the panel; success → into the session (late-join sync, netcode Rule 4) |
| Reduced motion ON | the setting | the tide loop holds a still frame; only the slow tint crossfade remains; all panel motion per #3 (cuts) |
No tutorial, locked or premium states; no platform variants (PC only). Loading exists only as Opening world / Connecting above — the screen itself reads local files and shows instantly.
Interaction Map
Input scope (technical-preferences): Keyboard / Mouse primary, Gamepad partial. There is no world behind this screen, so the UI map is live throughout and no capture handoff occurs. This is a map state ADR-0007 §3 does not enumerate (its rule (c) says UI is enabled only while a panel is open, and Player is enabled at boot): the title screen needs UI enabled standing, and Player present but with only Menu consumed — the reset floor reads Menu, which is a Player-map action (§1). Flagged as an ADR-0007 §3 clarification for ADR-0014's panel manager (Open Question 10) — not a redesign, but it must be written down before the first UI story.
| Component | Action | KB/M | Gamepad | Feedback | Outcome |
|---|---|---|---|---|---|
| any button | Focus | hover (hover == focus, #4) / ↑ ↓ | UI.Navigate | 3 px outline + inset tick | clears any Z3 notice |
| Continue | Press | Enter / click | Submit (South) | column inert; Z3 "opening the world…" | hosts the most-recent world |
| Host… | Press | Enter / click | Submit | the world list opens instantly (#3) | Z5 panel, focus on the most recent world |
| Join | Press | Enter / click | Submit | the join panel opens (playtest build) | flow owned by ADR-0008 |
| Settings | Press | Enter / click | Submit | the settings panel opens on the stack | settings.md; its Back restores focus here |
| Quit | Press | Enter / click | Submit | instant | desktop |
| world row | Focus / Press | hover, ↑ ↓ / Enter, click | Navigate, Submit | dashed rows show their refusal reason on focus (#6); Submit on a dashed row does nothing but re-assert the reason | opens that world |
| New world row | Press | Enter / click | Submit | same "opening the world…" | rolls a seed; the save system creates one file |
| any Z5 panel | Close | Esc / click Back | East / Submit on Back | 100–150 ms fade | back to the column, focus restored to the button that opened it |
| join-code steppers (provisional) | Step a character | ← / → ; type / paste (convenience) | Navigate ← / → | the character replaces; wraps (no dashed ends — the #27 deviation) | — |
| join-code steppers | Move between characters | ↑ / ↓ / Tab-equivalent via focus | Navigate ↑ / ↓ | focus tick moves | — |
| reset floor | Hold | Esc held 3 s | Start held 3 s | no visual until it completes — it must work when the screen is unreadable or every binding is wrecked | overrides wiped + the Z3 line |
Focus adjacency — first focus = Continue (or Host… in the empty state):
| From | ↑ | ↓ | ← / → |
|---|---|---|---|
| Continue | nothing | Host… | nothing |
| Host… / Join / Settings | the previous button | the next button | nothing |
| Quit | Settings | nothing | nothing |
| world row n (in panel) | row n−1 (row 1 → nothing) | row n+1 (last → New world → Back) | nothing |
No wrap — a deliberate deviation from #2's "wrap at both ends" (stated per #21's precedent of declaring every edge): wrapping from New world back to row 1 would put world creation one keypress from the top of the list.
| Back (in panel) | the last row | nothing | nothing |
| Join panel | character 1 … 6 → Connect → Back (↑ / ↓) | the reverse | ← / → step the focused character (#27) |
Five stops in the column, no traps. Esc / East closes any open panel and does nothing on the bare column — there is nothing above the root to go back to (ruled 2026-08-31: a "back" at the root would have no destination, and Quit is a deliberate press, never an Esc away).
Esc short-press vs the 3 s hold (ruled 2026-09-01): the two never race. A panel close fires on the release edge, and only if the hold did not complete; if the 3 s threshold passes, the floor resolves (overrides wiped, Z3 line) and the release is consumed — the panel stays open, so the player sees the reset land in context. A short press with a panel open closes it as usual; a short press on the bare column does nothing. Same press-vs-release grammar as Demolish (ADR-0007's 2026-08-31 amendment).
Events Fired
No analytics system exists; every event is local. Nothing crosses the wire until a session starts.
| Player Action | Event | Payload | Persistent? |
|---|---|---|---|
| launch | TitleShown | first-run? | no |
| photosensitivity OK | NoticeAcknowledged | — | yes — settings.json (photosensitivityNoticeShown) |
| Continue / a world row | HostSessionRequested | saveId | the save system opens the file (ADR-0004 load pipeline); this screen writes nothing |
| New world | NewWorldRequested | rolled worldSeed | yes — needs a CreateWorld(seed) → worldFolder operation on the save system; ADR-0004 §1 mints worldId "at world creation" but names no owner and exposes no such API (Open Question 9, amendment (d)) |
| Join → Connect | JoinSessionRequested | join code (provisional — the payload is ADR-0008's to fix; code assumes a resolving layer, an address does not: OQ 2) | no — ADR-0008 owns the flow |
| Settings | PanelOpened(Settings) | opener = title | no |
| Quit | QuitRequested → Application.Quit() | — | ADR-0004's wantsToQuit latch runs, but no session is open so the flush queue is empty and it returns immediately |
| reset floor completes | BindingsReset via IRebindService.ResetToDefaults() (ADR-0007 §4) | — | yes — bindings.json |
| focus moves; panel open / close | none (deliberate) | — | no |
Flagged for architecture: NewWorldRequested is the one action on this screen that creates persistent world state. It must belong to the save system (save Rule 1: one world = one host-owned file, never merged; no merge path may ever exist) — but no document assigns seed rolling or world creation to anyone, and SavePersistence exposes no CreateWorld. Amendment (d), Open Question 9. The UI never rolls a seed.
Transitions & Animations
Art bible §7.5 binds the UI: 100–150 ms ease-out only; nothing animates that gates an input. The tide loop is world-art, not UI, and follows §4.3's curve.
| Moment | Motion | Reduced motion ON |
|---|---|---|
| App → title | fade from black 150 ms; the tide loop starts mid-cycle (never from a "beginning" — the tide has no start) | cut; still frame |
| Panel open (list / join / notice) | instant (#3) | same |
| Panel close | 100–150 ms ease-out fade | cut |
| Title → session | fade to black 150 ms, then the world — the loop's current tint carries into the first frame (the one deliberate continuity flourish: you fall into the tide you were watching) | cut |
| Z3 notice appear / clear | instant, no slide | same |
| Tide loop | slow continuous amber ↔ violet cycle, ≈ 40 s, no parallax drift | still frame; tint crossfade only — luminance-stable |
| Reset floor completing | no progress indicator during the hold (by design — it must work when nothing renders); the Z3 line is the only feedback | same |
No spinner anywhere (Opening world / Connecting are text states), no bounce, no splash chain, no press-any-key gate.
Data Requirements
| Data | Source System | Read / Write | Notes |
|---|---|---|---|
world list — id, auto-name (creation date), last-played, saveFormatVersion / configVersion compatibility | save system — requires three ADR-0004 amendments (Open Question 9): (a) a SaveHeader ReadHeader(worldFolder) operation that parses the header keys without running the load pipeline; (b) createdUtc and (c) lastPlayedUtc added to §1's document and to save Rule 2's "what persists" list. None of these exists today — Load() is currently whole-pipeline-only, and the save format carries no timestamps at all | Read | on entering the list; must never run the load pipeline (that is what pressing the row does). Filesystem mtime was rejected: it does not survive the copy that save Rule 1's fork case is about |
| most-recent world (for Continue) | same — sorted by lastPlayedUtc (amendment (c) above) | Read | on launch; absent → the empty state. Until the amendment lands there is no source for "most recent" — the list order and Continue's target both depend on it |
| version-gate verdict + refusal reason | save system (save Rule 4 — refuse loudly, never partial-load, never silently migrate) | Read | per row; drives the dashed row + its #6 reason |
| recovered-load fact (a backup / the previous good file won) | save system (ADR-0004) | Read | drives the "older good copy" notice |
photosensitivityNoticeShown | settings service (settings.json) | Read / Write | one bit — the only write this screen owns directly |
| session-end cause (host ended / you left / connection lost) | netcode session, handed over on the return transition | Read | transient; drives the Z3 notice; never persisted |
| binding-override reset | IRebindService.ResetToDefaults() (ADR-0007 §4) | Write (trigger) | bindings.json; the operation belongs to the rebind service |
| build version + commit | build metadata | Read | the Z4 string |
| labels, notices, reason strings | string table (L10n) | Read | — |
Nothing updates per frame. The UI owns no game state; the single bit it writes is a UI preference, and world creation belongs to the save system.
Accessibility
Tier: Standard (design/accessibility-requirements.md).
- Keyboard-only: complete. Gamepad-only: complete for every element — including the provisional join panel, whose six #27 steppers are fully
Navigate-operable (ruled 2026-09-01, replacing an earlier KB/M-only address field). The project therefore has zero gamepad-parity exceptions, which is what technical-preferences ("every action reachable via gamepad"), ADR-0007's constraint list and the a11y doc's Gamepad-only-session test row each require without an exception clause. No Known-Intentional-Limitation row is needed. If ADR-0008 later introduces a typed address, that would be the violation — the code form exists precisely to avoid it (Open Question 2). - Focus: 3 px outline + inset tick (#4); first focus Continue (Host… when empty); exactly one focused element; every panel restores focus to the button that opened it.
- Colour never alone: a refused world row = dashed (#1 disabled grammar) + its reason on focus; focus = weight + tick; every notice is text.
- Text: buttons and rows Medium 16–18 px; notices, timestamps and the version string Small 13–14 px; ≥ 4.5:1 — including over the tide loop: the button column, the Z3 notice slot and the Z4 version string all carry the slate panel fill, so contrast never depends on the animation phase (corrected 2026-09-01 — Z3 and Z4 sit directly over the art and had no stated mitigation; verify at both tint extremes at theme lock, AC-11).
- Hit targets: 320 × 48 px buttons; 44 px rows.
- Photosensitivity: the one-time notice this screen shows is the a11y doc's committed pre-launch notice; the tide loop is a ≈ 40 s crossfade, nowhere near the ≤ 3/s flash cap.
- Reduced motion: still frame + tint crossfade only (Transitions). The a11y doc's reduced-motion test row is a closed list ("anything else still moving is a failure"), so this ruling was added to its keep-list in the same change (2026-09-01) rather than deferred — a ≈ 40 s luminance-stable crossfade is exactly the class of motion reduced-motion exists to keep.
- Lockout-proof: the reset floor lives here and works with no usable bindings and with an unreadable screen — no visual is required for it to function.
- Cognitive: five buttons, one column, no timed elements at all (the 3 s reset hold is opt-in, repeatable, and costs nothing to abandon).
- Screen reader: out of scope (a11y doc, Known Intentional Limitations).
Localization Considerations
| Element | Limit (EN source) | Layout constraint | Expansion risk |
|---|---|---|---|
| Join code | 6 characters, not localised (A–Z 0–9, ambiguity-free alphabet — no O/0, I/1); the payload is provisional (code vs address — ADR-0008, OQ 2) | six steppers in the 420 px panel | LOW |
| Continue label incl. world name | ≤ 300 px rendered, one line, ellipsised ("Continue — World of 8/29/2026" ≈ 250 px at Medium) | 320 px button | HIGH |
| Other button labels | ≤ 18 chars ("Settings", "Quit", "Host…", "Join") | 320 px, one line | LOW |
| Z3 notices | ≤ 56 chars, 2 lines allowed (EN max = 52 ≈ 470 px) | 560 px slot (Layout Zones), left-anchored with the column | MEDIUM |
| World-row auto-name | a localised template "World of {0}" (never concatenation — some languages do not compose noun + date that way), where {0} is the "d" short-date specifier in the current culture (createdUtc.ToLocalTime().ToString("d", CultureInfo.CurrentCulture) — "29-08-2026" in nl-NL, "8/29/2026" in en-US, "2026/8/29" in ja-JP). Budget ≤ 260 px rendered, not a character count — CJK glyphs are ≈ double-width, so a char budget misleads | 420 px panel row | HIGH (template + locale date) |
| Refusal reason (#6) | ≤ 56 chars, 2 lines | panel footer | MEDIUM |
| Photosensitivity body | ≤ 240 chars, wraps freely | 420 px panel (grows to content) | LOW |
| Version string | not localised (build id) | Z4 | LOW |
Dates and times are the one locale-formatting requirement in the project — world names and last-played stamps use the "d" standard specifier with CultureInfo.CurrentCulture, never a hardcoded or custom pattern. (The earlier EN example "World of 29 Aug" was itself a custom pattern and contradicted this rule; corrected 2026-09-01 — the EN rendering is "World of 8/29/2026".) No currencies, no RTL commitment at MVP.
Acceptance Criteria
- [PLAYMODE] The screen is interactive within 1 frame of the fade-in completing; no splash chain and no press-any-key gate exists; focus is on Continue (or Host… when no world exists).
- [MANUAL] With ≥ 1 world: Continue names the most-recent world and one press opens it. With none: Continue is absent and Host…'s list contains only New world.
- [PLAYMODE] New world → exactly one new save file appears host-side with a fresh seed; on return the world list shows it as most-recent.
- [MANUAL] A save with an unknown or future
saveFormatVersionappears as a dashed row that states its reason on focus and cannot be opened (Submit only re-asserts the reason); the game never partial-loads it and never migrates it silently. - [MANUAL] Returning from a host-ended session shows "the session has ended — everything is saved"; from your own Leave, "you left the session — rejoin any time"; from a lost connection, the rejoin line. Each clears on the next input, and only one is ever shown at a time.
- [PLAYMODE] First launch shows the photosensitivity notice exactly once; OK dismisses it and it never returns — including after a settings change, a session, and a restart.
- [MANUAL] Holding the default Esc (or Start) for 3 s wipes all binding overrides and shows the reset line — verified after deliberately rebinding several actions, and verified again with a
bindings.jsonthat leaves the screen otherwise unnavigable. With the world list open: a short Esc closes the panel; a 3 s hold resets the bindings and leaves the panel open (the release is consumed). - [MANUAL] Gamepad-only: every element is reachable with visible focus — including entering a full six-character join code and connecting, with no keyboard touched; East closes any open panel and does nothing on the bare column. No exception exists.
- [MANUAL] Settings opens from here and its Back returns here with focus restored to the Settings button.
- [MANUAL] Reduced motion ON: the tide loop is a still frame with only the tint crossfade; no parallax; panel motion is cuts.
- [MANUAL] 1080p and 1440p at 100 / 125 / 150 %: the column stays left-anchored and its x-range matches the Z-clearance table (Layout Specification) at each scale — the 150 % overlap into the focal band (48 px) and the 21 px column-to-panel gap are expected, not defects; all text measures ≥ 4.5:1 at both tide-tint extremes (the column, the Z3 notice slot and the Z4 version string each carry the slate fill); no clipping, and no overlap with the wordmark.
Open Questions
- Player journey map not yet created. Template available at
.claude/docs/templates/player-journey.md. Run/ux-designPhase 2b or create it manually to establish player context for this screen. - Join flow → ADR-0008 (after the friend playtest), restated 2026-09-01 to match the current design: the provisional panel is a gamepad-operable 6-character code on six character steppers. ADR-0008 decides whether a code-issuing lobby / relay layer exists at all — a code presupposes one; an address does not, and netcode Rule 7 reserves that choice. If it lands on a bare IP transport, the same six steppers become address entry (alphabet and payload change; the mechanism and the parity guarantee do not) — never a raw text field, which would reintroduce the gamepad gap this design removed. The connect-failure copy ("no session answered that code") is provisional with the payload.
- World naming: auto-named from the creation date at MVP; renaming needs a text-input pattern, which does not exist — tracked as the "Text input field" row in the pattern library's Gaps table (added 2026-09-01), Tier 1. The join panel deliberately does not need it.
- Fixed vs rotating host is still a group decision (netcode Rule 1); if save-anyone-can-host lands (Tier 1 first-to-slip), this screen needs a "whose world is this" read on each row.
- Should the tide loop reflect the actual current tide phase of the most-recent world (computable without loading — the schedule is pure
f(seed, wallClock))? Ruled: generic loop at MVP (2026-08-31); revisit as polish with art-director + tide — it would also need an empty-state rule. - Version-string format and where the commit hash comes from in a shipped build — devops / the build pipeline.
- ~~Reduced-motion keep-row for the tide loop~~ — done 2026-09-01: added to the a11y doc's reduced-motion row in the same change (it contradicted that row's closed list until then).
- Art bible §2 row 6 specifies the title mood as a gradient "held static", energy Low; this spec renders it as a ≈ 40 s cycle (the near-still reading). Also: the focal band used by the Z-clearance table (centre 40 %, x = 576–1344) is this spec's assumption. Both → art-director to confirm.
- ADR-0004 amendments this screen requires (none exist today): (a)
SaveHeader ReadHeader(worldFolder)— header parse without the load pipeline; (b)createdUtcand (c)lastPlayedUtcin §1's document + save Rule 2's persists-list; (d) aCreateWorld(seed) → worldFolderoperation with a named owner for seed rolling. Without (b)/(c) the world list cannot name or sort worlds; without (d) New world has no owner. - ~~ADR-0007 §3 clarification~~ — CLOSED 2026-09-01: ruled by ADR-0014 §5 and applied to ADR-0007 §3 as a fourth map state (title:
UIenabled standing,Playerpresent, onlyMenuconsumed), derived from an explicitUiContextset at scene load. Original note: the title screen needsUIenabled standing withPlayerpresent but onlyMenuconsumed — a map state §3 does not enumerate (its rule (c) tiesUIto open panels). For ADR-0014's panel manager.