DRIFTLANDS co-op survival on drifting tides — studio progress

UX Spec: Settings — Controls, Display, Audio, Accessibility

Status: Reviewed — APPROVED (/ux-review pass 3 2026-08-31: 0 blocking, 4 advisory — applied; pass 1 3 B / 8 A and pass 2 2 B / 4 A applied the same day) · v0.4
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: reached from the title screen and from pause (same screen, same state; the in-game variant returns to pause)
Requirements input: design/accessibility-requirements.md — the ten committed controls (UI scale, reduced-motion, flash-reduction, HUD opacity, mono, Music / SFX / UI volume, gamma + calibration image, toggle-hold revive) + full rebinding; ADR-0007 §4 (rebind flow, swap-on-confirm, protected actions, reset floor, bindings.json); pattern #20 Rebind Row; ui-hud quick-spec Rule 4 (Movement / Camera row) + queue item ⑤; art bible §7; Tier 1 stubs reserved: Help panel, deadzone / aim-sensitivity sliders
Patterns used: #1 Button · #3 Panel/Modal · #4 Focus & Nav · #6 Reason String · #20 Rebind Row · #25 Slider, #26 Toggle, #27 Option Cycler (new — defined by this spec; the pattern-library gap that blocks the accessibility commitments)
Related specs: design/ux/hud.md (opacity scope; UI-scale mechanics OQ 1), design/ux/build-mode.md (#31 single-press alternative), design/ux/inventory.md (panel conventions); title-screen and pause specs do not exist yet — named as dependencies in Navigation


Purpose & Player Need

Settings is where a friend makes the game fit their hands, eyes, ears and screen — and then never thinks about it again. The player arrives wanting to rebind a key, fix the volume balance, or turn on the option that lets them play at all (reduced motion, flash reduction, toggle-hold revive, UI scale). It must be reachable from anywhere (title and pause), operable entirely on either scheme, honest about conflicts (swap-on-confirm, never silent), impossible to lock yourself out of (protected Menu, the reset floor), and free of ceremony: every change applies the moment it is made and persists without an Apply button. It is the only screen in the game with sliders and toggles, so it also defines those controls for the project (patterns #25–27).


Player Context on Arrival


Navigation Position

Title → Settings; Gameplay → Menu (Esc / Start) → Pause → Settings. One screen, one state, two return targets. A captured panel (#3; UI map live) with a tab bar — the project's only tabbed screen. Title-screen and pause specs do not exist yet; this spec fixes only that both offer a "Settings" entry and that Back returns to whichever opened it. Sub-flows: Rebind (inline listening state on a row, #20); the title-screen reset floor (hold the default Menu control 3 s — lives on the title screen, ADR-0007 §4) is mirrored by this screen's Reset button.


Entry & Exit Points

Entry Source Trigger Player carries this context
Title screen"Settings" entry (UI.Submit / click)current settings.json + bindings.json; active scheme selects the Controls sub-tab
Pause (in-game)"Settings" entrysame; plus the live world behind it (HUD visible per #3 layering; panels never pause the tide)
Last-tab memory—the last-visited tab is remembered for the session (default: Controls); not saved
Exit Destination Trigger Notes
Whichever opened it (title / pause)Back button, UI.Cancel (Esc / East), Menu (Esc / Start via the panel stack)all changes already applied + written; nothing to confirm
Same screentab switchno state loss; a rebind in listening state is cancelled by a tab switch
Gameplay (force-close)host quit / disconnect while in pause-settingssettings already persisted; bindings.json untouched mid-listen (a cancelled listen writes nothing)

No one-way exits. The only persistent effects are per-change writes to settings.json (new — this spec; persistentDataPath/Driftlands/settings/) and bindings.json (ADR-0007 §4) — both per machine via ADR-0004's atomic replace (temp + File.Replace) without the save's rolling .1/.2/.3 backups or F_FULLFSYNC — the same trim ADR-0007 §4 applies to bindings.json: a lost settings write is not data loss, and a corrupt file recovers through the settings.bad.json path (States), not a backup — never in the world save (ADR-0007 §4; ADR-0004's save defines no settings table).


Layout Specification

Information Hierarchy

Absent by design: an Apply / Save button (live-apply, ruled), profiles, per-player settings (settings are per machine), search, a "restore previous" undo (every change is one step to reverse by hand).

Layout Zones

Arrangement: one centred captured panel (#3), 720 × 520 px @100 % scale (1080 × 780 at 150 % — fits 1080p with margins; ruled 2026-08-31). Height = 48 header + 40 tabs + 360 rows + 56 footer + 16 vertical padding (8 top, 8 bottom). Settings is the project's only centred panel: it is reached from the title or from pause, never from a world verb, so the bag's right-of-centre anchor (which keeps the player's own character visible) buys nothing here and 720 px at that anchor would clip at 150 % on 1080p. Width = 16 pad + 400 px label column + 24 gutter + 260 px control column + 20 pad.

All px at 100 % UI scale (pattern-library scale convention); the panel, its columns and its rows scale together.

Component Inventory

Every row is label · control · optional consequence line. Three control patterns are new — defined by this spec (#25 Slider, #26 Toggle, #27 Option Cycler); #20 Rebind Row and #1 Button already exist.

Control anatomy (new patterns — lifted to interaction-patterns.md as #25–27 when this spec is drafted; ruled 2026-08-31):

Tab Row (player-facing label) Control Range · default Consequence line Source
ControlsScheme#27Keyboard / Mouse · Gamepad — defaults to the active scheme on open; session-only, never saved—ADR-0007 §3 (scheme is presentation)
ControlsMove — KB/M: four part rows Move · up / down / left / right (the 2D composite's parts); Gamepad: one row (stick glyph)#20——ADR-0007 §1; ui-hud Rule 4
ControlsAim stick — Gamepad tab only#20—— (the KB/M tab simply has no aim row — an unrebindable pointer needs no explanation, the #27 "no cue" principle; OQ 10)ADR-0007 §1 (aim sources); OQ 10
ControlsStrike · Dash · Interact / Revive (hold) · Demolish · Inventory · Build mode · Menu 🔒#20—conflict prompt inline under the row: "also bound to Dash — swap?" + Confirm / Cancel (#1 Secondary ×2); Menu shows the lock glyph and no buttonADR-0007 §1 / §4; #20
ControlsPlace · Cancel · Previous structure · Next structure (Build map, under a "Build mode" divider)#20each row edits the action's primary binding for the scheme (KB/M: Place = LMB, Cancel = RMB, Previous = Q, Next = R — ADR-0007 §1 row split 2026-08-31); the fixed secondaries — Esc on Cancel, scroll wheel on Previous / Next — are not shown and not rebindable—ADR-0007 §1 / §4 amendment (fixed secondaries, 2026-08-31)
Controls(footer) Reset to defaults#1 Secondary——ADR-0007 §4 reset floor mirror; #20 (amended: Secondary)
DisplayWindow mode (provisional)#27Fullscreen · Borderless · Windowed · default Borderless—none — OQ (ADR-0014)
DisplayResolution (provisional)#27the current monitor's mode list · default native—none — OQ (ADR-0014)
DisplayVSync (provisional)#26OFF · ON · default ON—none — OQ (ADR-0014)
DisplayBrightness#25−50…+50 %, step 5 · default 0calibration strip, shown while the row is focused (non-focusable, no controls): three patches at 5 / 50 / 95 % luminance + a silhouette figure on the darkest — "set until the left figure is just visible"a11y doc (gamma + calibration image); art bible §1 strip-to-black
DisplayHUD opacity#2550–100 %, step 5 · default 100"never dims your own health or a downed friend"hud.md (element-scoped; SHOULD-KEEP tier only)
AudioMusic · SFX · UI#25 ×30–100 %, step 5 · default 80— (the bus is audible while you move it; UI plays its click per step)a11y doc (three buses, no voice bus)
AudioMono audio#26OFF · ON · default OFF—a11y doc
AccessibilityUI scale#27100 % · 125 % · 150 % · default 100 %"HUD and panels scale together"a11y doc; hud.md; mechanism OQ 1 (ADR-0014)
AccessibilityReduced motion#26OFF · ON · default OFF"removes shakes, pulses and slides; telegraphs and dashes are unchanged"a11y doc keep-list
AccessibilityFlash reduction#26OFF · ON · default OFF"caps hit flashes at 3 per second"a11y doc; OQ 5 there
AccessibilityRevive: toggle instead of hold#26OFF · ON · default OFF"press once to start reviving, again to stop"a11y doc hold inventory (1); #10
all(footer) Back#1 Secondary——#3
all(footer) reason-string slot#6—refused rebinds, save failures, recovery notices (States)#6

Rulings folded into the table (2026-08-31): Interact and Revive are one row — they share E / West by design (ADR-0007 §1) and rebinding the row applies one override to both actions; §4's same-map conflict rule must exempt this pair (Open Questions → ADR-0007 / ADR-0014). Shared controls survive rebinding by control identity (ruled 2026-08-31, ADR-0007 §3 amendment): Strike / Place share LMB and Demolish / Cancel share RMB by design; if a rebind moves one half away, build mode's shared-control routing follows whatever Player action now holds the control a Build action shares — nothing on this screen pairs those rows. Fixed secondary bindings (ruled 2026-08-31, ADR-0007 §4 amendment): ADR-0007 §1's rule (b) is a minimum, and two KB/M actions ship with a second binding — Build.Cancel RMB / Esc and PickPrev / PickNext Q · R / scroll (§1's single pair row named only "Q / scroll" and left PickNext without a key; split 2026-08-31 into Q = Previous, R = Next, scroll as the pair's secondary). Those secondaries (Esc, scroll) are fixed: never displayed, never rebindable, never overridden — Esc because it is Menu's protected control (so Cancel can never leave Esc and build mode's "Esc exits then opens pause" in build-mode.md always holds), scroll because it is a pointer affordance like AimPointer. Every rebind row therefore edits exactly one binding — the scheme's primary — and "one rebindable binding per action per scheme" is true of the default set. Window mode / Resolution / VSync are provisional: no GDD or ADR commits them; a PC build cannot ship without them; ADR-0014 owns confirming or removing them. AimPointer (mouse position) is not rebindable — there is nothing to rebind a pointer to; AimStick is (left-handed stick swap).

ASCII Wireframe

Controls tab (Keyboard / Mouse scheme, mid-rebind) and Display tab. * = the one focused element (3 px outline + inset tick ─▸); ▒ = scroll indicator.

┌─ Settings ───────────────────────────────────────────────────────────┐
│ ▌Controls▐   Display     Audio     Accessibility                     │
├──────────────────────────────────────────────────────────────────────┤
│ Scheme                                   ◂  Keyboard / Mouse  ▸      │
│ Move · up                                  [ W ]        Rebind       │
│ Move · down                                [ S ]        Rebind       │
│ Strike                                     [ LMB ]      Rebind       │
│ Dash  ─▸*                                  [ press a key… 5 ]        │ ← listening
│ Interact / Revive (hold)                   [ E ]        Rebind       │
│ Demolish                                   [ RMB ]      Rebind       │
│ Menu                                       [ Esc ]      🔒           │▒
├──────────────────────────────────────────────────────────────────────┤
│ [ Back ]                                        [ Reset to defaults ]│
└──────────────────────────────────────────────────────────────────────┘

┌─ Settings ───────────────────────────────────────────────────────────┐
│  Controls   ▌Display▐    Audio     Accessibility                     │
├──────────────────────────────────────────────────────────────────────┤
│ Window mode                              ◂    Borderless    ▸        │
│ Resolution                               ◂   1920 × 1080    ▸        │
│ VSync                                        [ OFF |▐ON▌ ]           │
│ Brightness  ─▸*                          ──────●──────   +10 %       │
│   ▓▓▓ ▒▒▒ ░░░ ♟   set until the left figure is just visible          │
│ HUD opacity                              ────────●────    80 %       │
│   never dims your own health or a downed friend                      │
├──────────────────────────────────────────────────────────────────────┤
│ [ Back ]                                                             │
└──────────────────────────────────────────────────────────────────────┘

The Controls list continues below the fold (Inventory, Build mode, then the Build-map divider with Place / Cancel / Previous structure / Next structure); the Gamepad scheme replaces the four Move part rows with one stick row and adds Aim stick. Checked at 150 %: 1080 × 780 on 1080p leaves 420 px of margin on each side and 150 px above and below; the row count per screen is unchanged because rows scale with the panel.


States & Variants

State / Variant Trigger What Changes
Default — titleopened from the title screentitle art behind; Back → title; first focus = the first row of the remembered tab (default Controls → the Scheme cycler)
Default — pauseopened from pausethe live world + closed-list HUD behind, still running; Back → pause; otherwise identical to the title variant
Controls · scheme sub-statethe Scheme cyclerKB/M: four Move part rows, no aim row; Gamepad: one Move row + Aim stick. Opens on the active scheme (InputFrame.Scheme); a device switch mid-panel does not flip the cycler (the player may be deliberately editing the other scheme); prompt glyphs elsewhere follow the scheme per #4
Gamepad tab, no gamepad connected (empty)Scheme = Gamepad while no gamepad is presentrows show the stored bindings; every Rebind is dashed (#1 disabled) with the reason "no gamepad connected" in the footer slot on focus; listening is impossible
Rebind — listeningRebind pressed (row Submit / click)the row's glyph cell shows "press a key… 5" counting down once per second (#20: 5 s → cancel); the interactive rebind captures the next input on that scheme's devices; UI.Cancel (Esc / East) cancels the listen, not the panel — on either device: the scheme's device exclusion (WithControlsExcluding, ADR-0007 §4) applies to capture only, so Esc aborts a gamepad listen and East aborts a keyboard listen without being captured; a pointer click outside the row cancels the listen, and if the click landed on a tab or Back that action then applies (the Entry & Exit "tab switch cancels a listen" rule)
Rebind — conflictthe captured input is already bound in the same map + schemeboth rows highlighted (3 px outline — weight, no colour); inline prompt under the listening row: "also bound to Dash — swap?" with Confirm / Cancel; focus lands on Confirm (the player chose that key); Confirm exchanges: the rebound action takes the new control and the colliding action takes the control just vacated (swap-on-confirm = exchange — ruled 2026-08-31, ADR-0007 §4 amendment); Cancel keeps both unchanged. The Interact / Revive pair never conflicts with itself (one row, one override for both); when the pair is the rebound row and collides with a third action, the exchange gives that action the pair's single vacated control (two ids move out, one id moves in — one atomic result). A collision with a protected action (Menu) never reaches this prompt — see Refused
Rebind — refusedthe captured control belongs to a protected action (Menu) — refused before any prompt, no Confirm / Cancel offered, Menu never moves and the reset floor's default control stays true. Protection is keyed on Menu's control regardless of who else shares it: Esc is also Build.Cancel's fixed secondary binding (below), and a capture of Esc is still refused. (An empty-binding refusal cannot arise from the default set — every action has exactly one rebindable binding per scheme; it remains only as the rebind service's safety net.)nothing changes; the row leaves listening with its old glyph; footer reason string (#6), glyph-templated from the Menu entry of IRebindService.Display(scheme) (the shape of BindingDisplay is ADR-0014's to pin — OQ 4) so it names the current scheme's control and follows keyboard layout: "Menu always stays on {glyph}" (KB/M: "…on Esc"; Gamepad: "…on Start") · "that would leave Dash with no key on this scheme"; persists while the row is focused
Reset confirmedReset to defaultsevery Controls row snaps to defaults in one frame; bindings.json rewritten; no dialog (no confirmation dialogs exist at MVP, #3). Honest cost: a reset discards every rebind at once and reversing it means redoing them by hand — which is why the button is Secondary weight, sits alone at the far end of the footer, and is reached only by ↓ from the last row or → from Back; it cannot be hit while scrolling or stepping a control
Settings recovered (error)settings.json was malformed at boot → defaults applied, file renamed settings.bad.json (the same recovery ADR-0007 §4 gives bindings.json)the first open after recovery shows one footer notice: "settings were reset — the old file is kept" (41 chars; the file name is settings.bad.json, stated in the log); cleared on the next change or on close
Save failed (error)the atomic write of settings.json / bindings.json failsthe value stays applied for the session; footer: "couldn't save settings — changes last until you quit"; the write is retried on the next change
Reduced motion ONthe toggle itselfthis panel's own motion drops out immediately: tab swap and close become cuts; the slider knob snaps instead of easing
UI scale changethe cyclerthe panel re-lays out in place at the new scale on the next frame; focus stays on the UI-scale row
Resolution list unreadablethe platform returns no display modesResolution shows the current mode alone with both arrows dashed
Downed / Dead while in pause-settingscombat statenothing — Downed = ordinary settings (the standing precedent: UI never invents a gate)
Force-closehost quit / disconnect while in pause-settingsthe panel closes with the session; nothing left to persist (every change was written when made; a live listen writes nothing)
Loading—none: settings.json is read at boot and bindings.json at adapter start (ADR-0007 §4); the screen never fetches

No tutorial, locked, premium or platform-variant states exist (PC only; nothing on this screen is progression-gated).


Interaction Map

Input scope (technical-preferences): Keyboard / Mouse primary, Gamepad partial — every action below is reachable on both. The panel captures the UI map (ADR-0007 §3); Menu and Inventory are the only Player edges that survive capture, and Inventory does nothing here (it only closes the bag it opened, #3).

Component Action KB/M Gamepad Feedback Outcome
any elementFocushover (hover == focus, #4) / ↑ ↓UI.Navigate (d-pad / left stick; key-repeat 300 → 100 ms)3 px outline + inset tick; the list scrolls to keep the focused row fully visible—
Tab barSwitch tab← / → on the bar, or click a cellNavigate ← / → on the baractive cell fills + underline moves; Z3 swapstab changes on focus; a live listen is cancelled first
#25 SliderStep← / → ; drag the knob ; click the trackNavigate ← / →knob moves one step; readout updates; the bus / effect applies at once (the UI click plays on the UI bus, so the UI slider is audible per step)SettingChanged + write
#26 ToggleFlipEnter / click / ← / →Submit / Navigate ← / →the cell fill swaps to the other wordSettingChanged + write
#27 CyclerStep← / → ; click an arrow ; Enter = forwardNavigate ← / → ; Submit = forwardvalue replaces; the arrow at an end goes dashed; at the end Submit is a silent no-opSettingChanged + write (Scheme: session-only, no write)
#20 Rebind rowStart listeningEnter on the row / click RebindSubmit on the row"press a key… 5" countdownlisten state
#20 listeningCapturethe next key / mouse buttonthe next gamepad controlglyph replaces; the row exits listeningBindingChanged + bindings.json write; or the conflict prompt
#20 listeningCancelEsc / a click outside the row / 5 sCancel (East) / 5 sglyph restoredno write
conflict promptConfirm / Cancel← / → between the two, Enter ; click ; Esc = CancelNavigate ← / →, Submit ; East = Cancelrows un-highlightswap + write, or nothing — the prompt is modal within the row: focus cannot leave it until Confirm or Cancel
Reset to defaultsPressEnter / clickSubmitrows snap; one click soundBindingsReset + write
BackCloseEnter / click / Esc from anywhereSubmit / East from anywhere100–150 ms fadereturns to the opener (title / pause)
MenuCloseEscStartsamecloses via the panel stack — protected; never opens pause from here (ADR-0007 §3)
scroll indicatorScrollwheel over Z3 ; drag the indicator— (focus-driven scrolling only)the list moves— (the wheel never adjusts a slider — accidental-change guard, ruled 2026-08-31)

Nothing is pointer-only: every control has a Navigate / Submit route, and Rebind is the row's own Submit (unlike crafting's pointer-only Craft button, which mirrors a focused row). No hold exists on this screen.

Focus adjacency — first focus = the first row of the remembered tab (default Controls → Scheme):

From ↑ ↓ ← →
Tab barnothingfirst rowprevious tab (stops at Controls)next tab (stops at Accessibility)
Row nrow n−1 (row 1 → tab bar)row n+1 (last row → Back)the control's own ← (rebind rows: nothing)the control's own →
Conflict Confirm / Cancel (modal within the row while shown)nothingnothingConfirmCancel
Backlast rownothingnothingReset (Controls only)
Resetlast rownothingBacknothing

No traps: every element reaches the tab bar in ≤ (rows + 1) presses of ↑ and Back in ≤ (rows + 1) presses of ↓; Esc / East closes from anywhere except two row-local states: mid-listen (the first press cancels the listen, the second closes) and a conflict prompt (Esc / East is the prompt's Cancel; the prompt is dismissed only by Confirm or Cancel, so a stray Esc never closes the panel over an undecided swap). The calibration strip is non-focusable (no controls; it shows and hides with the Brightness row's focus).


Events Fired

No analytics system exists in the project; every event here is local, and nothing crosses the wire (settings are per machine; ADR-0007: input never leaves the machine).

Player Action Event Payload Persistent?
openPanelOpened(Settings) → panel manager SetUiCaptured(true) (ADR-0007 §3; ADR-0014 owns the manager)opener (title / pause)no
slider / toggle / cycler changeSettingChangedkey, oldValue, newValueyes — settings.json, atomic write per change (ADR-0004 writer)
— consumers of SettingChangedaudio mixer (Music / SFX / UI buses, mono fold); HUD presenter (hudOpacity, hud.md); UI Toolkit PanelSettings scale (UI scale — mechanism a11y OQ 1 / ADR-0014); reduced-motion + flash-cap flags (presentation); revive toggle-hold → ADR-0015's local revive input handling — changes how this client interprets the Revive Held read (press starts, press cancels); the 3 s channel and its four cancel causes stay host-timed and untouched (#10)——
Scheme cyclernone—no (session-only)
rebind captured / swap confirmedBindingChanged via IRebindService.RebindAsync (ADR-0007 §4)actionIds, scheme, newPath, exchangedActionId?, exchangedPath? — the Interact / Revive row carries two ids; nothing is ever cleared under exchangeyes — bindings.json
rebind cancelled / timed out / refusedRebindEnded(reason) — log onlyreason (cancelled · timeout · protected · would-unbind)no
Reset to defaultsBindingsReset via IRebindService.ResetToDefaults()all overrides, both schemesyes — bindings.json
tab switch, focus move, scrollnone (deliberate — no telemetry, no state)—no
close (Back / Esc / Menu)PanelClosed(Settings) → SetUiCaptured(false)—no

Flagged for architecture: both persistent files live in persistentDataPath/Driftlands/settings/, per machine, written by ADR-0004's atomic replace without its rolling backups or F_FULLFSYNC (deliberate — see Entry & Exit), outside the world save (asserted by ADR-0007 §4; ADR-0004's save schema defines no settings table — the enforcing Schema_* test is ADR-0014's, OQ 4). settings.json is a new file: ADR-0014 owns its schema and the write path; whether it appears in ADR-0006's data manifest at all (it is runtime-written like bindings.json, not shipped data) is an Open Question. The UI owns none of this state — it reads the applied values and asks the owning systems to change them.


Transitions & Animations

Art bible §7.5 binds: 100–150 ms ease-out only; nothing animates that gates an input.

Moment Motion Reduced motion ON
Enterinstant (≤ 1 frame, #3); the focus tick appears on the first element the same framesame
Exit100–150 ms ease-out fade; input is released on the close edge, not at animation end (#3)cut
Tab swapZ3 content crossfade 100 ms; the underline slides to the new cell in 100 mscut + snap
Slider stepknob eases 100 ms to the new step; the readout updates instantlyknob snaps
Toggle flipcell fill swaps 100 mscut
Cycler stepvalue replaces instantly (no slide — nothing to imply direction)same
Listening countdownthe number ticks 5 → 1 once per second; no pulse, no glowsame (nothing to remove)
Conflict highlightoutline weight 2 → 3 px on both rows, instantsame
Calibration stripappears on focus, disappears on focus leave, instantsame
UI scale changere-layout in one frame, no tween (a scale tween is motion for its own sake and would re-flow text mid-animation)same
Resetrows snap in one framesame

No screen shake, no bounce, no open delay, no confirm-hold. The only timed element on the screen is the 5 s listen timeout, and it is a safety cancel (#20), not an input window — the a11y doc's "no timed input that cannot be extended or toggled" clause is untouched.


Data Requirements

Data Source System Read / Write Notes (update · null)
the 13 setting values — windowMode, resolution, vsync (provisional); gamma, hudOpacity, musicVolume, sfxVolume, uiVolume, monoAudio, uiScale, reducedMotion, flashReduction, reviveToggleHoldSettings service — new, ADR-0014; file settings.json (persistentDataPath/Driftlands/settings/)Read / Writeread on open; written per change with ADR-0004's atomic writer. Missing file → defaults; malformed → defaults + settings.bad.json + one notice (States)
current bindings per schemeIRebindService.Display(scheme) (ADR-0007 §4)Readrefreshed on BindingChanged / BindingsReset; never empty (asset test: every action bound in both schemes)
rebind result · conflict · refusalIRebindService.RebindAsync(actionId, scheme, ct)WriteAwaitable; the conflict is surfaced for the prompt; a refusal carries its reason
resetIRebindService.ResetToDefaults()Writeall overrides, both schemes
active scheme (the Scheme cycler's default)InputFrame.Scheme — presentation-only (Driftlands.Presentation is the only permitted referencer, ADR-0007 §2)Readonce on open; never re-read to flip the cycler
gamepad presentInput System device list, via the adapterReadon device-change events; drives the empty state
display modes · current modeplatform (Screen) via the settings serviceReadon open; empty list → single entry, arrows dashed
calibration stripstatic art assetRead—
labels, consequence lines, reason strings, noticesstring table (L10n)Read—
last-visited tabthis panel, session memory—the one UI-owned datum; no game meaning; not saved

Nothing updates per frame. The UI owns no game state: it displays applied values and asks the owning system (settings service, rebind service, mixer, HUD presenter, PanelSettings, combat's local revive handling) to change them.


Accessibility

Tier: Standard (design/accessibility-requirements.md). This screen delivers ten of that document's commitments; the document stays the source of truth for each one's scope.


Localization Considerations

Element Limit (EN source) Layout constraint Expansion risk
Tab labels≤ 14 chars180 px cells at 100 % (full-bleed bar, 4 × 180 = 720); cells may size to content (min 120 px) — "Barrierefreiheit" (16) forces itHIGH
Row labels≤ 32 chars, 2 lines allowed (row → 64 px)400 px column, MediumHIGH — "Revive: toggle instead of hold" is already 30
Consequence lines≤ 64 chars, 2 lines allowed684 px, SmallMEDIUM
Cycler values≤ 16 chars260 px minus two 24 px arrowsHIGH — "Keyboard / Mouse" is 16
Toggle words≤ 5 chars eachtwo 60 px cellsHIGH — a language whose words exceed 5 uses its short form (FR "OFF / ON" is acceptable)
Button labels≤ 18 chars160–200 pxHIGH — "Reset to defaults" is 17
Reason strings / notices≤ 56 chars, 2 lines allowed — a deliberate relaxation of #6's ≤ 32 because the footer slot is 360 px, not a row's trailing slotfooter centre ≈ 360 px, SmallMEDIUM
Binding glyphsnot localised — Input System display names, which are keyboard-layout-aware (AZERTY shows Z where QWERTY shows W) — verify (OQ 9)80 px glyph cellverify
NumbersASCII digits, %, ×; no thousands separators—LOW

Every layout-critical string above is flagged for 40 % expansion; the two-line allowances are the accommodation. No RTL commitment exists at MVP (no RTL language is planned for a friend-group release).


Acceptance Criteria

5b. [PLAYMODE] On the Keyboard / Mouse scheme, rebind Strike to Esc → no prompt appears; footer reads "Menu always stays on Esc"; Strike stays LMB; Esc still opens pause from gameplay and still exits build mode. 5c. [PLAYMODE] Rebind Inventory to LMB, Confirm → Strike = Tab and Inventory = LMB; enter build mode, press LMB → a structure is placed and build mode stays active; the Cancel row shows RMB only and rebinding it never affects Esc.


Open Questions