GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

FO3_BUILD_PLAN.md



RELEASE

planning/2.8.5/plans/FO3/FO3_BUILD_PLAN.md

sha256 226f9781b2c862c5 · 36891 bytes · original held in the private archive

# FO3 PIP-BOY 3000 — BUILD PLAN (Protocol 8 Stage 1 · Opus diagnose & plan) > **Planning document only. No code in this session.** Grounded in `dev` @ `709a96c` > (3002 tests / 222 suites / one Node runner / fast gate ~12.7s). Sonnet builds from this; > an independent Opus pass audits the diff. Companion design of record: > `planning/FO3_PIPBOY_DESIGN.md`. Canonical mockup: `planning/mockups/fo3/fo3-pipboy-true.html` > (verified renderable — see §1). **NV is untouched throughout; that is a hard invariant, not a goal.** --- ## 0. What I actually verified (not estimated) **The mockup runs and its measurements are real.** Served over HTTP and measured headless at 780×360: | Claim in the design | Measured | Verdict | |---|---|---| | Glass inner 634×262, content region 207px | `glassinner 634×262`, `content 634×207` | ✅ exact | | Zero horizontal overflow at 780×360 | `scrollWidth === innerWidth === 780` | ✅ | | Vault Boy SVG + 6 limb boxes fit | `figwrap svg` present, `limbbox` × 6 | ✅ | | Every tap ≥28px | steppers 28, subrail 28, limb 34, lamp 49, gauge 49, flick 43, knob 73, detail actions 28, filters 28 | ✅ all ≥28 | | MANIFEST is list+detail+actions+drawer filters | `.split` present, 12 rows, 6 filters, 3 detail actions | ✅ | | Portrait hands off to legacy | at 412×780 `.machine{display:none}`, `.legacycard{display:flex}`, no overflow | ✅ | The design is not a hypothesis that failed contact with measurement — it survives it. The open risks are **integration** risks (real DOM ≠ idealized mockup markup), not feasibility risks. **The real code, read directly (not from docs):** - **Panel visibility today is ONE CSS axis.** `css/15-overseer.css:408` — `.panel[data-tab]{display:none}`; `.tab-visible{display:block}`; `.panel:not([data-tab]){display:block}` (always-on, e.g. the chat panel). `switchTab(tab)` (`js/ui/ui-core-nav.js:119`) toggles `.tab-visible` on every `.panel[data-tab]`. DATABANK is the one merge: `_DATABANK_TABS=['data','campg']` shows both groups at once. - **`selectSubsystem(view)` (`ui-core-nav.js:383`)** is the single bezel entry point. It maps view→tab via `_NAV_TAB_FOR` and calls `switchTab()` (which routes, syncs the bezel LED/aria/telemetry via `_syncBezelNav()`, and restores per-subsystem scroll). `uplink` is special-cased (scroll/focus the composer, no tab). Hotkeys `[1]`–`[6]`/`[0]` and `#go=` deep-links all funnel through these two functions. - **`data-game` is on `<html>`** (`document.documentElement.dataset.game`, set in `ui-core.js:760` + `ui-core-modulebay.js:691`). Every `[data-game='FO3']` selector keys off the root element — already the established per-game CSS switch. - **The 23 shared boards are all `<details class="panel bay-board" data-tab="…">`** inside `.glass-frame > .main-grid > .col-left#uiPanel` (`index.html:770–793`). They stack (accordion); each `<details>` opens/closes independently. Full board→sub-tab map is in §2. - **The bezel nav is 6 `<button class="navkey" role="tab">` in one `.nav-cluster`** (`index.html:671`), DIR sits outside the tablist. Per-game flavor text is **already** done the right way: `.fieldkit--generic` vs `.fieldkit--fnv` are two static spans toggled by `[data-game]` CSS (`index.html:763`) — the exact pattern FO3 casing flavor must use (Protocol UI-7). - **CSS = 12 order-prefixed files, source order IS the cascade** (`05-base` → `99-mobile`), linked in `index.html:33–44`, mirrored in `sw.js` ASSETS (`sw.js:17–28`). **`CSS_SPLIT_FILES` is derived from index.html's `<link>` tags** (`tests/robco-diagnostics.js:200`) and **Suite 49.2 asserts every css/ file is in sw.js ASSETS** — a new CSS file is gate-checked on both surfaces automatically. - **No `data-subtab` / sub-tab concept exists anywhere yet** — clean slate for the second axis. - **FO3 `identity` exists as stubs** (`state.js:1152–1222`): `machine:'pipboy-3000'`, persona/ceremony/ motionTexture/audio/voice/overseer/databank all present. **Missing for this build: `rails`, `orientation`, `statusStrip`.** `getIdentity(ctx)` (`state.js:1480`) is the read accessor; `identity.theme` is aliased, never copied (Protocol 22). --- ## 1. THE CRUX — the second nav axis without forking the render pipeline **The problem, precisely:** today one axis (subsystem→tab) reveals a *group* of stacked boards. FO3 needs a second axis (subsystem→sub-tab) that reveals *one sub-tab's boards* at a time. This must NOT become a parallel renderer (Protocol 22/23). The boards keep their one shared markup and their one render path. **The mechanism — a second CSS class toggle, driven by identity DATA, gated to FO3, inert for NV:** 1. **Grouping lives in `identity.rails` (pure data, in `state.js`).** Keyed by subsystem, each rail lists the board **ids** it contains: ``` rails: { operator: { STATUS:['opVitalPanel','opHarnessPanel','statusEffectsPanel'], SPECIAL:['opSpecialPanel'], SKILLS:['skillMatrixPanel','skillBooksPanel','magazinesPanel'], PERKS:['perkLoadoutPanel'], // traits ride inside it GENERAL:['positionClockPanel','factionPanel','karmaPanel'] }, operations: { MANIFEST:['opsManifestPanel'], CRAFT:['craftPanel'], BARTER:['tradePanel'], SQUAD:['squadPanel'], CURIO:['curioPanel'] }, // opsBridgePanel → top strip, no rail databank: { MAP:['worldMapPanel'], QUESTS:['questLogPanel'], NOTES:['campaignNotesPanel'], LOG:['campaignLogPanel','campgPanel'], QUERY:['databankPanel'] }, } ``` This is temptation-list item 4 realized. NV's identity has **no `rails` key** → the whole axis is dormant. 2. **A game-agnostic stamper runs when the game context resolves.** `_applyRailGrouping()` reads `getIdentity().rails`; for each `[railName, [ids]]` it sets `data-subtab="RAILNAME"` on each board by id. **If `rails` is absent (NV/FO4-designOnly) it is a pure no-op — NV panels never receive `data-subtab`.** No `if (ctx==='FO3')` anywhere; the presence of the data drives the behavior (Protocol 38). 3. **A game-agnostic second-axis selector** `selectSubtab(name)` toggles a `.subtab-active` class on the boards whose `data-subtab===name`, renders the rail's active state, and persists the choice to a MetaStore pref (Protocol UI-6 — remembers on reload). It reads `getIdentity().rails[currentSubsystem]`; no-op if absent. 4. **The actual show/hide is CSS, scoped to FO3, in the new FO3 file:** ```css [data-game='FO3'] .panel[data-tab].tab-visible[data-subtab]:not(.subtab-active){ display:none; } ``` - NV boards have no `data-subtab` → the `[data-subtab]` requirement means **this rule can never match an NV element**. NV keeps its accordion, byte-for-byte. ✅ - FO3 boards are hidden unless they carry `.subtab-active`. ✅ - The rule lives only in the FO3 file and only fires under `[data-game='FO3']`. ✅ 5. **`selectSubsystem()` EXTENDS, it does not fork.** After `switchTab()` reveals the subsystem group, it calls one new helper `_applyRails(subsystem)` that (a) renders the sub-tab rail from `identity.rails` and (b) applies the last/default sub-tab. **`_applyRails` is a no-op when `identity.rails` is absent** — so NV runs the identical `selectSubsystem`→`switchTab` path it runs today, and nothing new executes. **Why this is the right shape (defending the design decisions the prompt demands):** - **"Show one board, hide others" without a JS game-branch?** A `data-subtab` attribute (stamped from data) + a `.subtab-active` class toggle + one FO3-scoped CSS rule. JS never branches on game name; it branches on *whether the active identity carries rails data*. - **Where does the grouping live?** `GAME_DEFS[FO3].identity.rails` — data, in `state.js`, read via `getIdentity()`. Never in markup literals, never in CSS selectors listing board ids. - **Does NV get sub-tabs?** **No — the second axis is FO3-only and inert for NV. This is the recommended choice and I defend it:** (a) NV's approved machine *is* the machinery-on-the-glass accordion — sub-tabs would be an unrequested redesign violating Protocol 25; (b) "NV MUST NOT CHANGE VISIBLY" is a hard invariant, and "no rails key → axis never activates" is the simplest possible guarantee of it; (c) the N-game abstraction is still *proven*, because the axis is fully data-driven — a future game enables it by adding its own `rails` map, zero code change. Building NV rails now would be speculative generality with a regression surface and no owner sign-off. - **Master-detail: new DOM region, or CSS re-layout of existing markup?** **Neither is free, and the honest answer splits the difference — see §3.** Today's inventory/squad/quest rows carry *inline* action buttons and have *no* detail pane in the DOM. So true master-detail (select-row-then-act) is a **new render path**, not a CSS re-layout — and it is exactly what costs the +1 tap the design flagged (§6). Therefore master-detail is **deferred/optional**, gated on the owner's edit-flag decision. The **spine keeps the existing inline-control rows**, re-skinned by CSS to the austere FO3 look — that IS a pure CSS re-layout, it preserves tap parity, and it needs no new renderer. The mockup's `.split`/`.detail` is the *polish* target, not the spine. --- ## 2. The board → sub-tab map (verified against real ids in index.html) Panels missing a stable `id` today get one added (NV-invisible; a prerequisite of the rails map). Marked ⊕. **OPERATOR / STATS lamp (11 boards → 5 rails):** | Rail | Boards (real id) | index.html | |---|---|---| | STATUS | VITAL TELEMETRY ⊕`opVitalPanel` · SKELETAL HARNESS ⊕`opHarnessPanel` · STATUS EFFECTS ⊕`statusEffectsPanel` | 806 / 1391 / 2211 | | SPECIAL | S.P.E.C.I.A.L. TUNING ⊕`opSpecialPanel` | 967 | | SKILLS | SKILL MATRIX ⊕`skillMatrixPanel` · `skillBooksPanel` · `magazinesPanel` | 1956 / 1971 / 1994 | | PERKS | PERK LOADOUT ⊕`perkLoadoutPanel` (traits ride inside) | 2121 | | GENERAL | POSITION & CLOCK ⊕`positionClockPanel` · `factionPanel` · `karmaPanel` | 1260 / 2289 / 2305 | **OPERATIONS / ITEMS lamp (6 boards → 5 rails; weigh bridge → top strip):** | Rail | Board (real id) | index.html | |---|---|---| | — (top strip `Wg`) | LOAD-CELL WEIGH BRIDGE `opsBridgePanel` | 1558 | | MANIFEST | CARGO MANIFEST `opsManifestPanel` | 1620 | | CRAFT | FIELD FABRICATION `craftPanel` | 1804 | | BARTER | BARTER UPLINK `tradePanel` | 1830 | | SQUAD | SQUAD ROSTER `squadPanel` | 1873 | | CURIO | CURIO ARCHIVE `curioPanel` | 1904 | **DATABANK / DATA lamp (6 boards → 5 rails; LOG merges 2):** | Rail | Board(s) (real id) | index.html | |---|---|---| | MAP | CARTOGRAPHY TABLE `worldMapPanel` | 2600 | | QUESTS | DIRECTIVE REGISTRY `questLogPanel` | 2023 | | NOTES | FIELD NOTES `campaignNotesPanel` | 2372 | | LOG | SERVICE TALLY `campaignLogPanel` + CAMPAIGN CHRONICLE `campgPanel` | 2572 / 2624 | | QUERY | CATALOG QUERY `databankPanel` | 2085 | **Body controls (no rails — the design's "controls on the casing"):** CHASSIS = `unitPowerPlantPanel` + `systemStatusPanel` + `serviceFaultConsolePanel` (data-tab=chassis, shown stacked as today). SETTINGS = `accountPanel` + `securityConfigPanel` + `savesPanel` + `campaignConfigPanel`. UPLINK = the always-on chat panel. These three subsystems keep their current stacked behavior even under FO3 — they have no `rails` entry, so the axis leaves them alone. DIR is not rendered as a casing control on FO3 (hotkey `[0]` still opens the shared directory modal). --- ## 3. How much mockup CSS survives contact with the real DOM Split into two layers with very different survival rates: **A. The austere glass interior + control language — ports almost directly (~80–90% reusable).** `.topstrip`, `.subrail`, `.split`/`.list`/`.detail`, `.lrow`, the 4 primitives (`.step` stepper, boxed field, `.tgl` toggle, `.act` action), `.statusgrid`/`.limbbox`/`.vaultboy`, `.speclist`. These style *content*, and we control the content markup — where the real board's markup differs, we adjust the board's render output (existing render functions in `js/ui/ui-render-*.js`) to emit class names the FO3 CSS targets, or add FO3-scoped selectors matching the existing markup. The Vault Boy SVG is literally copy-paste (it's inline markup, zero assets). **B. The casing chrome — needs new cosmetic wrapper markup; the mockup is the reference, not a drop-in (~40%).** The mockup builds three flex columns (left rail / main col / right col) with hood, lamp bar, fins, gauge, knob, toggle, screws, cant. The real DOM has: a bezel nav *block on top*, then `.glass-frame > .main-grid > .col-left#uiPanel`. Two mismatches to resolve: 1. **The 6 navkeys live in one horizontal `.nav-cluster`; the mockup distributes them to 3 casing regions** (3 lamps on the bottom bar, knob+gauge on the left rail, toggle on the right). Resolution: under `[data-game='FO3'] @media (orientation:landscape)`, CSS re-skins each existing `#navkey-*` into its casing form (dome / knob / gauge / toggle) and positions it (CSS grid on a re-flowed casing container, or absolute placement). The keycaps stay the *same buttons* wired to the *same* `selectSubsystem()` — only their skin and position change. This satisfies Protocol UI-7 ("chrome layered around the working shell, never a new shell") and Protocol 25's bezel-nav extension (keycaps keep routing through `switchTab()`). 2. **Casing frame pieces (rail backgrounds, hood, fins, screws, cant) are decorative and don't exist.** Resolution: add a small set of `aria-hidden` cosmetic wrapper divs (or pseudo-elements) *inside the FO3 landscape layer only*, shared markup, display-gated by `[data-game='FO3']`. The mockup's casing CSS (`.cast`, `.rail`, `.hood`, `.lampbar`, `.fins`, `.gauge`, `.knob`, `.flick`) is the reference implementation for styling them. **Bottom line:** the interior is a near-direct port; the casing is a guided re-implementation using the mockup CSS as the spec. **This is why the spine (§5) ships the interior + rails + a *minimal* casing frame, and defers the fully-dressed casing (gauge sway, roller, knob detent, cant) to polish.** --- ## 4. Landscape / portrait switching - **Pure CSS, orientation-gated, FO3-scoped.** New rules live under `[data-game='FO3'] @media (orientation:landscape){…}` (the Pip-Boy casing) and `[data-game='FO3'] @media (orientation:portrait){…}` (leave the existing flat layout alone). **No JS branches on orientation** (Protocol 38). `identity.orientation:'landscape-primary'` is a data flag recorded for the future abstraction (temptation item 5) but the CSS query is what actually drives layout. - **Portrait = the existing layout, now LOAD-BEARING.** Honest assessment: the "legacy view" is not a dusty fallback that needs building — **it is today's shipped FO3 mobile-portrait rendering** (the bezel + boards with FO3 green/amber theme). It already works, already passes the gate, already holds the Protocol 17 mobile invariants. **Cost to harden: LOW, and it is mostly a *containment* cost, not a *build* cost** — the real risk is the new FO3 landscape CSS leaking into portrait. Mitigation: every FO3 landscape rule is wrapped in `@media (orientation:landscape)`, and a gate test asserts the FO3 casing selectors are all inside a landscape query (see §7). No portrait markup changes; the portrait experience is "what ships today, unchanged." - **Real-device height is the one thing emulators can't prove (Protocol 29-class).** Phone browser chrome (URL bar, safe-area insets) eats landscape height; the deciding viewport is 360px tall and the design spends it to the pixel. **Use `100dvh`, not `100vh`**, for the casing height, and honor `env(safe-area-inset-*)`. **PWA standalone (no browser chrome) is the true target.** The unit that ships the casing is NOT "done" until the owner has loaded it on his real phone in landscape, in both a browser tab and the installed PWA, and confirmed no clipping/scroll. This is an explicit acceptance gate, not a nice-to-have. --- ## 5. Scope call — the SPINE, then layers **The spine = the smallest honest push that puts a recognizable Pip-Boy on the dev PWA in landscape, fully editable, with NV provably unchanged.** Everything else layers on top, each unit independently shippable and gate-green. **SPINE (what gets it onto his dev PWA to play with):** > FO3 landscape shell (minimal casing frame + 3 lamps/knob/gauge/toggle re-skinned from the existing > navkeys) · the austere glass interior CSS (top strip + sub-tab rail + re-skinned boards keeping their > inline controls) · the rails second-axis (data + stamper + `selectSubtab` + FO3 CSS toggle) · portrait = > today's layout untouched. **No Vault Boy, no master-detail, no casing dressing.** STATUS in the spine just > shows the 3 real boards (vital/harness/effects) stacked and re-skinned. Every tab edits exactly as today. That is testable, faithful enough to read as "the Pip-Boy," and safe. **The unit list below front-loads it.** **Polish (layers, in cut-order-reverse — most valuable first):** Vault Boy STATUS view → casing dressing (gauge sway, roller, knob detent, fins, screws) → master-detail split (only if owner chooses it over inline) → the cant → the arm/desk framing → per-game motion textures (DETENT-ROLL sweep) and audio (rotary detent). --- ## 6. Unit decomposition (each shippable, gate-green, app fully working after) Ordered so the spine lands first. Every unit ends with the full gate green + docs/counts synced (Protocol 2/2a) + a regression test (Protocol 13/20). Acceptance criteria are the Protocol 26 definition-of-done. ### U0 — Stable board ids + rails data (no visible change; the seam) - Add missing `id`s (⊕ in §2) to the 7 un-ided boards in `index.html`. Add `rails`, `orientation`, `statusStrip` to `GAME_DEFS.FO3.identity` in `state.js`. **No CSS, no layout, no cache-served behavior change beyond new ids.** - **AC:** every board in §2 has a stable id; `getIdentity('FO3').rails` returns the map; NV/FO4 identity has no `rails`; existing gate green; a new test asserts every id named in FO3 rails resolves to exactly one `.panel[data-tab]` in index.html and every `data-tab` board is either in a rail or an explicit no-rail allowlist (weigh bridge, chassis×3, settings×4). **NV visually identical (proven by U-REG below).** ### U1 — The second axis, inert (mechanism with no FO3 skin yet) - `_applyRailGrouping()` stamper (game-agnostic, no-op without rails), `selectSubtab(name)`, `_applyRails(subsystem)` called from `selectSubsystem()`, MetaStore pref `robco_fo3_subtab` per subsystem (Protocol UI-6). Render the sub-tab rail element (hidden until FO3 CSS reveals it). The FO3-scoped `:not(.subtab-active)` hide rule can ship here but has no casing around it yet. - **AC:** with `data-game='FO3'`, selecting OPERATOR shows only the STATUS group; the rail switches groups; reload restores the last sub-tab; **with `data-game='FNV'` nothing renders/executes new** (stamper no-ops, `_applyRails` no-ops, no `data-subtab` in the DOM). Deep-links/hotkeys unchanged. New tests: axis-inert-for-NV, rails-drive-visibility, subtab-persists, `selectSubsystem` still routes identically for NV. ### U2 — FO3 CSS file + landscape casing shell (THE SPINE lands here) - New `css/60-fo3-pipboy.css` (see §7 for slot rationale), added to `index.html` links **and** `sw.js` ASSETS. Landscape casing: minimal frame + re-skin the 6 navkeys into lamps/knob/gauge/toggle and position them; the austere glass interior (top strip from `identity.statusStrip`, sub-tab rail styling, board re-skin keeping inline controls). Portrait rules = leave existing layout alone. `100dvh` + safe-area. - **AC:** at 780×360 under FO3, the casing + austere glass render with zero horizontal overflow, all taps ≥28px, inputs ≥16px; portrait renders today's layout unchanged; **NV at every breakpoint pixel-identical to pre-U2 (U-REG)**; reduced-motion clean; AA contrast; a11y baseline-diff green (see §8). **Real-device landscape + PWA-standalone verified by owner (Protocol 29-class).** ### U3 — STATUS Vault Boy view - New full-bleed STATUS layout: inline-SVG Vault Boy (copied from mockup, strokes on the phosphor token) + 6 limb boxes wired to the **existing** limb toggle handlers + HP/RAD steppers wired to the **existing** setters. Replaces the 3-stacked-boards STATUS group under FO3 only. Crippled-limb blink is a plain `@keyframes` (reduced-motion-neutralized by the global block). - **AC:** tapping a limb box toggles crippled state in `state` (same one tap as today's harness); HP/RAD steppers write the same fields; **zero image assets** (gate-checked); NV STATUS untouched; figure re-tints with the optic token. Regression test: limb toggle + HP/RAD edit still hit the same state paths. ### U4 — Casing dressing (pure cosmetic polish) - Gauge sway, roller texture, knob detent + freq etch, fins, screws, nameplate, serial-plate flavor from `identity` (never a CSS `content:` literal / JS string — temptation item 13). Optional cant (−0.7°) behind the same real-device crispness check the design flags; default 0° if it softens text. - **AC:** cosmetic only, `aria-hidden`, `pointer-events:none`, never intercepts a keycap; reduced-motion zeroes all of it; NV untouched; no overflow. ### U5 — (OPTIONAL, owner-gated) master-detail for MANIFEST/SQUAD/QUESTS - **Only if the owner chooses master-detail over keeping inline controls.** New list+detail render path for those three boards. **This is the only unit that touches a render path and the only one that risks the +1 tap** (§ design §6). If the owner instead wants strict parity, this unit is *cut* and the spine's inline controls stand, or a compact inline primary-action glyph is added (much cheaper than full master-detail). - **AC:** editing survives at ≤ current tap count on all three; if +1-first-action is accepted, it is documented; NV untouched. **Batch/push discipline (Protocol 19):** U0+U1 can land as one push (seam + inert mechanism). U2 is its own push (the spine — real-device gate). U3, U4, U5 each their own. Never big-bang. --- ## 7. Re-sync checklist (everything a new CSS file / new markup touches) - **`index.html`** — new `<link rel="stylesheet" href="css/60-fo3-pipboy.css">` (after `55-`, before `99-mobile`); 7 new board `id`s; FO3 casing wrapper divs; sub-tab rail element. - **`sw.js`** — add `./css/60-fo3-pipboy.css` to `ASSETS`; **bump `CACHE_NAME`** (`v2.8.0-r12` → next; served files change — Protocol 1). Suite 49.2 fails if the file is linked but not in ASSETS. - **CSS cascade slot** — **`60-fo3-pipboy.css`, after `55-feedback-animations.css`, before `99-mobile.css`.** Rationale: it must come after all shared board/chrome CSS so its `[data-game='FO3']` rules win by source order; `99-mobile.css` must stay last (its own header mandates it). ⚠ **Cascade risk:** `99-mobile.css` rules load *after* the FO3 file, so a mobile rule could override an FO3 landscape rule at phone widths. Mitigation: FO3 rules are `[data-game='FO3']`-scoped (higher specificity than most plain mobile selectors); where a genuine conflict exists, resolve by specificity, **not** by moving the FO3 file after 99-mobile. - **`CSS_SPLIT_FILES`** — auto-derived from index.html `<link>` tags (`tests/robco-diagnostics.js:200`); no manual edit, but confirms the new file is picked up by the cascade-order tests. - **`scripts/check-boot-chain.js`** — CSS isn't part of the JS boot chain, but run it after any index.html/sw.js edit to confirm mutual consistency. - **`LOAD-ORDER-GUARD` blocks (CLAUDE.md / ARCHITECTURE.md)** — these guard the **JS** boot order only; a new CSS file does **not** touch them. No JS files are added by this build (all logic extends existing `ui-core-nav.js` / render files), so the load-order guard is untouched — **confirm** no new `<script>` tag. - **`tests/test.html`** — loads the JS boot chain, not CSS. New state field? No — `rails` is `identity` data, not a `state` field, so **no `KNOWN_KEYS` / Protocol 4 / Protocol 40 obligation**. Confirm no new `state.*` field is introduced (the sub-tab pref is a MetaStore device pref, not campaign state — Protocol 23/UI-6). - **`repomix.config.json`** — already globs `css/**` and `js/**`; **no change** (Protocol 37 — confirm). - **`library/CODE_MAP.md`** — add the FO3 CSS file + the new nav functions (`selectSubtab`, `_applyRails`, `_applyRailGrouping`) in the same commit they land (Protocol 46). No `library/MANIFEST.txt` change (no new `library/` file). - **`ARCHITECTURE.md` / `CHANGELOG.md` / `README.md`** — version bump (MINOR — new user-facing FO3 view), changelog `[Unreleased]` plain-English entries, README supported-view description (Protocol 2/2a). - **ESLint/Prettier** — new JS is in existing files; format/lint clean. **UTF-8 discipline (Protocol 39):** the Vault Boy SVG and box-drawing glyphs must be written via the Edit tool or Node `fs`, **never PowerShell** (Suite 90 guards mojibake). - **Test counts (Protocol 2a)** — every new test re-syncs the count across all 8 locations + `test.html` `Suites:` marker if a runtime suite is added. --- ## 8. Risk register | Risk | Detection | Rollback / mitigation | |---|---|---| | **NV regresses visibly** (the hard invariant) | **U-REG: a DOM/computed-style snapshot regression check** at 360/412/desktop comparing FO3-off (`data-game='FNV'`) rendering before vs after each unit — assert the visible panel set, the `.tab-visible` state, and key computed styles are identical. Plus the axis-inert-for-NV unit test (no `data-subtab` in NV DOM). | The FO3 axis is gated three ways (no `rails` data → stamper no-op → `_applyRails` no-op; every CSS rule `[data-game='FO3']`-scoped). If NV drifts, the offending FO3 rule isn't scoped — fix the selector. Revert the unit if needed (each is isolated). | | **a11y baseline-diff gate fails** (2nd-level tablist, `display:contents` scattered controls, knob/gauge/toggle as tabs) | The push-boundary a11y baseline-diff (part of `npm run gate`). Plan for it: lamps = one `role=tablist`, sub-rail = a distinct labelled `role=tablist`, each rail button `role=tab`/`aria-selected`; knob/gauge/toggle keep `role=tab` + literal `aria-label`s (never diegetic); focus-visible on all. | Fix ARIA in the same unit (Protocol 42). The mockup already models correct roles/labels — port them. Don't discover it at the gate; assert axe-clean per unit. | | **Cascade: 99-mobile overrides FO3 landscape** | Visual check at phone landscape + a gate test asserting FO3 casing selectors sit inside an `@media (orientation:landscape)` block in the FO3 file. | Resolve by specificity, never by reordering past 99-mobile. | | **Render pipeline forks** (Protocol 22/23) | Gate test: no second render path; boards keep one markup; the axis is a class toggle + CSS, not a renderer. Grep-guard that `selectSubtab` only toggles classes/attrs and reads `identity.rails`. | If a unit starts emitting parallel FO3 markup, stop — re-express as CSS re-layout of the shared board (the §3 principle). | | **Real-device landscape height** (emulator-invisible) | Owner loads U2 on his real phone (browser tab + installed PWA standalone) in landscape — Protocol 29-class explicit gate. | `100dvh` + safe-area insets; if chrome still clips, the interior scrolls internally rather than the page (never horizontal). | | **+1 tap on MANIFEST/SQUAD/QUESTS** | Design §6 flag; owner sign-off pending. | Spine keeps inline controls (parity). Master-detail is U5, optional. Inline primary-action glyph is the cheap middle path. **No tab ever goes read-only.** | | **UTF-8 mojibake** (SVG/box-drawing via PowerShell) | Suite 90. | Edit tool / Node `fs` only (Protocol 39). | | **Counter-rotated glass crispness** (cant) | Real-device text-crispness check. | Default cant to 0°; it's U4 cosmetic, safe to drop. | --- ## 9. Tests — what guards this - **Rails-are-data:** every id in `identity.rails` resolves to one real `.panel[data-tab]`; every rail'd board carries its `data-subtab` after stamping; the no-rail allowlist is exhaustive (weigh bridge / chassis / settings). - **Axis-inert-for-NV:** with `data-game='FNV'`, `_applyRailGrouping` stamps nothing, `_applyRails` no-ops, the FO3 hide rule matches zero elements; `selectSubsystem` routes identically to today. - **NV visual/DOM regression (U-REG):** panel-visibility + computed-style snapshot equal before/after at 360/412/desktop under FNV — the proof NV is unchanged. - **Rails-drive-visibility:** under FO3, each rail reveals exactly its board group and hides the rest. - **Subtab-persistence:** MetaStore pref restores the last sub-tab per subsystem on reload (Protocol UI-6), and it's a device pref, not `state` (Protocol 23). - **No-image-assets:** the Vault Boy view and casing use zero `<img>`/`url(...)` external assets (inline SVG only). - **Cascade-order:** FO3 file is slot 60 (after 55, before 99-mobile) — extends the existing `CSS_SPLIT_FILES` order test; FO3 casing selectors are inside a landscape query. - **Editing-survives:** limb toggle, HP/RAD steppers, SPECIAL steppers, faction/karma edits, manifest actions, book/curio toggles all hit the same state setters they hit today (per-tab, all 15). - **a11y roles:** two distinct labelled tablists; knob/gauge/toggle literal aria-labels; hotkeys `[1]`–`[5]`/`[0]` still route through `selectSubsystem`; `#go=` deep-links survive. - **Static source-invariant (Protocol 20):** the FO3 hide rule (`[data-game='FO3'] …:not(.subtab-active)`) and the `[data-game]` scoping can't be removed without a gate failure. --- ## 10. The temptation list — carried forward + extended The design §7 has 15 entries; this build **adds evidence** to them (each is now a concrete `identity` field or a concrete CSS/JS seam), and adds these observed-during-planning temptations: - **16. Board→sub-tab grouping as markup vs data.** Tempted to stamp `data-subtab="STATUS"` literally in index.html per board. Resolved: it's stamped *from* `identity.rails` data at runtime; the HTML carries only ids. *Future abstraction: rails is already the per-game composition table.* - **17. `selectSubsystem` growing an FO3 branch.** Tempted to `if (ctx==='FO3') renderRail()`. Resolved: one `_applyRails()` that no-ops on absent `rails` — the data's presence is the switch, not the game name. - **18. STATUS as a per-game render fork.** Tempted to write `renderStatusFO3()`. Resolved: STATUS is a CSS-revealed alternate markup block wired to the *same* limb/HP/RAD handlers; the render functions don't fork. - **19. Casing frame as a new shell.** Tempted to build a fresh FO3 DOM tree. Resolved: cosmetic wrapper divs + re-skinned existing navkeys, layered around the unchanged working shell (Protocol UI-7). **Owner real-device feedback pass adds these:** - **20. The lamp/gauge/knob/toggle re-skin needing the bezel markup reordered.** The real DOM is casing-top → `.bezel` → `.glass-frame` (the bezel sits *between* the header and the screen in the shared desktop/NV layout; it only becomes a bottom dock via `position:fixed` on mobile) — un-fixing it for the FO3 landscape shell left it rendering *above* the glass instead of below it. Tempted to physically move the `.bezel` markup after `.glass-frame` in index.html. Resolved: flex `order` (`casing-top:0`, `.glass-frame:1`, `.bezel:2`) reflows the visual stack without touching the DOM or NV's markup at all (Protocol UI-7) — the exact same "CSS re-layout of existing markup, not a new shell" discipline as item 19, applied to ordering instead of skinning. *Future abstraction: a per-game "visual stack order" is exactly the kind of thing `identity` should eventually carry, the same way `rails` already carries composition.* - **21. The left-rail/right-column casing layout (gauge+knob on one edge, toggle on the other, matching the reference mockup).** Tempted to give the bezel's individual buttons independent CSS Grid areas spanning the full device height (via `display:contents` cascaded down through `.bezel`/`.nav-cluster`), closing the "dead space on both sides" gap by filling it with real chrome instead of empty casing material. Resolved: deferred — this is a materially bigger structural change (multiple nested `display:contents` layers) than a bottom-row re-skin, and the actual dead-space cause turned out to be accumulated body/container padding, not a missing rail — so the fix that actually closes the gap (the full-viewport fixed shell, item 7 below) does not require it. Honestly disclosed as still-deferred casing-dressing scope, not silently dropped. - **22. Visually hiding the PWR/UPLINK/FAULT lamp text without touching accessibility.** The hood shrink needed the casing-top lamp row to read as tiny dots, not full-size text+dot buttons. Tempted to strip the text nodes via JS. Resolved: `font-size:0` on the button (the raw text collapses to zero size) with the icon `<i>` sized in `px` (unaffected by the parent's font-size) — a CSS-only visual declutter that never touches the DOM, the aria-label, or the a11y tree, mirroring the existing `#bezelTelemetry` clip-rect pattern already in this file. - **23. The lamp re-label (STATS/ITEMS/DATA) as a CSS `content:` swap or a JS-hardcoded string.** Tempted, since the existing casing-flavor/serial-flavor/fieldkit precedent (10-chrome.css) uses static markup spans toggled by `[data-game]` CSS. Resolved: the labels are pure `identity.navLamps` data (state.js), applied by a game-agnostic `_applyFo3NavLabels()` that is a complete no-op without that key — consistent with how `identity.statusStrip`/`identity.rails` already work, and avoids adding a fourth generic-vs-per-game static span pair to index.html for a value that's cheap to express as data instead. - **24. Exhaustively re-skinning every board's specific NV instrument classes (crt-mon, fader ladder, meters, tracker bars) bespoke, board by board.** The owner's "the glass still reads NV" feedback could have been chased board-by-board. Resolved: a small set of generic, board-agnostic rules (hide `.bay-slot-tag`/ `.bay-part-no`/`.board-led`; flatten `.crt-mon`'s scanline background; de-amber the fader ladder's top segment and the RAD drag trace) — matching the design's own "a small, consistent control kit, not a hundred bespoke reskins" constraint. Explicitly NOT exhaustive: many boards still carry their NV-styled meters/bars untouched — a real, disclosed scope boundary, not a silent gap. **The N-game abstraction is NOT built now** (design §7 discipline). FO3 is built concretely; the framework is extracted later from two real machines. The four disciplines hold: never hardcode `FO3`; the skin gets its own numbered CSS file; every flavor string in `identity`; this list stays maintained. --- ## 11. What I'd cut if it runs long In cut order (first cut first — mirrors design §9): 1. **The cant (−0.7°)** — U4, cosmetic, real-device-risky. Drop to 0°. 2. **The arm / desk framing** — pure decoration. 3. **Gauge sway / roller / knob-detent dressing** — U4 loses its texture, keeps flat casing. 4. **Master-detail (U5)** — keep the spine's inline-control rows (also *removes* the +1-tap flag entirely). 5. **Vault Boy STATUS view (U3)** — fall back to the 3 re-skinned stacked boards. STATUS still edits; it just isn't the figure. **This is the last thing cut** — it's the single most recognizable FO3 beat, so it's cut only under real pressure. **Never cut:** the rails second-axis (it's the whole IA), the austere interior, portrait=legacy, NV-unchanged, editing-survives, the real-device landscape gate. Those are the spine and the invariants. --- ## 12. Open questions for the owner (blockers before U5, not before the spine) 1. **Edit-flag class (design §6 / U5):** accept +1 first-action tap on MANIFEST/SQUAD/QUESTS (master-detail), keep strict parity with inline controls (cut U5), or ship the inline primary-action glyph (middle path)? *The spine ships inline-parity regardless, so this does not block getting it onto dev.* 2. **Serial/birthday scratch home** — left-rail micro-etch vs desktop-only (design §8/§10). Cosmetic, U4. 3. **Lamp sub-labels** (OPERATOR/OPERATIONS/DATABANK) visible vs aria-only. Cosmetic, U2. 4. **The cant** — keep pending real-device crispness, or default 0°. Cosmetic, U4. None block the spine (U0–U2). They gate polish units only.
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).