DRIFTLANDS co-op survival on drifting tides — studio progress

ADR-0014: UI Stack — UI Toolkit Screens, Settings Service, Panel Manager

Status

Accepted

Accepted 2026-09-01 (/architecture-review 2026-09-01 — findings F1 and E17 applied; see Amendment History)

Date

2026-09-01

Engine Compatibility

Field Value
EngineUnity 6.3 LTS (6000.3)
DomainUI
Knowledge RiskHIGH (VERSION.md project risk) / MEDIUM for this domain (architecture.md §Engine Knowledge Gap: "USS strict parsing blocks import on invalid selectors")
References Consulteddocs/engine-reference/unity/VERSION.md, modules/ui.md (⚠️ dated 2026-02-13 but cites 6000.0 doc URLs — treated as directional only, not authoritative on 6.3), breaking-changes.md, deprecated-apis.md; live 6.3 docs (below)
Post-Cutoff APIs UsedPanelSettings.scaleMode / .scale / .referenceResolution / .screenMatchMode / .sortingOrder; UI Toolkit built-in runtime event system (no EventSystem); strict USS import
Verification RequiredV1 PanelSettings.scale and .scaleMode are settable at runtime and re-layout live (property list is doc-attested; runtime settability is not) · V2 USS lint gate fails a CI build on an invalid selector · V3 AccessibilityNode.invoked + non-flags AccessibilityRole compile on 6.3 · V4 Awaitable-wrapped PerformInteractiveRebinding (ADR-0007 Verification 2, deferred here) · V5 150 % UI scale on a 1280×720 window does not clip any approved screen · V6 unscaled world text against 150 %-scaled panels does not read as broken (§3)

Live documentation verified (2026-09-01)

Two claims in this ADR are post-cutoff and were verified against live 6.3 docs rather than training data:

6.3 hazards this ADR is built around

ADR Dependencies

Field Value
Depends OnADR-0003 (assembly boundaries — Driftlands.Presentation is the only UI-referencing assembly), ADR-0004 (atomic writer reused by the settings service), ADR-0006 (ui.json joins the manifest — amended here), ADR-0007 (input maps, IRebindService — amended here)
EnablesEvery /ux-design spec's implementation stories: inventory, crafting, build-mode, hud, settings, pause, title-screen (all APPROVED, all blocked on this)
BlocksThe first UI story of any epic; the Settings screen story specifically (ADR-0007 defers its Awaitable rebind wrap here)
Ordering NoteDoes not depend on ADR-0008 (netcode wire). title-screen.md deliberately split mechanism from payload so the join flow's UI is parity-safe whatever ADR-0008 rules. ADR-0013 (tide-tint) is independent — different pipeline, no shared surface.

Context

Problem Statement

Seven UX specs are APPROVED (inventory, crafting, build-mode, hud, settings, pause, title-screen) against a shared interaction-pattern library at v0.19. Between them they name a settings service, a panel manager, a UI-scale mechanism, a reason-string contract and two schema-negative tests — none of which any ADR owns. traceability-index.md carries TR-ui-001/002/003 as ❌ and TR-ui-004 as ⚠️, all pointing here. No UI story can start until this is Accepted.

The design work is done. This ADR is deliberately not a redesign: where a spec or pattern already states a contract, this ADR ratifies it as a testable interface and names the owner. Where the specs left an explicit question for ADR-0014 (settings OQ 4/5/6/10, title-screen OQ 10, a11y OQ 1), it rules.

Constraints

Requirements

Decision

1. Framework and topology

UI Toolkit for all screen-space UI (confirming architecture.md's standing ruling; the ui-hud quick-spec deferred the tech choice, this ADR closes it). Sprites — not UGUI — for all world-space UI.

2. The settings service

A new Foundation-adjacent service in Driftlands.Runtime, exposed to Presentation as ISettingsService. It owns settings.json and nothing else — it is not a general key-value store and no gameplay system reads it.

File: persistentDataPath/Driftlands/settings/settings.json — the same folder as ADR-0007's bindings.json, per machine, written with ADR-0004's atomic writer (temp + File.Replace) without its rolling backups and without F_FULLFSYNC. Rationale (inherited from ADR-0007 §4, and stated in settings.md): a lost settings change is an annoyance, not data loss, so it does not deserve the durability budget the world save gets.

Schema — 14 keys, all present, no partial writes (settings OQ 4 closed; the three provisional Display rows are KEPT, settings OQ 5 closed):

Key Type Default Range / values Applied by
windowModeenumBorderlessFullscreen · Borderless · WindowedScreen.fullScreenMode
resolution{w,h}nativecurrent monitor's mode listScreen.SetResolution
vsyncbooltrue—QualitySettings.vSyncCount 0/1
gammafloat1.00.8–1.4tint/post material global
hudOpacityfloat1.00.5–1.0, step 0.05HUD presenter, element-scoped (§4)
musicVolumefloat1.00–1audio mixer
sfxVolumefloat1.00–1audio mixer
uiVolumefloat1.00–1audio mixer
monoAudioboolfalse—audio mixer
uiScalefloat1.01.0 · 1.25 · 1.5PanelSettings.scale (§3)
reducedMotionboolfalse—transition suppression (§6)
flashReductionboolfalse—feedback presenters
reviveToggleHoldboolfalse—combat's local revive handling
photosensitivityNoticeShownboolfalse—title screen — the one bit that screen writes directly (title-screen.md Z5 / Actions / Data)

Failure behaviour (mirrors ADR-0007 §4, so the two settings files behave identically): missing file → defaults, silent (first run is not an error). Malformed JSON → defaults + loud log + file renamed settings.bad.json + one non-modal notice on the Settings screen. A key present but out of range → clamped, logged by name; a key absent from an otherwise-valid file → that key's default, logged by name. This is deliberately not silent_default_on_missing_key (ADR-0006's ban): the default is applied and named in the log, because a settings file is player-editable and a silent correction is indistinguishable from the setting not working.

Write policy: written per change (each control commits on release/confirm, not per slider tick — pattern #25), debounced 250 ms, and flushed on screen close and on quit. Settings writes are not on the host commit rail and never touch HostCommitQueue — save_io_inside_drain does not apply because this is not save I/O, but the same principle holds: no file write inside a tick.

3. UI scale (a11y OQ 1 / settings OQ 6 — CLOSED)

The two concerns are separate PanelSettings properties, which is what makes this clean:

World-space text does NOT scale (ruled 2026-09-01): flyouts, name tags, the interact ring prompt and the ghost reason label keep their world size. They are sprites anchored to world objects (§1); scaling them at 150 % would occlude the combat read the art bible's silhouette rule depends on, and no spec has authored overlap rules for that case.

This is a real reduction in the accessibility commitment, and it is stated as one. The a11y doc's own "Minimum text size — HUD" row explicitly counts world-space flyouts as text subject to the size floor ("Damage / yield flyouts are world-space Medium digits"), so this ruling does not merely leave a gap the doc never claimed — it adds a new entry to that doc's Known Intentional Limitations. Two consequences follow and are accepted:

V6 covers the near-term risk cheaply: a screenshot check at uiScale = 1.5 confirming unscaled world text against scaled panels does not read as broken.

Layout obligation: every approved screen must survive 1.5 with no clipping and no overlap. settings.md, pause.md and title-screen.md already state per-zone clearance budgets measured at both ends; V5 turns that into a test at 1280×720 (the worst realistic case: smallest supported window × largest scale).

4. HUD opacity is element-scoped

hudOpacity applies to the SHOULD-KEEP tier only — hotbar, party rows, carry meter, and the dash ring when not Downed. Exempt and always 100 %: own HP pips, the bleed-out arc, ally + Downed edge indicators (Z5/Z7), the banner, and the tide glint (Z6). The slider must never dim your own health or hide a downed friend.

This is a presenter-level rule, not a panel-level opacity — a panel-wide alpha would dim the exempt elements too. The HUD presenter applies it per element group; a USS class (.hud-dimmable) marks the eligible set so the exemption is visible in the markup and testable (§10).

5. The panel manager

One IPanelManager in Presentation owns which screen is open and what the input maps are while it is open. Every approved spec's Entry/Exit table resolves to a call on it.

State Player Build UI Notes
Gameplay, no panelenabled—disabledADR-0007 §3 rule (c)
Build modeenabled (routed)enableddisabledBuildModeActive
Modal screen opensuppressed, except Menu + Inventory edges, which survive (ADR-0007 §3)disabledenabledUiCaptured = true, set from an Update after −1000 → capture applies from the next frame (ADR-0007 §3)
Title screenpresent, only Menu consumeddisabledenabled standingnew state — title-screen OQ 10, closed here

MapState is derived, and Open == null is not enough to derive it: no panel open means Gameplay in the world but Title at the title screen. The manager therefore takes an explicit UiContext (Title | World), set once when the scene loads — named here so two implementers do not invent two different flags.

The title-screen state is the one ADR-0007 §3 does not enumerate: rule (c) ties UI to open panels and enables Player at boot, but the title screen needs UI enabled with no panel and a live Menu read for the 3-second reset floor (ADR-0007 §4's recovery floor). This is an ADR-0007 §3 clarification, not a redesign — the amendment is recorded in §11.

6. Reason strings (TR-ui-003)

Pattern #6's contract is ratified verbatim as a testable pure function in Driftlands.Rules (not Presentation — it is a formula, and formula_in_runtime_assembly is forbidden):

ReasonSelector.Step(in ReasonState prev, int firstFailingGate, long nowMs,
                    int minDisplayMs, int switchFrames) → ReasonState

The function carries all of its own state in ReasonState (ShownGate, ShownAtMs, CandidateGate, CandidateFrames) and returns a new one — the caller stores the struct and passes it back next frame, exactly as ADR-0007's AimResolver.Resolve(prev, …) does. The consecutive-frame counter is inside the machine, not held by the presenter: a presenter that owned the counter would be holding a piece of the state machine, which formula_in_runtime_assembly forbids and which would make the unit test exercise a step rather than the machine. minDisplayMs / switchFrames stay parameters (fed from ui.json, satisfying hardcoded_gameplay_value).

Both tuning values live in ui.json (§8) — reasonMinDisplayMs = 250, reasonSwitchFrames = 2 — because pattern #6 marks them provisional, playtest-tuned.

7. Input — ratifying and enforcing the E2 mechanism

Review finding E2 (2026-08-28) found ADR-0007's original PanelEventHandler sentence named the wrong mechanism. That correction was already applied to ADR-0007 on 2026-08-28 — §Post-Cutoff (a) and the §1 UI row both carry the corrected text today. This ADR does not re-correct it; it independently re-verified the mechanism against live 6.3 docs (above), ratifies it, and adds the thing ADR-0007 could not: an enforcing test (§10), since the correct behaviour depends on a negative condition — the absence of an EventSystem — that nothing currently guards.

The ratified mechanism:

UI Toolkit's built-in, scene-independent default runtime event system consumes the project-wide UI map directly. PanelEventHandler and PanelRaycaster are created only if a uGUI EventSystem is present, and their presence deactivates the built-in path. Since this project has no EventSystem (§1), they are never created.

Consequences that survive unchanged: no components are needed on any panel, and rebinding never touches the UI map (UI.Cancel is protected, ADR-0007 §4).

What is actually load-bearing (specialist correction, 2026-09-01): UI Toolkit does not match a map literally named UI. It derives events from whatever is assigned at Project Settings → Input System Package → UI Actions, matched by role — Point, Click, Navigate, Submit, Cancel, ScrollWheel, and the tracked-device pair. In this project the two coincide (the project-wide asset's map is named UI and is the assigned UI Actions), which is why the outcome is right — but the fragile things are (a) the UI Actions assignment in Project Settings and (b) the individual action names and types. Renaming the containing map alone would break nothing in UI Toolkit's dispatch path — and that qualifier is load-bearing (architecture review E17): the Input System package's own UI-support documentation states that, for the default setup, "the name of the action map (UI), the names of the actions it contains, and their respective Action Types must remain the same" in order to remain compatible with UI Toolkit. That guidance describes name-based resolution of the stock asset; it is safe to depart from only because this project explicitly assigns the UI Actions reference rather than relying on the default. Do not generalise this into "map names never matter". §10's asset test asserts accordingly.

AimStick rebindable / AimPointer not — confirmed (settings OQ 10 closed): a pointer has nothing to rebind to, while a left-handed player swapping sticks is a real need. The Settings screen shows an AimStick row and no AimPointer row.

Awaitable rebind wrap (ADR-0007's deferred Verification 2, owner = this ADR): IRebindService.RebindAsync wraps PerformInteractiveRebinding with AwaitableCompletionSource, cancelled by the screen's CancellationToken on close, on the listen timeout, and on Menu.

Disposal is part of the contract, not an implementation detail. RebindingOperation is IDisposable and holds unmanaged memory — Unity's own docs warn it must be disposed or it leaks the native heap. The wrapper calls RebindingOperation.Dispose() on all four exit paths — successful completion, cancel, timeout, and screen close — before or immediately after completing the AwaitableCompletionSource. The happy path matters most here: a completed rebind exits via none of the three cancellation triggers, so a wrapper that only disposes on cancel leaks once per successful rebind, and settings.md's reset-to-defaults flow can drive many in one session. V4 asserts no native-heap leak on every path, not just the cancelled ones.

8. Data and the USS gate

ui.json — a new file in ADR-0006's manifest (amendment, §11). It carries every UI tuning value so none are hardcoded (hardcoded_gameplay_value): uiScaleSteps [1.0, 1.25, 1.5], hudOpacityRange [0.5, 1.0], reasonMinDisplayMs 250, reasonSwitchFrames 2, glintBandPx, pipQuotientMax 8, plus hud.md's stage-9 rows. Validated at boot in ADR-0006's stage 9, ranges as warnings (a UI value out of range is clamped and logged, never a boot failure — the game must still start).

ui.json is shipped data (assets/data/ → StreamingAssets/), read-only at runtime. settings.json is runtime-written and therefore correctly absent from ADR-0006's manifest — the two must not be confused (settings OQ 4).

USS lint gate (V2) — because 6.3's strict parser makes an invalid selector a build failure, the failure must surface in CI, not on a developer's next import. An editor test asserts every .uss under Assets/UI/ imports without error; it runs in the existing GitHub Actions unity-test-runner job. This is cheap insurance against a class of breakage that is invisible until someone reimports.

9. Performance stance

The stance splits in two, because the two halves have genuinely different cost models.

(a) UI Toolkit panels — zero allocated slice. The HUD panel updates only on change, never per frame: presenters subscribe to the events the systems already raise (SwingLanded, HealthChanged, tide floats via the tint driver's existing per-frame read). UI Toolkit is retained-mode, so an unchanged panel costs no rebuild. hudOpacity (§4) is applied on SettingsChanged only, never per frame, over a cached .hud-dimmable query result — the query runs once per screen build.

No per-frame slice is allocated to the panels, and this is a deliberate invariant, not an omission: if the panels ever need a measurable slice of 16.6 ms, something is being rebuilt that should not be. Registered as system: ui-panels, budget_ms: 0.0 with that note (the land-lifecycle row is the precedent for a zero-with-rationale entry) so a future ADR cannot quietly take one.

(b) World-space UI — a real, bounded slice. Flyouts, ally edge indicators, the ping marker and the interact ring are SpriteRenderer/TMP objects that move every frame with the world objects they anchor to. The retained-mode argument does not apply to them. hud.md already states the number: ≤ 0.2 ms/frame with 4 party rows, 8 flyouts and 4 edge glyphs on the reference machine (its performance AC). That is this ADR's registered budget — system: ui-worldspace, budget_ms: 0.2 — not a new claim.

The population is provably bounded, which is what makes the number safe: flyoutCap = 8 (ui.json, culled oldest-first), edge indicators ≤ party size, one ring, one ping. Nothing here is unbounded.

Draw calls. The project budget is < 100. World-space UI sprites are sorted alongside the DriftLayer sprites (sortingLayerName/sortingOrder) but they do not share that atlas: ADR-0005 §2 keeps the UI atlas separate and non-addressable, while every DriftLayer atlas is packed inside a per-template Land_<id> Addressables group and is "referenced by exactly one addressable (this prefab)" — the invariant that keeps the setup free of the duplicate-texture pitfall. Putting UI sprites in a land atlas would both break that invariant and unload the flyouts at LandTornDown. So the honest cost is one batch for the UI atlas plus one for the shared TMP font-atlas material — about +2 draw calls, not zero — a fixed cost independent of the flyout/indicator population, which is what the bounded population above actually buys. Against a < 100 budget with a measured total near 40, this is immaterial, but it is stated as a real cost rather than claimed away. (ADR-0013 owns tint and BB-overlay draw-call cost; flyout/indicator cost is owned here. ADR-0013 §8 makes the same point from the other side: "Batching with 'the atlas' is only true of the right one.")

Frame rate and vsync. Application.targetFrameRate = 60 is pinned regardless of the vsync setting — the 16.6 ms budget every other system is designed against must not become 6.9 ms because a player on a 144 Hz monitor turned vsync off. vsync (§2) selects QualitySettings.vSyncCount 0/1 for tearing behaviour only; it never changes the frame target.

The one genuinely per-frame rules cost is the ghost reason label in build mode. ReasonSelector.Step is pure, allocation-free (a readonly record struct in, one out), and runs once per frame — the same class of cost as ADR-0007's AimResolver.

Architecture Diagram

┌─ Driftlands.Presentation ────────────────────────────────────────────────┐
│                                                                          │
│  HudPanel (PanelSettings, sortingOrder 0)     ScreenPanel (order 10)     │
│    └ hud.uxml  ── HudPresenter                  └ one of:                │
│         .hud-dimmable ← hudOpacity (§4)             inventory · crafting │
│         exempt: HP pips, bleed arc, Z5/Z6/Z7        settings · pause     │
│                                                     title-screen         │
│                        both: scale ← uiScale (§3)                        │
│                                                                          │
│  IPanelManager ── owns open screen + map state (§5) ── opener stack ≤ 2  │
│         │                                                                │
│  World-space sprites (NOT UI Toolkit, NOT uiScale-scaled):               │
│    flyouts · ally edge indicators · name tags · ring prompt · ping       │
└──────────┬───────────────────────────────────────────────────────────────┘
           │ reads views only (never writes game state)
           │ asks services to change settings
┌──────────▼─ Driftlands.Runtime ──────────────────────────────────────────┐
│  ISettingsService → settings.json (ADR-0004 atomic writer, no backups)   │
│  IRebindService   → bindings.json  (ADR-0007; Awaitable wrap here §7)    │
│  InputAdapter     → InputFrame (UiCaptured set by IPanelManager)         │
└──────────┬───────────────────────────────────────────────────────────────┘
           │
┌──────────▼─ Driftlands.Rules (pure, no UnityEngine) ─────────────────────┐
│  ReasonSelector.Step(prev, prevAtMs, firstFailingGate, nowMs) → shown    │
└──────────┬───────────────────────────────────────────────────────────────┘
┌──────────▼─ Driftlands.Data ─────────────────────────────────────────────┐
│  UiConfig ← ui.json (ADR-0006 stage 9, ranges as warnings)               │
└──────────────────────────────────────────────────────────────────────────┘

NO uGUI EventSystem anywhere → UI Toolkit's built-in runtime event system
consumes the project-wide `UI` map directly (§7).

Key Interfaces

// Driftlands.Runtime — settings
public interface ISettingsService {
    SettingsSnapshot Current { get; }              // immutable record, 14 fields
    void Update(Func<SettingsSnapshot, SettingsSnapshot> edit);  // clamps + logs by name; debounced write
    Awaitable FlushAsync();                        // on screen close and on quit
    event Action<SettingsSnapshot> SettingsChanged;
    SettingsLoadResult LastLoad { get; }           // Ok | DefaultsFresh | DefaultsRecovered(badPath)
}

// Driftlands.Presentation — panels
public interface IPanelManager {
    ScreenId? Open { get; }
    void OpenScreen(ScreenId id, ScreenId? opener = null);  // closes any current screen
    void Back();                                            // returns to opener, else closes
    void CloseAll();
    UiMapState MapState { get; }                            // Gameplay | Build | Modal | Title
    event Action<ScreenId?> OpenChanged;
}

// Driftlands.Rules — pure, allocation-free, unit-tested
public readonly record struct ReasonState(
    int ShownGate, long ShownAtMs, int CandidateGate, int CandidateFrames);

public static class ReasonSelector {
    // The machine owns its own frame counter; the caller only stores the struct.
    public static ReasonState Step(in ReasonState prev, int firstFailingGate,
                                   long nowMs, int minDisplayMs, int switchFrames);
}

SettingsSnapshot is an immutable record and Update takes a pure edit function, so every change produces a new snapshot and raises SettingsChanged; nothing mutates in place (project immutability rule). A generic Set<T>(SettingKey, T) was rejected: it cannot be compile-checked (Set(SettingKey.UiScale, true) would compile), and every other interface in this project is precisely typed.

Alternatives Considered

Alternative A: UGUI (Canvas) for all UI

Alternative B: UI Toolkit screens + UGUI world-space canvases for flyouts

Alternative C: uiScale via referenceResolution manipulation

Alternative D: One panel for HUD and screens

Alternative E: Settings inside the world save

Consequences

Positive

Negative

Risks

GDD Requirements Addressed

GDD / Spec Requirement How This ADR Addresses It
design/quick-specs/ui-hud-2026-08-27.md Rules 1–2Closed screen-space HUD list; everything else diegetic§1 topology: HudPanel renders the closed list; all other state is world-space sprites (TR-ui-001)
ui-hud Rule 3Co-op affordances at MVP: edge ally indicators, storage who-took-what, world-space ping§1 world-space sprite set; §4 exempts ally/Downed indicators from hudOpacity so they can never be dimmed away (TR-ui-002)
ui-hud Rule 4 / BB UI RequirementsPer-cause reason strings, precedence, stability, snap hysteresis§6 ReasonSelector.Step in Rules — 250 ms floor, 2-frame switch, string-only hysteresis (TR-ui-003)
ui-hud Rules 5–6Aggregation ban; gamepad nav + visible focus; no hover-only; ≥ 4.5:1; no animation-gated input§10 schema-negative aggregation test; §1 focus ring in theme.uss; §5 modal focus context (TR-ui-004)
design/gdd/combat-system.md UI RequirementsOwn + party HP, Downed ally edge indicator, host-confirmed damage numbers, no enemy HP bars§1 flyouts are world-space sprites driven by host-confirmed events only; §4 keeps own HP and the bleed-out arc at full opacity (TR-combat-013)
design/ux/settings.md Data Requirements13 settings, settings.json, atomic writer, settings.bad.json§2 in full — schema, path, writer, failure behaviour (OQ 4, 5, 6, 10 closed)
design/ux/hud.md Tuning KnobsAll values in assets/data/ui.json; nothing hardcoded§8 ui.json + ADR-0006 manifest amendment (hud OQ 7)
design/ux/title-screen.md OQ 10UI enabled standing with Player present, only Menu consumed§5 map-state table — the fourth state, plus the ADR-0007 §3 clarification (§11)
design/accessibility-requirements.md OQ 1UI-scale mechanism; does world text scale?§3 — PanelSettings.scale for panels; world text stays world-sized (ruled, recorded as a limitation)
design/ux/interaction-patterns.md #6, #25Reason-string contract; slider commit semantics§6 ratifies #6 verbatim; §2 write policy implements #25's commit-on-release

Performance Implications

Migration Plan

No production code exists yet. Order:

Validation Criteria

Blocked On — Not Owned Here

This ADR closes the UI stack. Three surfaces the approved specs describe depend on amendments that belong to other ADRs, and are listed here so the first story does not discover them:

Surface Blocked on Owner Effect if it does not land
World list, Continue's target, world auto-names (title-screen.md OQ 9)Four ADR-0004 amendments: (a) SaveHeader ReadHeader(worldFolder) — header parse without the load pipeline; (b) createdUtc; (c) lastPlayedUtc; (d) CreateWorld(seed) → worldFolder with a named owner for seed rolling. None exists today.ADR-0004The world list cannot name or sort; Continue has no target. The title screen's other paths (Host, Join, Settings, Quit, reset floor) are unaffected.
HUD element E10 Ping (hud.md) — and therefore one third of TR-ui-002The ADR-0007 Ping amendment (MMB / R3). No Ping action exists in §1's table.ADR-0007hud.md already degrades gracefully: the element simply does not render, no layout or budget change. TR-ui-002 closes on two of three limbs (edge indicators, who-took-what) until this lands.
Storage who-took-what feed (pattern #13, inventory.md)The confirmed-InventoryTransfer broadcast payload from ADR-0002's drain — no ADR defines its shape.ADR-0010 (inventory replication)The chest panel renders host-confirmed contents without the attribution tags. Tag lifetime is carried in ui.json so only the payload is missing.

None of these is this ADR's to make: extending the save header is ADR-0004's, binding Ping is ADR-0007's, and the transfer broadcast is inventory replication's. Claiming them here would be exactly the cross-ADR overreach the coordination rules forbid.

Rulings Confirmed (no document changes)

These were explicitly deferred to ADR-0014 by another document, and are confirmed here. Nothing in the source document changes:

Ruling Deferred by Confirmation
AimStick rebindable (left-handed stick swap), AimPointer notADR-0007 Amendments (2026-08-31, item 3) — "a UX ruling for ADR-0014 to confirm"Confirmed. A pointer has nothing to rebind to; the stick swap is a real need. Settings shows an AimStick row, no AimPointer row.
Window mode / Resolution / VSync keptsettings.md OQ 5 — "ADR-0014 confirms or removes"Confirmed kept (§2).
UI-scale mechanismaccessibility-requirements.md OQ 1Resolved (§3) — this one does amend the a11y doc, see below.

Applied on Acceptance — ✅ ALL APPLIED 2026-09-01

Historical record. While this ADR was Proposed, the amendments below were described but deliberately not applied — amending Accepted documents on the strength of a Proposed one would strand those edits if /architecture-review returned this for revision. That deferral ended when this ADR was Accepted on 2026-09-01: all four amendments and every registry stance are now applied and committed (dc7c1b0). The list is kept as the record of what landed, not as outstanding work.

Until this ADR is Accepted, the following statements in it describe an intended future state, not the repository as it stands:

This ADR says Target document currently says Applied when
§8: ui.json is validated at ADR-0006 stage 9ADR-0006 stage 9 does not mention ui.json; Configs has no Ui memberAcceptance
§5: the title-screen map state is ruledtitle-screen.md OQ 10 reads "for ADR-0014's panel manager" (unresolved)Acceptance
§3: the UI-scale mechanism is resolvedaccessibility-requirements.md OQ 1 reads "Unresolved"Acceptance
§2/§7: settings OQ 4/5/6/10, hud OQ 7 are closedThose OQs are still open in their specsAcceptance

The registry stances this ADR creates (ui-panels 0.0 ms, ui-worldspace 0.2 ms, the ISettingsService/IPanelManager/ReasonSelector interfaces, and the no-EventSystem forbidden pattern) are likewise not registered yet, per the architecture-decision gate: a Proposed decision does not claim registry ownership.

One registry correction was applied ahead of this ADR because it is independent of it: rebind_policy still recorded "swap on confirm", which ADR-0007's 2026-08-31 amendment superseded with exchange. That row was wrong for anyone reading it today, regardless of ADR-0014's fate.

Documents Amended by This ADR

Status: ✅ applied 2026-09-01. See "Applied on Acceptance" above. Each row records the exact edit that was made when this ADR was Accepted.

Document Amendment
docs/architecture/adr-0007-input-action-map.mdOne amendment: §3 gains the title-screen map state — UI enabled standing, Player present with only Menu consumed (a state §3's rule (c) does not enumerate). §4's deferred Verification 2 (Awaitable rebind wrap) is claimed here as its named owner — ADR-0007 already assigns it to ADR-0014, so no text in ADR-0007 changes.
docs/architecture/adr-0006-game-data-loading-validation.md§1 file list gains ui.json (uiScaleSteps, hudOpacityRange, reasonMinDisplayMs, reasonSwitchFrames, glintBandPx, pipQuotientMax + hud.md stage-9 rows); §2 step 3 Configs.{…, Ui, …}; stage 9's title gains Ui (Movement / Camera / Input / Inventory / Save / Land / Ui) and the stage gains the rows, ranges as warnings. settings.json is runtime-written and stays out of the manifest.
design/accessibility-requirements.md(a) OQ 1 resolved: mechanism = PanelSettings.scale (knob) over Scale with Screen Size @ 1920×1080 (baseline). (b) A new Known Intentional Limitation: world-space text does not scale — with the rationale, the load-bearing 100 % floor, and the Tier 1 world-text scale. (c) The "Minimum text size — HUD" row is annotated to note that its flyout clause is unaffected by uiScale.
docs/architecture/architecture.md§4 gains ISettingsService / IPanelManager / ReasonSelector; Required-ADR row 14 marked Accepted on approval; row 14's open half ("UGUI or sprites") resolved to sprites.
design/ux/settings.md, design/ux/hud.md, design/ux/title-screen.mdOQ closures recorded: settings OQ 4/5/6/10, hud OQ 7, title-screen OQ 10. Specs remain APPROVED — no design changes, only answers.

Related Decisions

Amendment History