LIBRARY
library/TEST_CATALOG.md
sha256 42d03e5e34a51a00 · 226023 bytes ·
original held in the private archive
# RobCo U.O.S. — Test Catalog
> ## ⛓ BASELINE PIN — read this first
>
> | | |
> | --- | --- |
> | **Commit** | `9970cc8` (`9970cc85f1bad85a83a661208fce3100a1ea9568`) |
> | **Branch** | `dev` |
> | **Date** | 2026-07-20 (R4 re-pin) |
>
> **This catalog is accurate ONLY as of that commit.** Compare the hash above against
> `git rev-parse dev` — if they differ, the repo has moved and every concrete claim here is
> **degraded**: a reconstruction, not current truth. Re-verify against the code before relying on
> anything specific. That comparison is the whole point of the stamp; it replaces guessing.
>
> Nothing derivable is written here on purpose. If a fact can be computed — a cache rev, a test
> count, a line number, a file count — this doc points at the command instead of copying the
> answer, because a copied answer is a future lie. Its coverage is **partial** and its per-suite entries are frozen build narration — `tests/robco-diagnostics.js` is the only complete list of what exists.
> **Class: LIVE, hand-maintained — and deliberately NARROWER than it used to be.** Relocated out of
> `CLAUDE.md`'s tail by 2.8.5 U-B1 (2026-07-11) so a session stops auto-loading suite-by-suite history it
> usually does not need. `library/` is gitignored — this file exists only on the owner's machine.
>
> ### What this file is FOR (its job after Protocol 2a's retirement, R1 2026-07-20)
>
> Exactly one thing: **the qualitative WHY behind a suite** — what it covers, and the work-unit or
> incident that caused it to exist. That is un-derivable, which is the only reason a hand-maintained
> doc is allowed to hold it.
>
> ### What this file must NEVER carry again
>
> **Counts.** Not a test count, not a suite count, not a per-suite `N tests` figure — for this file or
> for any other. Protocol 2a required those to be hand-synced across eight-plus files and was retired
> as ritual that protected nothing; the numbers themselves were then stripped from the runner and from
> this file at R3, because an unmaintained number still reads as fact to the next session. **The runner's
> exit status is the only signal.** If you want a count, run `npm run gate:fast` and read it off the run.
>
> ### Coverage is PARTIAL — do not read absence as absence
>
> The runner holds substantially more suites than this file narrates. Some have a full per-suite entry,
> many appear only inside the one-line "Test suite:" summary below, and a large tail has no entry here
> at all. **`tests/robco-diagnostics.js` is the only complete list of what exists.** A suite missing
> from this catalog is a gap in the catalog, never evidence the suite is absent.
>
> ### Known-stale by construction
>
> Per-suite entries are frozen build narration written when each suite landed. Entries referencing the
> PowerShell mirror (`tests/robco-diagnostics.ps1`, deleted at 2.8.5 U-B3 with Protocol 15) are accurate
> as of their writing, not claims the mirror exists. Where this file and the runner disagree, **the code
> wins** (Protocol 3) — flag the drift, do not silently trust the doc.
>
> **The intended end state is GENERATED, not maintained** — a script that rebuilds this file from the
> runner. It is a separate, later unit (QUEUE item D), still blocked on the gitignored-`library/`-vs-CI
> diff tension: a clean CI checkout has no committed copy to diff against. Not solved here.
---
**Test suite:** a broad behavioural + static-invariant suite in the single canonical Node runner `tests/robco-diagnostics.js`, run by the pre-commit hook (via `npm run gate:fast`) and CI. (The former PowerShell mirror `tests/robco-diagnostics.ps1` was deleted and Protocol 15 — runner parity — retired in 2.8.5 U-B3.) Covers parser sanity, autoImportState coverage, faction registry, skill keys, save envelope, file maps, cloud sync, backward compatibility, registry structural integrity, reputation 2D matrix, C2 CRUD function existence, C3 CAMPG tab DOM binding, C4 Protocol 4 campaignMode (binary) + separation, render contracts, CSS invariants, SW invariants, structural integrity (Protocol 20 static guards), detail-current dedup guard (Protocol 27), FO3 database structural integrity, CSV column-count integrity, security regression guards (XSS-1/XSS-2/XSS-3), critical feature presence and SW update banner regression (Suites 22-41, 49-130 gate guards: UI controls, prohibited patterns, protocol completeness, AI contract lock, architectural boundaries, assets completeness, meta/single-runner guard (Suite 28 — guards that the single canonical runner stays single: the deleted PowerShell mirror and the deleted RULES.md cannot silently return, and the stripped per-suite count comments cannot come back. Its former end-of-run count reconciliation against CHANGELOG.md was DELETED with Protocol 2a's retirement, R1 2026-07-20 — no count is reconciled anywhere now), SW update banner, Phase 1b guards, CI/CD automation guards, command registry, chem-boost, optics/empty-state/utility CSS, Phase 2c CSS hygiene, Phase 3a perf guards, keyboard shortcuts group), DB↔registry weapon parity (Suite 38), ammo token split (Suite 39), inventory category filter + mod type (Suite 40), weapon mods CSV + registry parity (Suite 41), native command router (Suite 42), GAME_DEFS structural integrity (Suite 43), anonymous auth + security rules + XSS coercion fix (Suite 44, including no-double-escape behavioral regression), Google sign-in + account panel (Suite 45: GoogleAuthProvider, linkWithPopup (popup-only, no redirect), getRedirectResult, signOut + re-anon, collision recovery, renderAccount, ACCOUNT panel, CSP apis.google.com, hardened boot order, gesture safety), cloud save picker + local migration (Suite 46: addDoc additive upload, _contentHash dedup, syncLocalSavesToCloud, loadCloudSave confirm+sanitize+migrate, deleteCloudSave confirm, renameCloudSave updateDoc, renderSavesList, #savesListBody), Gemini key sync + AI Studio link (Suite 47: saveGeminiKeyToCloud, loadGeminiKeyFromCloud, anonymous+toggle guard, secrets path, setGeminiKeySync, robco_gemini_key_sync local mirror, geminiKeySyncToggle, AI Studio link + rel=noopener, NAME button, dateStr regression guard), remote kill-switch + client auto-disable (Suite 48: loadRemoteConfig, config/flags path, timeout race, try/catch fail-open, not-awaited boot, isFeatureEnabled fail-open, robco_feature_flags LKG, aiChat gate in transmitMessage, cloudSync gate, _recordFeatureFailure + FAIL_THRESHOLD, firestore.rules config rule), CI/repo hardening guards (Suite 49: asset-manifest completeness js/+css/, Firestore no-allow-all, release.yml CI gating, deploy.yml *.png glob guard), gate parity guards (Suite 50: pre-commit invokes npm run gate:fast, gate enforces --max-warnings 0, boot-smoke uses HTTP, package.json has gate script, gate.js has --fast flag, gate.js powershell fallback, pre-push invokes full gate, install-hooks installs pre-push), save integrity + rolling backups (Suite 51: computeSaveChecksum FNV-1a helper, verifySaveEnvelope legacy/future_version/checksum_mismatch returns, exportSaveFile/saveToSlot checksum+schemaVersion stamp, loadFromSlot/handleFileUpload/pullFromCloud/loadCloudSave integrity check + rolling backup, restoreRollingBackup sanitize+migrate path, robco_backup_1/2/3 ring + ptr, QuotaExceededError drop-oldest-retry, index.html restore button, no duplicate contentHash in cloud.js), repo/site enrichment guards (Suite 52: repomix.config.json exists+valid JSON+include+ignore tuning, .nojekyll+robots.txt+404.html+PRIVACY.md exist, manifest.json has description+categories, manifest.json start_url/scope pinned to './' (Protocol 36b-adjacent, added alongside the r16 orientation-lock fix), README CI badge, Protocol 37 in RULES.md), AI + Gemini-key resilience (Suite 53: _AI_RETRY_MAX constant, _AI_RETRY_DELAYS_MS exponential array, fetchAuthorizedModels 401/403 explicit check, auth-failure returns before saveApiKeySilent, REJECTED message, transmitMessage 401/403 no-setTimeout guard, 429 RATE LIMIT message, bounded _AI_RETRY_MAX references, _validateTriNode defined+called before autoImportState, behavioral null/array/valid-object checks), prompt-injection hardening + input caps + quota warning (Suite 54: getSystemDirective injection-resistance section, player-input data delimiter in transmitMessage, #chatInput maxlength ≥4000, newItemName/newQuestName/newPerkName/newCampaignNote maxlength caps, JS userText.length guard, saveState QuotaExceededError catch + appendToChat warning), CSP Stage 2 origin guards + Firebase SDK pin (Suite 55: enforcing CSP present + report-only absent regression guard, load-bearing CSP origin checks for Gemini/Firebase Auth/token refresh/Firestore/App Check/gstatic/apis.google.com + nv-overlord, object-src/base-uri/frame-ancestors 'none' directives, unsafe-inline tripwire vs sha256/nonce additions, blob: in img-src, Firebase firebasejs version-pin guard), UI module split + boot-loader migration guards (Suite 56: js/ui-audio.js, js/ui-render.js, js/ui-saves.js, and js/ui-account.js file-exist guards, each in sw.js ASSETS, script tags in index.html, load-order guards — each before api.js and before ui-core.js; js/ui-core.js in sw.js ASSETS; js/ui.js must NOT exist on disk; document.write absent; dynamic createElement/appendChild/async=false injection present; all five boot-script paths referenced; activeContext/try-catch/FNV-default/FO3 fail-safe guards), PWA app shortcuts guards (Suite 57: manifest.shortcuts array length 4 (Data removed), all 4 shortcut names+urls correct (Comm-Link/Inventory/Stats/New Campaign), offline-safe ./#go= hash mechanism, SHORTCUT_ROUTES const + routeLaunchShortcut function in ui-core.js (data route kept in code), /^go=/ allow-list regex + no eval/innerHTML in routing fn, New Campaign → wipeTerminal + double-confirm gate regression, tab routes via switchTab, routeLaunchShortcut called after initTabs boot-order guard, history.replaceState reload-safety, custom per-shortcut icon src guards, shortcut icon file-exist + sw.js ASSETS guards, app icon.png file-exist guard), client error ring-buffer + [LOGS] command (Suite 58: ERROR_LOG_KEY + ERROR_LOG_CAP constants, _recordError function + localStorage-only write, showErrorLog function + sysModal reuse, NATIVE_COMMAND_ROUTER [LOGS] entry, no-exfil guard — no fetch/XMLHttpRequest/sendBeacon in _recordError or showErrorLog), inline handler integrity (Suite 59: on* attribute extraction from index.html, definition-anchored resolution — not substring match — per name regex function\s+NAME\b|NAME\s*=|window\.NAME\s*=, ≥20 unique handler names found, all resolve as definitions in js/*.js), a11y baseline-diff gate (Suite 60: @axe-core/playwright in devDependencies, tests/a11y-check.mjs exists, tests/a11y-baseline.json exists + valid JSON, scripts/gate.js invokes a11y-check.mjs in non-fast block, package.json has a11y script), mobile layout overflow guards (Suite 61: .main-grid minmax(0,1fr) grid track, .list-row-content overflow-wrap+word-break, .inventory-list li > span min-width+overflow-wrap, .panel overflow-wrap, mobile @media overflow-x:clip for col-left/col-right), changelog viewer guards (Suite 62: tests — env-aware [Unreleased] gate prod-hide/dev-show via _visibleChangelog + _isStagingEnv fail-safe default, cf-staging robco-env marker / prod none, runtime-fetched-asset publish+precache invariant, plus WU-C11 glow-up: _parseChangelog + _showChangelogModal structured renderer, version <select> dropdown, collapsible category <details class="changelog-cat">, diegetic FIRMWARE REVISION LOG + [+]/[*]/[~]/[-] tags, escapeHtml on entries, source-order preserved (no reverse/sort), addEventListener change wiring, ~700px reading-column CSS + changelog-wide modal mode + closeModal cleanup; WU-C15 locked WIDTH + expand/collapse-all toggle + default-open-newest; and the WU-C15 follow-up locked HEIGHT — .modal-box.changelog-wide pins min(85vh,820px) + box-sizing:border-box so collapse/expand keeps the box dimensions constant, content scrolls internally, CLOSE pinned, no 360/412 overflow), save/cloud UI consolidation guards (Suite 63: saveCurrentToCloud additive addDoc + isAnonymous guard + contentHash dedup, renderSavesList unified local+cloud list, #savesListBody mount point, #btnSaveToCloud), SPECIAL stats editable guards (Suite 64: all 7 s_s..s_l inputs have onchange=commitStat + oninput=capStatMax, no oninput clampStat regression, commitStat defined+1-10 clamp+updateMath+saveState+isNaN/state[k]||5 revert, all 7 have inputmode=numeric, clampStat absent, capStatMax defined+n>10 upper-only+no lower force, syncStateFromDom Math.max/min clamp), blocking update modal guards (Suite 65: tests — id=updateModal replaces updateBanner, role=dialog+aria-modal+aria-labelledby, _triggerUpdate focus trap Tab+Esc block, Case A/B/C &&controller call-site regression guards, Case C already-installing-worker prompt fix via _watch(reg.installing) + one-shot _updatePromptShown idempotency guard, !modal||!btn fail-safe, ui-core.js ESC handler does not reference updateModal, #updateModalMsg text-align:left), FO3 Lincoln memorabilia tracker (Suite 66: tests since Health-U3 slice 1 — state.lincolnItems {} default+migration, reg_fo3.js lincolnMemorabilia 9 items, reg_nv.js excludes it, GAME_DEFS.FO3.tracksLincoln, renderLincolnMemorabilia defined+loadUI+tracksLincoln guard, toggleLincolnItem+setLincolnDisposition+vocab validation, #lincolnMemorabiliaDisplay, getSystemDirective lincolnItems, LINCOLN_VOCAB excludes 'other', opts excludes OTHER; the four static autoImportState greps 66.3-66.5/66.20 — plain-object check, vocab, registry-key filter, 'other'→'found' coercion (comment-satisfiable) — CONVERTED to behavioral, now Suite 76's 76.12-76.14), FNV Traits tracker + trait name filter (Suite 67: state.traits [] default+migration, autoImportState registry-validated+dedup array, reg_nv.js traits 16 items, reg_fo3.js excludes it, GAME_DEFS.FNV.hasTraits, renderTraits defined+loadUI+hasTraits guard, toggleTrait+soft-cap warn, #traitsDisplay distinct from #perksList, getSystemDirective traits, #traitFilter input, renderTraits substring-filter), FNV location database expansion (Suite 68: FALLOUT_REGISTRY.locations ≥108 entries, seed examples present — Jean Sky Diving/Jack Rabbit Springs/Powder Ganger Camp South/Wolfhorn Ranch/Ivanpah Dry Lake, type validity, no duplicates, zone [4,2] parity Goodsprings Cave, zone [4,1] parity Powder Ganger Camp East), FO3 game-switch regression (Suite 69: onGameContextChange sets state.gameContext=ctx + window._contextSwitching guard, beforeunload early-exit guard, saveState debounce early-exit guard), FNV unique apparel + Vault 13 Canteen seed (Suite 70: ARMOR floor ≥103, MISC Vault 13 Canteen, named mandated items Benny's Suit/Suave Gambler Hat/Vault 13 Canteen registry present, no-dupe check, 7-col ARMOR integrity, DB↔registry parity for mandated items, seedNewCampaignInventory defined+FNV guard+inventory guard+ticks guard, WU-D2 Mysterious Stranger Outfit DT=0 + WU-D6 1st Recon DT=15 regressions), Phase 6 UI consistency (Suite 71: tests — #traitsSection is <details.sub-panel>+data-sub-id, renderTraits compact inline effect+no-inline-flex, #collectiblesSubPanel+#lincolnSubPanel as sub-panels, sub-panel persistence+fail-safe+default-collapsed via robco_panel_state, Lincoln compact rows+no-dup-header+data-lname onclick safety+setLincolnDisposition re-render guard+no-inline-flex, faction lone-card CSS nth-child(odd)), location datalist bleed fix + update-modal whitespace (Suite 72: static nv_locations datalist removed → dynamic #locationOptions from FALLOUT_REGISTRY.locations+escapeHtml+initLocationDatalist call, bleed-class regression guard no static option children in any datalist, #updateModalMsg white-space:normal+single-line content guard), skills panel game-aware render (Suite 73: tests — #skillsGrid empty container in index.html, no static sk_guns/sk_survival ids, renderSkills() defined+getSkillKeys()+SKILL_LABELS+escapeHtml+called from loadUI, SKILL_LABELS covers all 15 keys, big_guns→'Big Guns'/small_guns→'Small Guns'/guns→'Guns'/survival→'Survival', syncStateFromDom getSkillKeys regression guard), collectible coord guards (Suite 74: tests — every FNV snow globe + FO3 bobblehead + lincolnMemorabilia entry has gridRow/gridCol in 1..6 matching an existing zones[] cell, counts unchanged 7/20/9, no dupes, renderWorldMap badge is coord-based not name-based, uncollected bobblehead flags its cell, all FO3 zone names unique (WU-D1 — no duplicate world-map region labels), WU-D5 fabricated 'Vault 92 South' → real 'Bethesda Offices East'), registry items[] no-duplicate guard (Suite 75: tests — FNV items[] no duplicate name+type pairs, FO3 items[] no duplicate name+type pairs, Rebound weapon appears exactly once regression guard), autoImportState hardening guards — BEHAVIORAL since Health-U3 slice 1, 2026-07-16 (Suite 76: tests — F2 equipped explicit-null clear EXECUTED in a vm sandbox (Suite 133/229 idiom, real state.js + reg_nv): null clears only the sent slot / all three slots / positive re-equip unregressed, old || short-circuit absence kept static; F3 collectibles registry pins kept static (behavioral twins: Suite 133 + 229.7/229.8 + test.html Suite 6); F1 status-type whitelist EXECUTED: 'banana'→BUFF coercion, lowercase 'debuff'/'neutral' normalized not rejected, bare-string entry → {name,ticks:0,type:'BUFF'}, raw passthrough absence kept static; Lincoln disposition validation EXECUTED against real reg_fo3 with gameContext FO3 (converted from Suite 66's 66.3-66.5/66.20): legacy 'other'→'found', registry-key + vocab filtering drops fake keys and bogus dispositions, array-shaped payload ignored), faction reputation regression + ±5 increment (Suite 77: tests — FACTION_THRESHOLDS 3-breakpoint canonical NV scheme only for ncr/legion/bos, _DEFAULT_THRESHOLDS generic 8/25/50, getFactionStanding 4×4 MATRIX lookup, no adjustFaction ±50 in ui-render.js), VENDORS.CSV structural integrity (Suite 78: tests — 39 data rows, all rows 7 columns, sentinel names Gloria Van Graff/Joshua Graham/Doctor Usanagi/Quartermaster Bardon/Street Vendor), FO3 location database expansion (Suite 79: tests — FALLOUT_REGISTRY.locations (FO3) count === 90, all entries non-empty names, all types valid {settlement/landmark/base/factory/vault/other}, sentinels The Pitt/Point Lookout/Mothership Zeta/Andale/Vault 106, Georgtown West typo removed + Georgetown West present), CHEMS.CSV consumables expansion (Suite 80: tests — 76 data rows, all rows exactly 8 fields (stray-comma guard), sentinels Cram/Sunset Sarsaparilla/Rebound/Wasteland Omelet/Sierra Madre Martini, lookupItemInDb column-mapping spot-checks wgt/val/type for Sunset Sarsaparilla + MRE), FO3 [ARMOR.CSV] (Suite 81: tests — 61 data rows (WU-D2: 62→61, removed NV-bleed 'NCR Ranger Armor'), all rows exactly 7 fields (stray-comma guard), sentinels Combat Helmet/Samurai Armor/T-51b Power Helmet/Ghoul Mask/Composite Recon Armor, lookupItemInDb column-mapping spot-checks wgt/val/type for Combat Helmet + Samurai Armor, NV-bleed guard no NCR-faction armor in FO3), FO3 quest canon corrections + quest items (Suite 82: tests — quests 64 (WU-D1: removed non-canon 'Fires of Anchorage' + fabricated duplicate 'Strictly Business (Paradise Falls)'; WU-D5: added the 2 real missing Operation: Anchorage quests 'The Guns of Anchorage' + 'Paving the Way', all fallout.wiki-verified) with DLC quests for all five add-ons, dedup guard for 'Strictly Business' + canon-removal regression guard + Operation: Anchorage 4-quest per-add-on completeness guard, DLC sentinels + dlc field check + type validity, QUEST_ITEMS 15→25 data rows, 5-field stray-comma guard, sentinels Steel Ingot/Krivbeknih/Cryo Key, lookupItemInDb Steel Ingot wgt spot-check), crafting recipe + breakdown registry data (Suite 83: tests — NV 25 recipes (workbench/campfire/recycling) + 12 breakdowns, FO3 7 workbench schematics; station/skillReq/sentinel/no-dup guards for both games), craft panel UI + batch craft/scrap mechanics (Suite 84: tests — renderCraft defined+called from loadUI, renderCraftCard+renderScrapCard defined, craftRecipeSelect with optgroup station grouping+scrapItemSelect picker, _craftGetHave ammo-first+inventory fallback, _craftConsume clamp ≥0+splice-at-0, craftSetMax input assignment, doCraft confirm-gate+ingredient-check+consume+ammo vs inventory route+no pushToCloud, doScrap confirm-gate+have-check+consume+yields+no pushToCloud, doCraft no state.skills hard-block (soft gating), #craftPanel+craft_breakdown sub-panel+data-sub-id, expandPanelForCategory craft key, _updatePanelBadges CRAFTING entry), skill books tracker READ/UNREAD split (Suite 85: tests — reg_nv.js+reg_fo3.js skillBooks 13-item arrays with skill key mapping, FNV Wasteland Survival Guide→survival + FO3 U.S. Army: 30 Handy Flamethrower Recipes→big_guns sentinels, Guns and Bullets different skill per game, state.skillBooks [] default+migration, autoImportState registry-validated+dedup array, getSystemDirective unconditional, renderSkillBooks defined+loadUI wiring, #skillBooksDisplay container, skill_books_read/unread sub-panel markers, toggle persistence wiring, static skillBooksSubPanel removed, read.includes split correctness, NO BOOKS READ/ALL BOOKS READ empty states), maskable shortcut icons + OPTICS label wrap (Suite 86: tests — manifest shortcut icon purpose=maskable for all 4 shortcuts, terminal.css .optics-label white-space:nowrap, OPTICS label static guard), NV Skill Magazines tracker (Suite 87: tests — reg_nv.js magazines 14 entries+skill keys+no-dupe+sentinels, reg_fo3.js exclusion guard, state default+migrate, autoImportState registry-validated+dedup, getSystemDirective FNV-only, renderMagazines defined+sub-panels+toggle wiring+empty states, toggleMagazine defined, GAME_DEFS.FNV.hasMagazines, #magazinesDisplay), UI consistency structural guards (Suite 88: tests — every details.panel has <summary><h2> starting with ">", every details.sub-panel has data-sub-id, no <span onclick="toggle..."> tracker pattern, all 5 tracker render functions use .tracker-row class, renderFactionRep MINOR FACTIONS sub-panel+data-sub-id, ±5 not ±50 faction button increment, _updatePanelBadges [n/total] for SKILL BOOKS + SKILL MAGAZINES, terminal.css button.tracker-toggle class), game-agnostic refactor guards (Suite 89: tests — api.js no two-game coercion, _nativeCrossroads uses getFactionRegistry() not hardcoded key arrays, index.html no two-game coercion, ui-core.js no two-game coercion, onGameContextChange uses !GAME_DEFS[ctx], seedNewCampaignInventory no ctx !== 'FNV' guard, seedNewCampaignInventory reads GAME_DEFS, wipeTerminal uses Object.values(GAME_DEFS), GAME_DEFS.FNV has seedInventory, GAME_DEFS.FO3 has seedInventory, no hardcoded faction key array in _nativeCrossroads, RULES.md contains Protocol 38), UTF-8 corruption guard (Suite 90: tests — api.js/state.js/ui-core.js/ui-render.js/ui-saves.js/ui-audio.js/ui-account.js/index.html/README.md/ARCHITECTURE.md each checked for U+FFFD replacement char and â€/â– double-encoding sequences caused by PowerShell Latin-1 read + UTF-8 write; CHANGELOG.md checked with full-sequence matcher only to avoid false-positives on intentional examples in Protocol 39 docs), loadUI dirty-check / targeted re-render guards (Suite 91: tests — _renderSig module-level cache, _isDirty() helper, _clearRenderCache() force-render path, _isDirty wired in loadUI, renderWorldMap tab-btn-data guard, renderWorldMap not unconditional, renderAccount + renderSavesList always-called), vertical-broken-text anti-recurrence guards (Suite 92: tests — .tag class white-space:nowrap in terminal.css, .badge class white-space:nowrap in terminal.css, inventory type-tag uses .tag in ui-render.js, map-collectible-badge includes badge class in ui-render.js, .macro-buttons button white-space:nowrap (WU-C12 device-wrap fix guard), .rng-mode-group > label white-space:nowrap + .rng-mode-group flex-direction:column (WU-C14 COMPLETE RNG label desktop-crush fix)), FO3 autocomplete guard (Suite 93: tests — registry-core.js file-exist + sw.js ASSETS + registrySearch fn + FALLOUT_REGISTRY[category] game-agnostic + reg_nv.js no-duplicate + FO3/FNV boot-path index.html guards + FO3-context behavioral registrySearch("quests","galaxy") returns Galaxy News Radio), accessibility guards (Suite 94: tests — :focus-visible rule in terminal.css + prefers-reduced-motion block + animation-duration 0.01ms + animation-iteration-count 1 + .crt-overlay restore + #chatDisplay aria-live=polite + aria-atomic=false + #sysModal role=dialog + aria-modal=true + _openSysModal() defined in ui-core.js), WU-B9 cloud.js → state.js boundary fix (Suite 101: tests — state.js defines + exposes getGameContext() and snapshotActiveCampaign() accessors, getGameContext() preserves the `|| 'FNV'` fallback, snapshotActiveCampaign() is the single-source robco_v8 builder using getGameContext() + deep-snapshot, cloud.js no longer reads global state.gameContext or JSON.stringify(state) directly — routes through the accessor, Protocol 23 boundary restored), WU-B10 boot-drone autoplay timing (Suite 102: tests — `_bootActive` boot-window flag declared default-false, runBootSequence opens it (_bootActive=true) before arming the drone + closes it (_bootActive=false) at completion, _onFirstInteract returns early on !_bootActive before _tryDrone() (stale post-boot drone suppressed), playBootDrone still arms the drone to the first click/keydown gesture (autoplay-policy-safe), behavioral gate replica — boot-active first gesture plays once / post-boot first gesture suppressed), WU-C13 SAVE MENU "?" help affordance (Suite 103: tests — index.html save-panel "?" button with onclick=showSaveHelpModal + descriptive aria-label + .btn-sm ≥28px tap target, showSaveHelpModal() defined and opens via _openSysModal (WU-C4 focus-trap + ARIA), SAVE_HELP documents every save action (export/import/restore/slots/cloud push+pull), escapeHtml on help text, game-agnostic copy no FNV/FO3/Fallout/New Vegas literals), WU-D4 deterministic-feature coefficients (Suite 104: tests — fallout.wiki-verified coefficients in GAME_DEFS that feed the Phase-N natives: barter buy/sell multipliers buyBase 1.55 / sellBase 0.45 / slope 0.0045 for both games with canon-range guards (buy never below value, positive vendor margin, sell ∈ [0.45,0.90]); VATS critBonus FNV 0.05 vs FO3 0.15 + hit-% clamp 5–95 + skill-spread breakpoints 50/100; ammoPerAttack default 1 (positive integer); and the WU-D4a-RANGED-GAP Protocol-3 flag documented in both entries — per-weapon ranged spread/falloff is unsourceable, so an exact ranged hit-% is not locked), WU-N1 VATS native calculator (Suite 105: tests — the deterministic V.A.T.S. calc consuming the WU-D4a coefficients: GAME_DEFS combatSkills present + valid per game with the GA-10 FO3 big_guns live-bug regression (FO3 includes small_guns + big_guns, never collapses to FNV's `guns`); vats.regions {name,mod,ap} table + AP formula apBase 65 / apPerAgility 3 FNV / 2 FO3 (canon AP pool 95/85 at AGI 10); recomputeVATS reads the hit-% clamp + critBonus + regions from GAME_DEFS (GA-7, no hardcoded table); the §1.6 melee-scope gate shape `playstyle === 'melee' || weaponIsMelee`; the dropped "DETERMINED BY AI" deferral label; the real #vatsTargetDT input replacing dead targetDT=0 (QA-DEAD-6); lookupWeaponStats in both db runners; and read-only-ness), WU-N2 TRADE native barter terminal (Suite 106: tests — the deterministic offline barter terminal consuming the WU-D4b coefficients: getVendors/getTradeCatalog in both db runners; _tradeBuyPrice/_tradeSellPrice read GAME_DEFS[ctx].barter (Protocol 38); behavioral canon invariants buy ≥ value + sell < buy across barter 0..100 for both games; buy floored at 1 / sell clamped ≥ 0; additive + confirm-gated buy/sell (Protocol 34); the caps-revert regression fix — doBuy/doSell mirror new caps to #c_caps so saveState/syncStateFromDom can't revert it (Protocol 42); no auto-cloud-push; the retired AI TRADE modal (mType==='TRADE' branch + TRADE directive shape removed, dead tradeItem deleted); #tradePanel + BARTER UPLINK + trade_buy/trade_sell sub-panels; [TRADE] button repointed to the native panel; expandPanelForCategory trade route + loadUI wiring; and the .trade-row min-width:0 overflow guard), WU-N3 THREAT native bestiary + TTK (Suite 107: tests — the deterministic offline THREAT assessment: lookupBestiaryEntry in both db runners; renderThreat + the pure _threatCompute math helper; [THREAT]/[TH] routed via NATIVE_COMMAND_ROUTER → renderThreat with the AI TTK path retired (legacy "predictive loops" directive removed + native-defer note); _routeNativeCommand forwards the typed target; NO ENTRY IN BESTIARY refusal (Protocol 3); ammoPerAttack read from GAME_DEFS (game-agnostic, Protocol 38); _vatsIsMelee reuse for the strikes-vs-rounds melee-scope label (Protocol 22); and a vm + PowerShell behavioral twin proving TTK = ceil(HP/max(1,DPS−DT)), the DT≥output floor, the WU-D4c ammo-per-attack coefficient, and the no-weapon path), WU-N4 CONSULT native databank lookup (Suite 108: tests — `> CONSULT <topic>` (+ [CONSULT] / [CON]) routed via NATIVE_COMMAND_ROUTER → renderConsult; registry search across items/perks/quests/locations/companions + DB cross-reference (lookupItemInDb/lookupBestiaryEntry/lookupWeaponStats); the NO ENTRY IN DATABANK refusal (Protocol 3 — never invents); XSS-safe topic escaping (escapeHtml(q) + escaped hit names); read-only-ness; Protocol-38 agnosticism (no FNV/FO3/Fallout literals — registry-driven, verified via a stable source slice with case-sensitive matching so the sanctioned FALLOUT_REGISTRY API is allowed); COMMAND_REGISTRY discoverability; shared _openSysModal entry point; and the .consult-card / .consult-hit-name min-width:0 overflow guard), WU-N5 BIO-SCAN native medical advisory (Suite 109: tests — `> [BIO-SCAN]` (+ [BIO]) routed via NATIVE_COMMAND_ROUTER → renderBioScan; the pure _bioScanCompute core (behavioral eval: crippled limb + critical HP + radiation tier + active-addiction advisories, med items sourced from CHEMS, healthy → nominal); getChemsTable() defined in BOTH db_nv.js + db_fo3.js (game-agnostic parity); read-only; XSS-safe escaping (escapeHtml on advisory + limb text); Protocol-38 agnosticism (no FNV/FO3/Fallout or hardcoded Stimpak/RadAway/Med-X literals — med items derived from CHEMS Effect text); the retired AI BIO-SCAN path (getSystemDirective defers to the native calculator); COMMAND_REGISTRY discoverability; shared _openSysModal; the RUN BIO-SCAN button + aria-label; and the .bio-card / .bio-limb-name min-width:0 overflow guard), WU-E2 reusable disabled banner templates (Suite 112: tests — owner override "never delete": #updateBannerTemplate + #fo3WarningBannerTemplate `<template>` elements still EXIST in index.html, the .update-banner + #fo3WarningBanner markup is wrapped INSIDE those templates (disabled-by-default — `<template>` content is inert and never renders on its own), #fo3WarningBanner additionally carries display:none, the .update-banner + .fo3-warning-banner CSS rules are preserved in terminal.css, and each banner handle appears exactly once and only inside its template — no active rendered banner outside a template), WU-E3 FEATURES / command-reference consistency (Suite 113: tests — COMMAND_REGISTRY (ui-core.js) ↔ NATIVE_COMMAND_ROUTER (api.js) ↔ the [FEATURES] help modal kept in lock-step: a "NATIVE TERMINALS — OFFLINE, NO AI" group surfaces all six native terminals, each marked Offline; every router-native token (VATS SIM/THREAT/BIO-SCAN/LOOT/CONSULT) resolves in the router AND appears in help; [FEATURES] → showHelpModal renders COMMAND_REGISTRY; TRADE is advertised as the barter PANEL and correctly absent from the router; and the retired AI macros — VVATS/TACTICS/SYNC/STASH/EXCESS/CURRENCY/AUDIT/TIMER-CHEM/SQUAD/TRAVEL CLUSTER/CASINO/COMM LINK/PAUSE/PAGE 2-3/ARCHIVE — stay absent from the registry), map location discovery persistence (Suite 114: tests — fog-of-war world-map status: the shared state.js recordLocationVisit() helper records a location the moment it is/was current (case-insensitive dedup, permanent — never truncates), both the manual onLocationChange() path (records the left + new location) and the AI autoImportState() path route through it, renderWorldMap renders [CURRENT]/[VISITED]/[UNKNOWN] from state.locationHistory, sanitizeImportedContainer coerces locationHistory as a string[] (Protocol 4), and no destructive slice(-N) cap survives anywhere so discovered locations are never un-discovered), WU-F1 Sustained Power Cell / Screen Wake Lock (Suite 115: tests — WAKE_LOCK_KEY + isWakeLockEnabled() localStorage device preference (not campaign state), _wakeLockSupported() feature-detects before use, _acquireWakeLock() requests a 'screen' lock guarded + try/catch (fail-soft), toggleWakeLock persists + acquires/releases and #wakeLockToggle onchange wires to it, a visibilitychange listener re-acquires on tab-visible, initWakeLock() disables the toggle as graceful fallback when unsupported, _releaseWakeLock() never throws + initWakeLock() called from boot, and the POWER MANAGEMENT sub-panel has #wakeLockToggle + label + #wakeLockStatus note), WU-F2 Haptic Solenoid / Vibration API (Suite 116: tests — HAPTIC_KEY + isHapticEnabled() opt-in (default OFF) localStorage device preference (not campaign state), _hapticSupported() feature-detects navigator.vibrate before use, _hapticReducedMotion() reads the prefers-reduced-motion media query, triggerHaptic() no-ops when unsupported / when the pref is off / AND when reduced-motion is set + wraps navigator.vibrate in try/catch, toggleHaptic persists + fires a confirmation buzz and #hapticToggle onchange wires to it, initHaptic() disables the toggle as a graceful fallback when unsupported + is called from boot, fire points on level-up + faction-threshold alert (api.js) and the critical-HP crossing (ui-core.js), and the POWER MANAGEMENT sub-panel has #hapticToggle + label + #hapticStatus note), WU-F3 Eject Holotape / Web Share API (Suite 117: tests — _buildHolotapeText() is the single transcript builder reused by the .txt export (Protocol 22, no duplication), _shareSupported() feature-detects navigator.share before use, _clipboardSupported() feature-detects navigator.clipboard.writeText, ejectHolotape() shares the transcript via navigator.share({text}) guarded + inside try/catch, a dismissed share sheet (AbortError) returns silently without falling through, a three-tier graceful fallback chain share → clipboard copy → _downloadBlob file eject, an empty-log NOTHING TO EJECT refusal, and the #ejectHolotapeBtn diegetic button wiring onclick=ejectHolotape() with a descriptive aria-label), WU-F4 Pending-Directives Tally / Badging API (Suite 118: tests — _badgeSupported() feature-detects navigator.setAppBadge + clearAppBadge before use, _pendingDirectivesCount() (active quests) is the single source reused by the QUEST LOG panel badge (Protocol 22), _updateAppBadge() returns early as a graceful no-op when the Badging API is unavailable, posts the count while the terminal is hidden + clears it while visible inside try/catch, swallows a rejecting setAppBadge/clearAppBadge promise (no unhandled rejection), clears the badge at zero pending (no stale count), is wired into _updatePanelBadges (render) + a visibilitychange listener, and the manifest keeps display:standalone — the installed-PWA prerequisite for app badges), WU-F7 Overseer's Maintenance Log (Suite 119: tests — OVERSEER_LOG_KEY + _readOverseerLog() back the log with a localStorage device stat (not campaign state) and return zeroes on parse failure (never throws), _writeOverseerLog() persists inside try/catch (a quota-full or disabled store never throws), initOverseerLog() bumps the boot count exactly once (guarded by _overseerBooted) + starts the session clock + is wired into boot, _flushOverseerLog() recomputes the running total from _overseerBaseMs + session (idempotent, no double-count) + tracks the longest session, renderOverseerLog() renders CURRENT UPTIME / LONGEST SESSION / TOTAL POWER-ON / BOOT COUNT into #overseerLogDisplay via _fmtOverseerDuration, the totals flush on visibilitychange-hidden + pagehide so they survive a closed tab, the read-out stays fresh via a loadUI call + a periodic timer, and the OVERSEER'S LOG DATA-tab panel (summary>h2 with ">") carries #overseerLogDisplay + a local-only note and is game-agnostic), WU-F8 High-Lumen Optics (Suite 120: tests — a pure-CSS AA+ high-contrast mode reachable two ways that apply identical boosts: a manual html.high-lumen class (persisted as the robco_high_lumen localStorage device pref) AND the OS @media (prefers-contrast: more); both force a pure-black #000 background + remove the glow halo (--robco-glow: none) + lift every dimmed surface (inactive tabs, list prefixes, inactive toggles, tracker-meta, filter buttons, empty-states) to opacity:1 + drop the scanline/refresh CRT overlay to opacity 0.50; HIGH_LUMEN_KEY + isHighLumenEnabled() back the toggle, toggleHighLumen persists + _applyHighLumen flips the class on <html>, #highLumenToggle onchange wires to it, initHighLumen restores the pref to the checkbox + is wired into boot, the inline head script applies high-lumen before first paint (no flash), and the accessible #highLumenToggle + label[for] + #highLumenStatus note is game-agnostic), WU-F5 Pip-Boy Radio / synthesized zero-byte WebAudio station (Suite 121: tests — the station bed is fully synthesized from WebAudio buffer/oscillator nodes with NO audio file (no <audio>/Audio()/.mp3/.ogg/.wav), startRadio() guards on masterMute + the AudioSettings.radio ON-pref + no double-start and reuses ensureAudioCtx(), AudioSettings.radio is seeded from localStorage robco_radio_on (opt-in ON-semantics device preference, NOT a mute key in the toggleAudio map), toggleRadio persists the pref + starts/stops and #radioToggle onchange wires to it, master mute stops the radio (pref preserved) and un-mute resumes it when on, initRadio() does an autoplay-safe one-shot first-gesture restore of a saved on-pref and is wired into boot, a looping static bed + a self-rescheduling tonal-motif generator drive the station, and the #radioToggle + label + #radioStatus UI plus the synth carry no game-specific station literals — Protocol 38), WU-F6 Cold-Start / Degraded-Tube Boot (Suite 122: tests — runBootSequence picks a boot flavor via _pickBootFlavor: a RARE degraded "cold tube warming up" variant (DEGRADED_BOOT_CHANCE Math.random() roll + degraded POST lines + a #bootScreen.boot-degraded flicker class), the first-ever cold POST (RETROS BIOS + counting memory test gated once by localStorage robco_booted_before), and the unchanged normal warm boot; CRITICAL — the degraded roll is evaluated BEFORE the first-boot gate so it can surface on ANY boot, not just first launch (owner pref); the flicker is a CSS @keyframes animation neutralised by the global prefers-reduced-motion block (CR-1); a window.__robcoBootFlavor override forces a flavor for verification; runBootSequence still guards a missing #bootScreen + always calls onComplete; normal cadence (120ms) + boot-fade-out + the WU-B10 _bootActive window are preserved; and the boot strings are game-agnostic, versioned via APP_VERSION — Protocol 38). WU-F9 TERMLINK Command Console (Suite 123: tests — the native TERMLINK launcher surface: [TERMLINK]/[TL]/bare TERMLINK route to showTermlinkConsole via NATIVE_COMMAND_ROUTER; the TERMLINK_CONSOLE manifest’s six subsystem entries each resolve to a router token, with [TRADE] the documented BARTER-panel exception (panel:true, not a router key); _termlinkLaunch routes native tokens through _routeNativeCommand and opens BARTER via expandPanelForCategory(’trade’); offline / zero-AI; escapeHtml on rendered token/label/blurb; game-agnostic — Protocol 38; #termlinkBtn affordance + COMMAND_REGISTRY discoverability + overflow-safe console CSS). WU-T1 per-game theming (Suite 124: tests — data-driven optics: ONE read-only THEMES table (state.js, window.THEMES) replaces the if/else palette in changeOpticsColor (ui-audio) + the duplicated fgMap (ui-saves) (Protocol 22); THEMES.green = canon NV green #14fdce, THEMES.green3 = FO3’s distinct duller Pip-Boy green #4fb05a; GAME_DEFS.{FNV,FO3}.theme.defaultOptics (green/green3) carrying the full {defaultOptics,framing,pipBoyModel,bootFlavor,saveLabel} shape (WU-T3 seam); _resolveDefaultOptics reads _activeDef().theme.defaultOptics → green fallback (Protocol 38, no game literal); ui-core boot applies the explicit pick else applyDefaultOptics; a real WCAG relative-luminance computation enforces AA ≥4.5:1 for every contrastSafe:true theme vs the #010a07 background; green3 is selectable + the pre-paint head script applies it flash-free). WU-F10 session statistics merged into OVERSEER’S LOG (Suite 125: tests — the standalone SESSION STATISTICS panel is retired and its campaign readout (kills/caps earned/damage dealt/CAMPAIGN TIME/LOCATION VISITS via renderSessionStats) is merged into the OVERSEER’S LOG panel beside the WU-F7 device telemetry (CURRENT UPTIME/TOTAL POWER-ON/BOOT COUNT via renderOverseerLog) under labelled UNIT TELEMETRY / CAMPAIGN LOG dividers so campaign play-time and device uptime are clearly distinguished; #sessionStatsList moved into #overseerLogPanel exactly once (no duplicate); RESET CAMPAIGN STATS → resetSessionStats clears state.stats + re-renders; game-agnostic, Protocol 22 reuse). WU-F11 native mark-visited map control (Suite 126: tests — a per-row LOG VISIT <button> on the WORLD GRID flags a location discovered directly via markLocationVisited → the single-source recordLocationVisit() helper → saveState → renderWorldMap (flips [UNKNOWN]→[VISITED]); add-only — no un-mark/forget helper (permanent fog-of-war); NO AI (no fetch/transmit); apostrophe-safe escaped data-loc; literal aria-label (UI-3); a real <button> not a span (UI-5) with a ≥28px tap target + width:auto; suppressed on the current/already-visited rows; game-agnostic Protocol 38). WU-T3 per-game identity strings + native save header (Suite 127: tests — GAME_DEFS[ctx].theme.bootFlavor/pipBoyModel surface in runBootSequence as a per-game identity boot line, injected AFTER _bootLinesFor so it is flavor-independent — the cold/degraded/normal POST content + the WU-B10 _bootActive window + onComplete are untouched, no WU-F6 regression — and theme.saveLabel surfaces as the renderSavesList .saves-archive-header (escaped); FNV vs FO3 strings are distinct (LUCKY 38 TELEMETRY — MOJAVE vs VAULT-TEC ARCHIVE — CAPITAL WASTELAND); both paths fail-safe to a generic label; the consuming code is game-agnostic — Protocol 38 — strings sourced only from the GAME_DEFS theme block). WU-REN runner-rename escape-ratchet (Suite 128: tests — the diagnostics runners were renamed to tests/robco-diagnostics.{js,ps1} (git mv, history preserved); guards that both new runners exist, the legacy-named runner files are gone, ZERO stale legacy-runner-name references remain anywhere in code/scripts/CI/docs (Protocol 36b escape-ratchet — the forbidden literal is assembled at runtime so the guard never self-matches), and that gate.js / package.json / nightly-tests.yml invoke the renamed runners with the dual-runner gate intact). first-load desktop-layout pointer/hover gate (Suite 129: tests — the desktop app-shell @media is gated on (min-width:1000px) and (hover:hover) and (pointer:fine) so a touch phone can never boot into the PC fixed-shell layout even if a first-paint layout-viewport race momentarily reports ≥1000px; no ungated @media (min-width:1000px) remains; the ui-core panel default-open uses the same matchMedia gate instead of a raw window.innerWidth read; and the gated block still defines the two-column 380px 1fr shell + body overflow:hidden so real desktops are preserved — Protocol 42 first-load-desktop fix). per-game optics + dynamic (Default) label (Suite 130: tests — item 4: the OPTICS picker dynamically tags the ACTIVE game’s default optic “(Default)” via _updateOpticsDefaultLabel, matching GAME_DEFS[ctx].theme.defaultOptics and stripping the tag from the rest; item 5: the chosen optic persists PER GAME via _opticStorageKey() = robco_optic_<ctx> — changeOpticsColor writes it, _resolveOptic = per-game pick → one-time legacy-global migration → _resolveDefaultOptics (default → green), and ui-core boot + the index.html pre-paint head script + the ui-saves export all read the per-game key; game-agnostic / N-game scalable — keyed by gameContext, no two-game hardcode, a new game needs zero theming-code change). Step 2 (v2.8.0) Phase 0 U1 — `getSystemDirective()` decomposition + GA-5 retirement (Suite 131: tests — the monolithic directive assembly split into 8 module-scope per-section builder functions in `api.js` (`_directivePersonaAndContract`/`_directiveCoreTracking`/`_directiveSkills`/`_directiveFactions`/`_directiveSystems`/`_directiveTrackers`/`_directiveInjectionBoundary`, plus the internal `_directiveConstraints` helper), composed via a single array-join in original order; the last Protocol-38 residual (the 3 `ctx === 'FO3'/'FNV' ?` tracker ternaries) retired in favor of data-driven `GAME_DEFS[ctx].ai.trackerDirectives` (`state.js`, FNV = Traits+Magazines, FO3 = Lincoln, empty/absent = no-op for a future game); getSystemDirective() keeps its single call site and ctx fail-safe; a Protocol 14 golden-master VM test evaluates the real builders + real GAME_DEFS across an 11-point playstyle/playthroughType/campaignMode/game matrix and asserts SHA-256 equality against the pre-refactor output, proving byte-identical AI-facing behavior). Step 2 (v2.8.0) Phase 0 U2 — window.onload boot decomposition (Suite 132: tests — the ~568-line monolithic boot block split into 12 named, order-preserving phase functions (_hydrateStateFromStorage/_restoreApiKeyAndChatHistory/_wireRotaryDialClick/_wireStandby/_wirePanelPersistence/_restoreOpticsPreference/_restoreDevicePrefs/_wireKeyboardShortcuts/_runBootSequenceAndBriefing/_startAmbientTimers/_wireInputHistoryNav/_wireUnloadFlush) called from a slim window.onload in the exact original order — zero logic added/removed/reordered, verified live via the full Playwright gate (boot-smoke + 360/412 render-check, zero console errors, no black screen); the standby-mode shared state (_standbyActive/_uptimeInterval/_memCycleInterval/sessionStart) and its four functions (_startUptimeClock/_startMemCycle/enterStandby/exitStandby) moved to true module scope so _wireStandby() and the later _startAmbientTimers() share one set of guards, guarded by static checks that the shared state/functions are declared before window.onload and each boot-phase function exists exactly once). Step 2 (v2.8.0) Phase 0 U3 — `autoImportState()` VM-sandbox behavioral test (Suite 133: tests — a Node `vm` sandbox loads the real `js/state.js` + `js/reg_nv.js` + the real `autoImportState()` body extracted from `js/api.js` into one shared context and actually EXECUTES it — not a static grep — against malformed JSON, wrong-top-level-type, hostile/injection-shaped (`__proto__`/`constructor.prototype` pollution attempts, script-tag and SQL-injection-shaped strings), oversized (200-cap campaign notes, 2000-item inventory, 100k-char strings), wrong-typed (arrays/strings where an object is expected), and valid/partial payloads, asserting no throw escapes the caller, no prototype pollution, registry allow-lists hold, and — the Protocol 42 fix landed in this same commit — nine numeric fields in `autoImportState()` [`lvl`/`xp`/`hpCur`/`hpMax`/SPECIAL `s-p-e-c-i-a-l`/`caps`/`karma`/`rads`/`ticks`] that previously did a bare `parseInt(v)` with no `|| 0` fallback (unlike every sibling numeric coercion in the file, `sanitizeImportedContainer()`, and `syncStateFromDom()`) could be driven to `NaN` by a hostile or malformed AI response, silently corrupting player state; discovered by actually running the function under this harness, then fixed to match the established `|| 0` idiom). Step 2 (v2.8.0) Phase 0 U4 — per-suite parity gate (`scripts/gate.js`): the Protocol 15 runner-parity check upgraded from comparing only the grand total to also diffing per-suite composition — same suite numbers, same titles (dash/arrow-style normalized between the Node and PowerShell conventions), same per-suite counts — for every suite the Node runner explicitly numbers (currently 49–134, i.e. all Step 2/Phase 0 work and everything added from here forward), plus an aggregate cross-check for the pre-Suite-49 legacy zone (suites the Node runner's `header()` calls never numbered); this is what would have caught the historical 173-vs-209 gap, which a matching grand total alone could hide. **When you change one runner, update the other in the same commit** — drift here is what let the PS runner silently fall to 173. Step 2 (v2.8.0) Phase 0 U5 — MetaStore device-preference accessor (`js/state.js`): a single `MetaStore.get/set/remove/has/keys` choke point + a registered-key `META_MANIFEST` (31 device-preference keys — audio mutes, optics, power/haptics/high-lumen, UI layout, telemetry, the Gemini key/model/sync toggle, and the feature-flags LKG cache) that every device-key read/write across `js/ui-audio.js`/`js/ui-render.js`/`js/ui-core.js`/`js/api.js`/`js/cloud.js` now routes through, replacing bare `localStorage` calls; campaign/save keys (`robco_v8`/`robco_v7`/`robco_chat`/`robco_playstyle`/`robco_playstyle_type`/save slots/rolling backups/cloud-push bookkeeping) are deliberately NOT registered and keep their existing direct-storage paths (Protocol 23); the two index.html pre-paint `<head>` scripts (flash-free optics + high-lumen) keep bare `localStorage` calls too, since they run before `state.js` (and therefore MetaStore) has loaded — the one sanctioned exception, proven by Suite 134.7 to sit strictly before the first `js/*.js` `<script>` tag. Step 2 (v2.8.0) Phase 0 U6 — MetaStore boundary gate (Suite 134: tests — makes the two-store boundary structural rather than conventional: `META_MANIFEST` is proven campaign-key-free by actually executing `state.js` in a Node `vm` sandbox (not a static grep), `MetaStore.has()` is behaviorally verified true for a registered key + the per-game optic family and false for every campaign key, `MetaStore.get/set/remove` round-trip correctly and never throw even when the underlying `localStorage` mock throws (quota/private-mode fail-soft), and static guards confirm no served code ever passes a campaign key to MetaStore and no served code bypasses MetaStore to touch a registered device key via raw `localStorage`). Step 2 (v2.8.0) Phase 0 U7/U8 — OS event bus + faction-agnostic fix + auto-log expansion (Suite 135: tests — a synchronous `RobcoEvents.on`/`emit` pub-sub (`js/state.js`) that the 3 previously-inline state-crossing detectors (level-up and faction-threshold in `autoImportState()`, HP-critical in `updateMath()`) now emit through instead of acting inline, with the pre-existing sound/haptic/chat reactions re-homed as subscribers (`ui-audio.js` for level-up, `api.js` for the faction-threshold chat+sound+haptic, `ui-core.js` for the HP-critical flash+haptic); the U7 Protocol-38 fix retires the FNV-hardcoded `majorFactionKeys` array (`['ncr','legion','house','bos','boomers','khans']`, which meant FO3 campaigns never fired a threshold alert) in favor of `getFactionRegistry()`, proven behaviorally by a VM-sandbox test that crosses an FO3-only faction ('talon') into Vilified and asserts the event fires; U8 expands auto-logging (previously factions/quests/locations only) to level-ups, collectible acquisitions (`toggleCollectible`), crafts/scraps (`doCraft`/`doScrap`), trades (`doBuy`/`doSell`), and sleeps (`_nativeSleep`), each a new `RobcoEvents.emit()` call site consumed by dedicated `state.js` auto-log subscribers that write through the single new `_logCampaignEvent()` append+cap helper — which also replaces the two duplicated inline `campaign_notes.push`/`slice(-200)` sites (faction fame/infamy delta log, quest-status-change log); bus behavioral guarantees — emit→subscribe fires exactly once, N emits fire N times not more, multiple independent subscribers each fire once per emit, a throwing listener is swallowed and never blocks siblings or escapes `emit()`, emitting with zero subscribers never throws — are all VM-executed, not just statically asserted; and — a Protocol 42 fix landed in the same commit — each cross-file `RobcoEvents.on()` registration (level-up/hp-critical in `ui-audio.js`/`ui-core.js`, faction-threshold in `api.js`) is wrapped in a named `_wire*EventBusSubscribers()` function called from `window.onload`, because those 3 files are static `<script>` tags that can execute before `state.js` (dynamically, context-conditionally loaded) defines `RobcoEvents` — a bare top-level call threw `"RobcoEvents is not defined"` on some boots, caught by the Playwright boot-smoke check and locked by a structural guard that no `RobcoEvents.on()` call sits outside its wiring function). Step 2 (v2.8.0) Phase 0 U9/U10 — cheap connector sweep + native-input-path audit (Suite 136: tests — the dead Projected Timeline stub fully retired (the `index.html` shell, the `COMMAND_REGISTRY` entry, and every AI-directive reference removed; `[TIMELINE]` added to the Suite 113 RETIRED-macro list); the campaign readout's raw-minutes CAMPAIGN TIME line replaced with a human-formatted CURRENT SITTING duration (`_fmtOverseerDuration`, still derived from `stats.sessionStart`); THREAT gains an ammo-reserve advisory comparing the projected `ammoBurn` against actual `state.ammo` reserves via the reused craft-panel lookup `_craftGetHave()` (Protocol 22); CONSULT now searches the tracker registries too (collectibles/skillBooks/magazines/traits/lincolnMemorabilia — game-agnostic, since `registrySearch()` already no-ops on a category absent from the active game) and surfaces two previously-parsed-but-never-shown DB columns via a new `getQuestItemDetail()` accessor in both db runners (QUEST_ITEMS.CSV `Associated_Quest`/`Special_Property`, mirroring the `getChemsTable()` pattern) plus BESTIARY's already-parsed `xpYield`; a new `GAME_DEFS.hasWeaponMods` flag (FNV true, FO3 false — FO3 has no weapon-mod system or data at all) hides the inventory Mods filter button per-game via `_updateContextPanels()`, with a fail-safe reset off the mod filter so FO3 can't be stranded on it; and the one live player-authority violation the U10 native-input-path audit found — squad/companion affinity was AI-write-only via `autoImportState()`, with no way for the player to ever set or correct it — is closed with native `adjustAffinity()` `[+]`/`[-]` buttons always rendered on every squad row (an unset member now defaults to 0% instead of hiding the bar entirely) and `addSquadMember()` seeding `affinity: 0` for new companions; the audit's one remaining gap — `state.equipped` has no native setter at all — is documented in ARCHITECTURE.md's new Native-Input-Path Audit table rather than silently folded into this unit's scope). Step 2 (v2.8.0) Phase 0 U11/U12 — hygiene ledgers + modal consolidation (Suite 137: tests — U11: a per-game data parity ledger (every asymmetry labeled GENUINE or GAP, measured directly against live source at commit time), a reserved-column register (every authored-but-unconsumed DB column tagged with an intended future consumer or PARKED[-FOR-REMOVAL]) committed to both `db_nv.js`/`db_fo3.js` headers and `ARCHITECTURE.md`, and a skill-less (FO4-class) degradation audit proving every `getSkillKeys()` consumer already degrades to `[]` safely and locking the one hard requirement for a future `GAME_DEFS.FO4` entry (`skillKeys: []`, never omitted); U12: the ~50 blocking `alert()`/`confirm()` sites across `js/*.js` purged in favor of one consolidated `openModal()` driver (D-2/FP-SYS-8, replacing the ad hoc `_openSysModal()` + manual title/content pattern at every caller) and a diegetic, Promise-based `confirmAction()` helper (FP-SYS-10) that every destructive confirm gate now `await`s instead of blocking on `confirm()` — `doCraft`/`doScrap` (ui-render.js) and `restoreRollingBackup` (ui-saves.js) were split into an async confirm-gate wrapper plus a synchronous `*Apply`/`*Prepare` mutation core so the gate and the mutation stay independently testable; a real behavioral proof (not just a structural grep) drives `confirmAction()` against a synthetic DOM in a Node `vm` sandbox and asserts CONFIRM resolves true while CANCEL/CLOSE/Escape all resolve false via one shared `onClose` path; the index.html firmware-update dialog (`_triggerUpdate`) stays a deliberate, separately-tested exception (Suite 65) — a non-dismissable blocking modal never folded into the dismissable driver; and a Protocol 42 fix landed in the same commit — a pre-existing PowerShell-runner flake where piping a large heredoc to `node` via `| node` could intermittently corrupt/truncate stdin deep into a long session was traced to the pipe transport itself (proven harness-only: the identical script always passed when run in isolation) and fixed by writing to a temp file and invoking `node <file>` instead). Step 2 (v2.8.0) Phase 2 A2/Developer-Console unit — Developer Console: the ONE canonical dev/debug console (Suite 149: tests — this panel IS the developer/debug console the roadmap's hacking minigame will later unlock in normal builds, not a separate throwaway test panel; a live inspector + trigger panel for the Ambient Runtime; visibility is centralized in ONE gate, `_devConsoleUnlocked()`, which TODAY delegates verbatim to the identical staging-env signal the changelog viewer (Suite 62/WU-C11) uses to hide `[Unreleased]` on production (`_isStagingEnv()`, never re-implemented) and fails safe to HIDDEN on any uncertainty — a documented MINIGAME-UNLOCK SEAM comment marks it as the exact hook a future hacking minigame will also flip to true on a production build, so no second gate is ever re-derived (Protocol 22); the panel markup lives inert inside a `<template>` (the WU-E2 disabled-banner pattern) so it can never render by accident; surfaces the live runtime state, force-transition buttons for every canonical state, an Immersion-tier selector that reuses the real dial's own `onImmersionChange()`/`getImmersionTier()` setters, and a read-out of every registered runtime observer via the new read-only `AmbientRuntime.listObservers()` introspection API; writes NOTHING durable to the campaign — runtime state + the existing Immersion device pref only; both-sides behavioral proof (hidden when the staging signal is missing/throws/false, shown only when genuinely true) mirrors the Suite 62.5 eval-based technique). Step 2 (v2.8.0) Phase 2 A3 — IDLE/STANDBY/SHUTDOWN ambient experiences (Suite 150: tests — the showcase consumers of the runtime states, layered on top of the A2 standby machine + timers (unchanged): `_wireAmbientExperiences()` (`ui-core.js`) registers three dial-gated observers — idle-phosphor (states `['IDLE']`, tier `'balanced'`, a gentle phosphor-preservation dim + diegetic corner note that reverts instantly on any interaction), standby-deepen (states `['STANDBY']`, tier `'balanced'`, an additional breathing-vignette flourish layered OVER the A2 essential `'minimal'`-tier dim, which never quiets), and shutdown-crt (states `['SHUTDOWN','OFF']`, tier `'full'`, a CRT collapse-to-a-dot power-down that degrades to a plain instant cut at Balanced/Minimal); onEnter/onExit are NOT tier-gated by the runtime itself (only `onTick` is), so each observer's onEnter checks `immersionAllows()` itself while onExit always cleans up unconditionally; shutdown-crt's states include `OFF` so its one-shot animation trigger survives the internal SHUTDOWN→OFF cascade (`shutdown()` fires both transitions synchronously) without re-firing, and force-clears any lingering idle/standby class first so a genuine shutdown always wins; every new `@keyframes` animation is neutralised to its static/instant final frame by the existing global `prefers-reduced-motion` block (Protocol 17); a companion fix in `exitStandby()` (found while verifying this unit, then hardened further after an owner review, Protocol 42) closes the wake-sequence race properly: the shared `_isShuttingDown()` helper is re-checked at EACH half of the wake sequence's own fire time — synchronously before the wake tone (fire time is now), and again as the first statement inside the delayed 650ms `setTimeout` before the audio ramp/"COURIER RETURNED" chat line/`updateMath()` (that half's fire time is later) — so a shutdown landing either as a direct STANDBY→SHUTDOWN edge OR partway through the wake window after the tone already played both correctly suppress the rest of the sequence, never just the direct-edge case a single up-front check would catch; a Node `vm`-sandbox behavioral proof actually executes the real `enterStandby`/`exitStandby`/`_isShuttingDown` bodies against a synthetic DOM with a synchronous-mock `setTimeout` (avoiding both a real 650ms wait and the Suite 137.6 deferred-async trap) across all three scenarios — direct shutdown (no tone, no chat), normal wake (tone + chat + `updateMath`, byte-identical to before), and the mid-window race (tone already fired, chat/`updateMath` suppressed) — with a PowerShell structural mirror (Suite 149.8 convention, no JS eval available) locking the same two-check-sites shape; writes NOTHING durable to the campaign anywhere — body classList toggles only, game-agnostic). Step 2 (v2.8.0) Phase 2 B1 — Command-Line MODE system (Suite 151: tests — the inline `#modePill` toggling `robco_input_mode` (MetaStore device pref, `getInputMode`/`setInputMode`/`otherInputMode` in `state.js`, default `'overseer'`) between OVERSEER (`transmitMessage(overrideText)`, an additive optional param, no-arg behavior byte-identical) and TERMINAL (`transmitTerminal`, native commands via the unchanged `_routeNativeCommand` then `QUICK_LOG_PATTERNS` quick-log routing — `killed <target>`/`+N caps`/`arrived <location>`/`rep <faction> up/down` — onto the existing `_logEvent`/`markLocationVisited`/`adjustFaction` setters, never forked, faction key validated via `getFactionRegistry()`, unrecognized falls through to a `[TERM] UNRECOGNIZED` hint, never calls the AI); `submitCommandInput()` is the one choke point both `[TRANSMIT PROTOCOL]` and Ctrl+Enter call, resolving a one-off `/`/`@` override — first-character-only, `/msg` and `/ msg` both supported, proven behaviorally in a Node `vm` sandbox — and always routing an attached image to `transmitMessage()` regardless of mode; `wireInput()` (`ui-saves.js`) generalized to accept a resolver function alongside the existing registry-category strings so `#chatInput` gets TERMINAL-only command/quick-log suggestions via `_commandSuggestions()`; documented in a new `COMMAND_REGISTRY` group so `[FEATURES]` stays current; game-agnostic, no durable campaign write beyond the reused native setters' own). Step 2 (v2.8.0) Phase 2 B1 follow-up fixes (owner audit, Suite 151 updated + Suite 152: tests — FIX 1: `_resolveCommandInput()`'s `/`/`@` one-off overrides changed from relative ("send to whichever mode I'm not in", which made the two prefixes redundant) to FIXED targets — `/` always resolves to `'terminal'` and `@` always resolves to `'overseer'`, from either starting persisted mode, keyed directly off `first === '/' ? 'terminal' : 'overseer'` rather than `otherInputMode(persisted)`; the `/`/`@` hint reveal (`_updateModeHint()`, `ui-core.js`) now calls `_resolveCommandInput()` itself as the single source of the displayed target instead of re-deriving it, so the hint can never drift from the real routing; Suite 151.8/151.17 updated to prove the fixed targeting (a Node `vm` behavioral test drives both prefixes from BOTH starting persisted modes). FIX 2: a diegetic `#powerOnBtn` (`▶ PRESS TO POWER ON`) fixes an owner-reported bug where forcing SHUTDOWN/OFF (the A3 `shutdown-crt` observer) left a fully black screen with no visible way back on — the button is shown ONLY during SHUTDOWN/OFF via the SAME `body.rt-shutdown`/`body.rt-shutdown-plain` classes the observer already toggles (Protocol 22, pure CSS, no separate JS visibility bookkeeping to drift), sits at `z-index:100002` above the shutdown cover's `100001`, and its click handler `_powerOnFromShutdown()` (`ui-core.js`, beside the `shutdown-crt` observer) recovers using ONLY legal `AmbientRuntime.transition()` edges — walking SHUTDOWN → OFF → COLD_BOOT (never `forceState()`, the documented TEST-ONLY escape hatch Suite 146.15 guards stays production-untouched) — after which the runtime's own heartbeat auto-advances COLD_BOOT → READY → ACTIVE exactly as on a real page load. Cache r34. Step 2 (v2.8.0) Phase 2 B1 upgrades — inline `@` ping, comma multi-action, content autocomplete (Suite 153: tests — `_resolveCommandInput()`'s precedence changed to: a leading `/` (first-char-only, whole line) → `'terminal'`, checked and returned FIRST so it always wins over any later `@`; otherwise a `@` ANYWHERE in the line (`raw.indexOf('@')`) → `'overseer'`, with everything after the FIRST `@` sent and everything before it dropped (superseding the prior first-character-only `@`); the hint reveal (`_updateModeHint()`) now derives entirely from `resolved.override`, so it can never disagree with the resolver. `transmitTerminal()` gained comma-separated multi-action routing: `_routeQuickLogMulti()` splits on commas, trims, drops empty segments, and routes each through the unchanged single-segment `_routeQuickLog()`, collating ONE combined hint (never one per segment) when some but not all segments match — native `[TOKEN]` commands still run on the whole unsplit line first, so a token's own arguments are never comma-split. `_commandSuggestions()` gained content-aware autocomplete: `_quickLogContentSuggestions()` recognizes a recognized quick-log verb's lead-in (`killed `/`arrived `/`at `/`rep `) and suggests registry/DB CONTENT for the next token instead of re-suggesting verbs — creature names via a new `getBestiaryNames()` (added to both `db_nv.js`/`db_fo3.js`, refactored to share the same lazy `_bestiaryCache` `lookupBestiaryEntry()` already builds, Protocol 22, no second CSV parse, behavior-preserving per Suite 153.5), location names via `FALLOUT_REGISTRY.locations`, and faction keys via `getFactionRegistry()` (then `up`/`down` once a valid key is typed) — every suggestion (content or verb/token) is prefixed with whatever the resolver stripped, so picking one never silently drops a `/` override; game-agnostic, no hardcoded name list. Cache r35. Step 2 (v2.8.0) Phase 2 B2a — Module Bay core reframe (Suite 154: tests — the SECURITY & CONFIGURATION panel contents reframed as installable hardware boards, an owner-approved redesign (the new Protocol 25 sanctioned-exception clause): the hatch (a NEW device pref `robco_bay_opened` gates a first-visit-only ceremony, honors prefers-reduced-motion), all 5 SLOT boards + the SVC tray as collapsible `data-sub-id` sub-panels defaulting open on first boot, the phosphor tube rack (one button per THEMES key, wired through the unchanged `changeOpticsColor()`/`_updateOpticsDefaultLabel()` — reworked for buttons instead of a `<select>`), the permanent Schematic View fallback (regenerated from the same MetaStore prefs on open, never a parallel wired control set), and `renderModuleBay()` as the one re-sync point (combined SLOT 01/02 status lines + a Protocol-42 fix closing a real bay↔schematic checkbox-sync gap a live-browser check found); the 13 SLOT-02 channel-mute checkboxes are explicitly UN-FLIPPED this unit (B2b re-skins them); two more Protocol 42 fixes landed in the same commit after being caught by manual/automated render-check verification — a bare `1fr` grid track's implicit content-based minimum forcing every board wider than its track at 360px (fixed via `minmax(0, 1fr)`), and a two-column `@container` breakpoint that could never engage because the app's desktop shell fixes the settings panel's own column at a hard 380px (confirmed via live desktop measurement, removed as dead code) — plus a nowrap+ellipsis decorative strip that clipped without ever visibly overflowing. Cache r36. Step 2 (v2.8.0) Phase 2 B2c — Module Bay refinements + hardware SFX (Suite 156: tests — two owner bug/request fixes plus the Module Bay's SFX unit: FIX 1 closes an owner-reported display bug where ejecting the Sonic Processor board (master mute) only showed the individually-muted channel chips as pulled while the rest still read seated even though ALL audio was offline — a `.sonic-board--ejected` CSS class toggled on `#chipGrid` purely from `AudioSettings.masterMute` (via `_updateSonicBoardStatus()`) now makes every chip read pulled together, display-only and never touching the 13 individual `robco_*_muted` prefs, which is what lets reseating the board restore each chip to its own preserved state; FIX 2 (best-effort, owner request) layers pointer/touch drag-to-rotate onto the existing tap-to-cycle Immersion dial — a horizontal drag steps through the same `IMMERSION_ORDER` via the same `onImmersionChange()` setter (Protocol 22/25, never a forked control), a `_dialDragSuppressClick` flag consumed by `_cycleImmersionDial()` stops the trailing synthetic click from double-applying after a drag, and `touch-action:none` plus a graceful no-PointerEvent fallback keep it Protocol 17-safe; the B2c SFX unit adds a SERVO CLICK RELAY channel (`robco_hardwaresfx_muted` / `AudioSettings.hardwareSfx`, registered in `META_MANIFEST`, presented with the same INVERTED checked=installed=audible semantics as the master-mute board, Protocol 25) gating two new synthesized WebAudio functions — `playChipClick()` for the 13 channel chips plus the radio/power-cell/haptic bay-module toggles, and a heavier `playBoardThunk()` for the Sonic Processor board's own eject/reseat, both guarding `AudioSettings.masterMute` first then `AudioSettings.hardwareSfx` second per Protocol 7 — with `toggleMasterMute()` sequencing the board-thunk calls around the exact instant the masterMute flag flips so the click is heard leaving on eject and arriving on reseat without ever bypassing the guard, and `_restoreDevicePrefs()`/`renderModuleBay()`/`renderBaySchematic()` all extended with the same inverted-checkbox three-way sync every other bay control follows so the bay checkbox, the schematic mirror, and a fresh boot can never drift). Cache r38. Step 2 (v2.8.0) Design Overhaul DO-K — identity keystone (Suite 157: tests — widens each `GAME_DEFS[ctx].theme` into a full `identity` block (`machine`/`material`/`structuralMode`/`theme`/`persona`/`ceremony`/`motionTexture`/`cursor`/`audio`/`voice`/`ambient`) that every later overhaul unit (bezel nav, Overseer, ceremony, motion, audio) will read from, proven by a Node `vm`-sandbox behavioral test that loads the real `js/state.js` and asserts all three games (FNV/FO3/FO4) carry a complete contract; `identity.theme` is aliased to the SAME `theme` object each game already declares (a post-construction loop, never a duplicated/forked literal — Protocol 22), verified by reference-equality; NV is populated richly and accurately from the owner-approved mockup (`planning/2.8.0/mockups/nv-machine-mockup.html`/`nv-machine-rationale.md` — salvaged-terminal casing, amber-block cursor, Mojave-uplink boot, oscilloscope-Overseer persona), FO3 gets a sensible stub (its real Vault-Tec-molded facets land at DO-M), and a brand-new `GAME_DEFS.FO4` entry is authored design-only (`designOnly: true`) to prove the N-game abstraction — it validates fully (including the Mark-IV `identity` spec) but stays unreachable: `onGameContextChange()` refuses a `designOnly` ctx, `wipeTerminal()`'s context-list chat prompt filters designOnly entries out, and `#gameContextSelect` offers no FO4 `<option>`; per the Step 2 Phase 0 U11 skill-less (FO4-class) audit already on record, FO4's `skillKeys`/`factions`/`combatSkills` are declared as empty arrays (never omitted, so `getSkillKeys()`/`getFactionRegistry()` degrade safely instead of throwing); a new read-only `getIdentity(ctx)` accessor mirrors `getFactionRegistry()`/`getSkillKeys()` and fails safe to FNV's identity for an unknown ctx; a `data-game` root attribute is set at three sites — the `index.html` pre-paint head script (flash-free, alongside the existing optics/high-lumen reads), `_restoreOpticsPreference()` once state is live, and `onGameContextChange()` before the reload; and the unit is byte-identical/zero-behavior-change — nothing reads `identity` yet (DO-N/DO-O/DO-C/DO-M/DO-Q2-6 are the future consumers), no `state.<field>`/`saveState`/`robco_v8` write appears anywhere in the addition, and `APP_VERSION` stays 2.7.0 under `[Unreleased]`). Cache r39. Step 2 (v2.8.0) Design Overhaul DO-N — bezel chrome + subsystem nav (Suite 158: tests — the tab bar (`.tab-bar`/`.tab-btn`) is fully retired and replaced by an illuminated bezel subsystem selector (`.nav-cluster[role=tablist]`, six `.navkey` keycaps OPERATOR/OPERATIONS/DATABANK/UPLINK/CHASSIS + a sibling DIR key outside the tablist for a11y, per the owner-approved `nv-machine-mockup.html`) sitting inside a new `.casing-top`/`.glass-frame`/`.bezel` device-chrome shell (Protocol UI-7); `selectSubsystem(view)` is the ONE new click/hotkey entry point and it routes every tab-mapped subsystem through the UNCHANGED `switchTab()` — OPERATOR/OPERATIONS/DATABANK map 1:1 to stat/inv/data, and `switchTab()` itself gained exactly one behavior-preserving widening: selecting `data` or `campg` now shows BOTH panel groups together (`_DATABANK_TABS`) so DATABANK houses the old DATA+CAMPG split, while UPLINK/CHASSIS (never gated by a tab) scroll-and-focus the always-visible Comm-Link column / Module Bay directly, mirroring the pre-existing `SHORTCUT_ROUTES.comm` pattern; hotkeys `[1]`-`[5]` + `[0]` (DIRECTORY), `role=tab`/`aria-selected` ARIA, `#go=` deep-link routing, and `robco_active_tab` reload-restore are all verified byte-for-byte preserved, with a new `robco_bezel_subsystem` MetaStore pref (Protocol UI-6) additionally restoring the UPLINK/CHASSIS highlight on boot; the flat DIRECTORY fallback (`openBezelDirectory()`) reuses the existing `openModal()`/`#sysModal` driver rather than a bespoke dialog (Protocol 22); the casing-top FAULT lamp reads the pre-existing client error ring-buffer read-only, and the UPLINK lamp / DATABANK-keycap accent read a new `--bezel-wire` CSS custom property that defaults to the local phosphor color and is only overridden amber under `[data-game='FNV']` (Protocol 38 — no per-game JS branch); the custom amber-block cursor is likewise scoped to `[data-game='FNV'] ` under a fine-pointer media query; the SWEEP re-tune transition (Protocol UI-9 motion-verb grammar) is a plain CSS `@keyframes` animation so the existing global `prefers-reduced-motion` block neutralises it with no bespoke carve-out; and the unit writes nothing durable to the campaign anywhere — view/device prefs only, verified live at 360/412/desktop with zero horizontal overflow and the full Playwright gate green). Cache r40. Step 2 (v2.8.0) owner bug-fix batch — five staging bugs fixed in one commit (Suite 159 + extensions to Suites 114/149/151/154): (1) live-render sweep — _logEvent() (the ONE writer for state.eventLog) now re-renders CROSSROADS RECORD + INCIDENT LOG after every append, a single choke point covering every current and future eventLog writer, and the quick-log "arrived <location>" TERMINAL verb now sets the location as CURRENT (via the shared onLocationChange(overrideLoc) setter) instead of only flagging it discovered, so the WORLD MAP moves [CURRENT] live; (2) the Module Bay hatch ceremony now fires only on securityConfigPanel’s own first user-initiated toggle (never at window.onload), and that panel defaults CLOSED on every viewport (excluded from the generic desktop auto-open branch) instead of popping the hatch open at page load; (3) SLOT 05’s AI Uplink status reflects a REAL validated Gemini key + engine (robco_gemini_validated_key, set only inside fetchAuthorizedModels()’s live 200-response success path) via _updateUplinkBoardStatus(), replacing the hardcoded NO CARRIER string; (4) the centering rule extends to the 13-chip grid and 7-tube phosphor rack (converted from CSS Grid auto-fill, which can’t center a responsive incomplete last row, to the reusable flex+wrap+justify-content:center pattern .bay-tools already established) and to the SAVE/cloud-sync button row; (5) the Test Console’s dead COLD_BOOT/READY/ACTIVE force buttons (no A3 ambient effect of their own) are replaced by REBOOT (replays the real boot sequence) and WAKE → ACTIVE (the one-click undo for the IDLE/STANDBY/SHUTDOWN/OFF buttons). Cache r41. Step 2 (v2.8.0) owner audit — bezel bottom placement + stray-pin cleanup (Suite 160: tests — the nav-cluster's independent flex-wrap, which let CHASSIS wrap onto its own second line with DIR floating beside it, is fixed to a single non-wrapping strip that shrinks together; the bezel keycap nav, which rendered above the glass on mobile (a normal document-flow page, unlike desktop's fixed-height single-viewport shell, so a reorder-only fix would have buried the nav after the very long single-column panel stack instead of keeping it reachable), now docks as a genuine position:fixed thumb-zone bar at the true bottom of the mobile viewport, reserving bottom padding on the machine casing so the dock never covers the last panel's content; desktop is untouched, since it already rendered the bezel at the bottom via its existing casing-top/glass-frame/bezel order:1/2/3 flip; and the amber connector/vent-pin strips (Module Bay board dividers, SLOT-02 chip sockets) move from a repeating-linear-gradient — whose fixed cycle length rarely divides the strip's actual responsive width evenly, leaving a stray thin partial pin at the end — to background-repeat:round, which rescales the tile so a whole number of pins always fits; a known trade-off is flagged rather than silently swept — three ambient-runtime body states (idle dimming, night mode, the shutdown power-down flourish) apply filter/transform to the casing, which per spec makes it the containing block for its fixed descendants, so the fixed bezel gracefully degrades to the bottom of the page (not broken, just temporarily not pinned) while any of those three states is active; closing that gap fully is deferred as a separate, wider-reaching follow-up). Cache r42. Step 2 (v2.8.0) owner batch — saves OVERWRITE + live-update sweep + save-help + hatch replay (Suite 161: tests — four owner reports fixed together: an explicit, confirm-gated OVERWRITE control on the SAVES LIST for both a local slot (`confirmOverwriteSlot()`, ui-saves.js, delegating straight to the existing `saveToSlot()` write path — Protocol 22 — so the slot keeps its name and its P5 version ring still captures the prior contents) and a cloud save (`window.overwriteCloudSave()`, cloud.js, gated exactly like every other cloud write and writing via `updateDoc` only, never `setDoc` — Protocol 34), neither ever prompting for a rename; the confirmed root cause (Protocol 27) of "the version history doesn't live update, I have to click LOAD before it updates" — `saveToSlot()` never called `renderSavesList()` itself, only `loadUI()` (triggered by a LOAD) did — fixed at the one write path so the SAVES LIST and its VER badge now refresh immediately after every save, overwrite, or version restore; `SAVE_HELP` (ui-core.js) gains an OVERWRITE entry plus the previously-undocumented EXPORT FULL BACKUP entry; and a REPLAY HATCH button in the (still `_devConsoleUnlocked()`-gated, inert-by-`<template>`) Test Console resets the exact same `robco_bay_opened` MetaStore key `releaseBayHatch()` sets, so the Module Bay's view-once hatch ceremony can be re-triggered for testing without a page reload. Cache r43. [NOTE: this prose changelog narration is drifted/incomplete past this point relative to the shipped code — sw.js CACHE_NAME is actually at r124 as of this commit; reconciling the gap is out of scope for the Visual Upload OCR unit below and is flagged separately, not silently patched over.] Step 2 (v2.8.0) Visual Upload OCR Unit 1 — infra proof (Protocol 8 Stage 1 of planning/2.8.0/plans/VISUAL_UPLOAD_OCR_PLAN.md; Suite 205: tests, Suite 55 extended to tests): vendors self-hosted Tesseract.js (Apache-2.0) into `js/vendor/` + `assets/ocr/eng.traineddata.gz` (~9.5MB total), lazy-loaded on first use only via a new `js/ocr.js` (`_ensureTesseract()`, idempotent, never called at boot) — never a CDN, so `worker-src 'self'` holds and `createWorker()` is configured with `workerBlobURL: false`; adds `'wasm-unsafe-eval'` to the CSP `script-src` (required for `WebAssembly.instantiate` under CSP, does not touch the `unsafe-inline` tripwire); splits `sw.js` ASSETS so only the small `tesseract.min.js`/`worker.min.js` shims precache while the heavy core+lang (`tesseract-core-lstm.wasm(.js)`, `eng.traineddata.gz`) cache at runtime best-effort on first OCR use (offline works only after that first use); and wires a staging-only Dev Console board (`_wireOcrTest()`, gated behind the existing `_devConsoleUnlocked()`) that runs image → canvas preprocess (grayscale/threshold/invert) → OCR → dumps the RAW recognized text, proving the pipeline end-to-end. No parser, no state write, and the existing AI-vision Visual Upload path (`handleImageSelection()`/`transmitMessage()`'s `inlineData` branch) is fully untouched — Unit 2 (parser + preview/confirm apply) and Unit 3 (hybrid routing + kill-switch) land as separate units. `corePath`/`workerPath`/`langPath` are all resolved to absolute URLs via `new URL(path, location.href).href`, and `corePath` deliberately points at the exact vendored core filename rather than a bare directory, sidestepping Tesseract's SIMD-vs-non-SIMD auto-probe (which would 404 since only the non-SIMD lstm-only core is vendored, to hold the repo-weight addition to ~9.5MB instead of ~15MB+ for both variants). A Protocol 42 fix landed in the same commit, caught by live browser verification (a real synthesized-canvas image OCR'd end-to-end on a static prod-like local server, not just the static test gate): a relative `corePath` is resolved by the dedicated Worker against the WORKER SCRIPT's own location (not the page's), which silently doubled the path (`.../js/vendor/js/vendor/tesseract-core-lstm.wasm.js`, a 404) and was invisible to every static/CSP guard — fixed by resolving every path to an absolute URL before it reaches `createWorker()`, re-verified live (OCR correctly read back synthetic text, the heavy core+lang assets runtime-cached, and the same pipeline still worked correctly under full Service-Worker control). `repomix.config.json` ignores `js/vendor/**`/`assets/ocr/**` (Protocol 37); `.prettierignore`/`eslint.config.mjs` also exclude `js/vendor/**` so the vendored, pinned third-party bundle is never reformatted or linted (a first Prettier pass bloated the pinned wasm glue file before this guard was added, caught and reverted in the same commit). Cache r125. Step 2 (v2.8.0) Design Overhaul DO-O — the living Overseer / Director Uplink (Suite 162: tests (originally 15, extended
+7 by the mobile-density follow-up, +4 by the composer-redesign follow-up, and +4 more by the
composer-integration follow-up, all below) —
the Comm-Link reskinned as a phosphor-oscilloscope presence over the UNCHANGED appendToChat()/transmitMessage() pipeline (Protocol 22, never forked): an `#overseerScope` `<canvas>` whose waveform is driven by the REAL AI lifecycle — `setOverseerState('thinking')` at the exact `transmitMessage()` thermal-load window (after the native-router/flag/no-key gates, so a deterministic command never touches the scope), `'speaking'` at the `appendToChat()` AI-typewriter start and `'listening'` at its completion (guarded on `!isHistoryLoad`, so replayed history never touches it), and `'disabled'`/`'offline'` from a pure, vm-tested `_overseerRestState({hasKey,aiEnabled,online})` helper; the `transmitMessage()` `finally` block resets the scope ONLY when it is still `'thinking'` (a blind reset would truncate a `'speaking'` typewriter that starts asynchronously after `finally` runs); a new `identity.overseer` block (title/relay/signalStrip/per-state tags/greeting) extends the DO-K per-game identity keystone (Protocol 38) with NV populated from the approved mockup, FO3 a distinct stub, and FO4's design-only entry validating the same shape — Suite 157's `CONTRACT157` completeness list now requires `overseer` on all three games; trace colour stays the existing `--bezel-wire` `[data-game]` token (zero JS colour branch); a new `document.body.dataset.subsystem` write inside the existing `_syncBezelNav()` choke point drives a mobile-only self-contained UPLINK view (fixes the pre-DO-O `.col-right` "infinite scroll" — it had no `data-tab` and rendered at full length below every other subsystem) plus a non-`position:fixed` `.carrier-strip` shown on every other subsystem so the Overseer never fully leaves the screen; reduced-motion/Minimal-dial/`document.hidden`/runtime-STANDBY-SHUTDOWN-OFF all degrade the canvas to one static frame via `_scopeShouldAnimate()` (no bespoke carve-out); a dial-gated, non-persisted idle-life blip observer (`cadenceMs:35000`, `states:['ACTIVE','IDLE']`) renders occasional `identity.persona.blipBank` lines via `appendToChat(line,'sys',true)` (never pushed to `chatHistory`/`robco_v8`); Protocol UI-10 (Overseer Presence) is adopted at this unit; and the whole block writes NOTHING durable to the campaign — `_scopeState` is a transient module var, zero `saveState()`/`robco_v8`/`state.*` write anywhere). Cache r44. Step 2 (v2.8.0) DO-O follow-up — UPLINK mobile-density/de-bloat/restyle (Suite 162 extended, +tests, 19 total): fixes an owner-reported live-mobile complaint (the oscilloscope + SIGNAL strip ate huge vertical space, the transcript read as a small box, the command input was cut off/cramped, and an old blue/green boxy D-PAD + native-command cluster sat below it). The D-PAD/native-command cluster (`[THREAT]`/`[VATS]`/`[TRADE]`/`[LOOT]`/`[CONSULT]`/V.A.T.S. CALCULATOR/TERMLINK CONSOLE) is tucked into a collapsible `details.sub-panel` (`data-sub-id="uplinkCommandTray"`, Protocol UI-1/UI-2) that defaults COLLAPSED on mobile — freeing the space that was squeezing the transcript down to its `#chatDisplay` `flex:1` min-height floor — and defaults OPEN on a real desktop (the one documented per-id exception in `_wirePanelPersistence()`, matching its pre-existing always-visible, zero-added-taps behavior there); every control keeps its exact onclick wiring (Protocol 22 — only the container/styling changed). The oscilloscope shrinks from a fixed 120px to a 64px mobile banner and the command input (`#chatInput`) gains a real 76px height (was the bare 2-row textarea default) — both scoped to the existing UPLINK mobile block, no wiring touched. The whole cluster is restyled amber (`--bezel-wire`, Protocol 38 — no game literal) instead of the old `--robco-blue`/`--robco-green` boxy look (`.tactical-dashboard`, `.d-pad button`, and the TRADE/LOOT/CONSULT/VATS-CALC macro buttons); zero behavior regression, every button verified to still fire its exact pre-existing handler, no campaign-state write, PWA/desktop two-column layout unchanged. Cache r45. DO-O follow-up, part 2 — the modern rounded composer (owner-supplied Claude-style reference layout, superseding the initial "fix the cramped input" item): one bordered rounded `#composer` box (18px radius, amber `--bezel-wire` border) now holds the borderless `#chatInput` textarea plus a bottom `.composer-toolbar` row — `[+]` (calls the pre-existing `triggerImageUpload()`, replacing the old standalone dashed `[ > VISUAL UPLOAD ]` button), the unchanged `#modePill` (`toggleInputMode()`), a round `[?]` (`showHelpModal()`, replacing the old `[?]` bracket button), and a circular `↑` send button pinned right (`margin-left:auto`) that keeps the exact `id="transmitBtn"` + `onclick="submitCommandInput()"` the old bottom `> TRANSMIT PROTOCOL` button had, which is now removed entirely; the image-preview and token-budget readout move inside the composer too (the token count sits on its own row below the toolbar so its longer text never crowds the icon row at 360/412px) — every element keeps its exact `id`, so every JS reference (`getElementById` only, zero DOM-adjacency dependencies across `triggerImageUpload`/`handleImageSelection`/`submitCommandInput`/`updateTokenBudget`) resolves identically (Protocol 22 — reskin, not a fork). `transmitMessage()`'s busy/cancel/reset button states shrink from long strings (`> TRANSMITTING...`/`> CANCEL`/`> TRANSMIT PROTOCOL`) to short glyphs (`⋯`/`✕`/`↑`) + a matching `aria-label` so they fit the small circular button. A pre-existing bug found while adapting this button (Protocol 42): the `finally` block rebound `btn.onclick` straight to `transmitMessage()` instead of back to `submitCommandInput()`, so every click after the FIRST completed AI round-trip silently skipped `submitCommandInput()`'s TERMINAL-mode/quick-log routing — fixed by restoring `btn.onclick = () => submitCommandInput();`, the button's original inline-handler entry point. Suite 162 extended again (162.20–162.23, 19→tests, 2001 total). Cache r46. DO-O follow-up, part 3 — the composer INTEGRATES into the transcript box (owner refinement): a `.transcript-card` now wraps BOTH `#chatDisplay` and `#composer` (transcript first, composer second, in source order) so the composer pill docks flush at the bottom of the SAME messenger-style card instead of reading as a separate box below it — the card owns the ONE visible border/20px-radius/background/`overflow:hidden`, `#chatDisplay` goes borderless/transparent, and `.composer` keeps only a subtle top divider. Making `.transcript-card` (not `#chatDisplay` directly) the flex-grow item surfaced a real desktop crush bug found live during this unit's own verification (Protocol 42): with the command tray defaulting open on desktop and `.transcript-card`'s `min-height:0`, `.chat-panel`'s flex algorithm could shrink the card toward zero before its own overflow ever engaged, crushing `#chatDisplay` against `.composer`'s fixed height inside the card's `overflow:hidden` with no scrollbar anywhere — fixed with a real `min-height:282px` floor on desktop `.transcript-card` (sized for `#chatDisplay`'s true 90px-content-plus-30px-padding minimum, since it isn't `box-sizing:border-box`) plus `overflow-y:auto` on `.panel.chat-panel` itself, so any further squeeze scrolls the panel instead of clipping the transcript. Verified at 360/412px and desktop from 800–1024px viewport heights, tray both open and collapsed. Suite 162 extended again (162.20b/162.23a–c/162.24, +tests, 2005 total). Cache r47. Small-UI-polish batch — owner-reported composer/pill/help-button fixes: `.composer-input` starts small (a 40px floor sized by `_autoGrowComposer()` to fit its own placeholder, not a fixed box) and grows with typed content up to a 160px cap that scrolls, replacing the fixed 80px (desktop) / 76px (mobile) `#chatInput` heights entirely (both now keep only `flex-shrink:0`); `_wireComposerAutoGrow()` wires the re-measure to every keystroke from boot (alongside `_wireModeHint()`), `_renderModePill()` re-measures on every mode toggle so the box refits the new placeholder, and `transmitMessage()`/`transmitTerminal()` (api.js) both call `_autoGrowComposer()` immediately after clearing `#chatInput`, snapping the box back to its small size after every send. The mode pill's touch-sticky `:hover` fill (the general `button.action-btn:hover` rule persisting after a tap with no `pointerleave` to clear it on mobile) is neutralized: `button.mode-pill--terminal:hover`/`--overseer:hover` unconditionally reset `background`/`filter` back to the pill's own resting look, with the real green fill restored only inside a `@media (hover: hover) and (pointer: fine)` gate (the same signal Suite 129 already uses) so real mouse users keep the feedback; `toggleInputMode()` also blurs the pill after a tap as a second line of defense. The ALL SAVES bracket `[?]` help button is modernized to the same round shape as the composer's `?` via a new shared `.icon-btn-round` class (Protocol 22 reuse) driven by a `--icon-btn-color` custom property (falls back to `--bezel-wire`; the save-menu button sets it to the existing `--robco-blue` so its panel's accent color is unchanged). Suite 162 extended again (162.25–162.28, +tests, 31 total); Suite 103 extended (103.3 updated + 103.8 added, +1 test, 8 total). Cache r48. Step 2 (v2.8.0) owner batch — SAVES LIST overhaul: local DELETE, cloud VERSION HISTORY, per-game filter, row-fit (Suite 163: tests) — four owner reports fixed together: local save slots gain a confirm-gated DELETE control (`confirmDeleteSlot()`/`_deleteSlotApply()`, ui-saves.js) that clears the localStorage mirror, the IDB-primary slot, and its P5 version ring together, so a deleted slot leaves nothing orphaned; cloud saves gain their own VERSION HISTORY — an additive Firestore subcollection (`users/{uid}/saves/{docId}/versions`) mirroring the local P5 ring structurally rather than as an embedded array field (a single Firestore document is capped at 1MB, and stacking several full campaign+chat snapshots into one doc could approach that ceiling), archived by `_pushCloudSaveVersion()` before every `overwriteCloudSave()` write and capped/oldest-pruned via `deleteDoc`, with a `versionCount` field stamped onto the parent doc so the SAVES LIST can show a VER button with no extra read, `window.listCloudSaveVersions`/`window.restoreCloudSaveVersion` mirroring `readSlotVersions`/`restoreSlotVersion`'s fail-safe + confirm-gated + local-only-apply contract, `viewCloudSaveVersions()` (ui-saves.js) mirroring `viewSlotVersions()`'s modal exactly, and `deleteCloudSave()` now purging a save's version subcollection first (Firestore doesn't cascade-delete); the SAVES LIST filters BOTH local slots and cloud saves to the ACTIVE game (`getGameContext()`), degrading to SHOW — never hide — a save with no recorded `gameContext` (an older save predating the field), with the empty state distinguishing "no archives for the active game" from "no archives at all"; and the row markup moves from inline styles to `.save-row`/`.save-row-label`/`.save-row-actions` CSS classes, with `.save-row-actions` wrapping (`flex-wrap`) so the growing LOAD/OVERWRITE/VER/DELETE(/NAME) button set never clips at 360/412px. tests total. Step 2 (v2.8.0) Design Overhaul — the command-cluster overhaul: Tool Deck + Quick-Draw Holster (Suite 164: tests — the always-present D-Pad + native-command cluster is retired in favor of a zero-standing-footprint ◈ launcher key in the composer toolbar that raises an in-glass Tool Deck bottom sheet (THREAT/VATS/TRADE/LOOT/CONSULT/CALC keycap-chip rows, reusing every tool's existing handler unchanged — Protocol 22); the D-Pad itself is redesigned into the Quick-Draw Holster, four gear-vector sockets that SHOW the bound gear (LED lit) instead of firing blind, backed by a NEW native, player-authored state field state.padBindings ({up,down,left,right} -> gear name or null, full Protocol 4: default, migration normalizing bogus shapes to the all-null default, sanitizeImportedContainer coercion on the serialized-whole cloud/file path); [BIND: gear, DIR]/[PAD: DIR] become native regex intercepts in _routeNativeCommand() mirroring [WAIT:], with _nativePadBind()/_nativePadFire() (ui-core.js) the sole reader/writer -- the AI is fully blind to padBindings (never in getSystemDirective, never mapped in autoImportState -- Protocol 14/24 proven by a VM-sandbox behavioral test); firing a bound socket hands the Director a resolved "Deploy <gear>" action. Cache r50. Owner-report follow-up — Tool Deck #deckTarget autocomplete + no-autofocus-on-open (Suite 164 extended, +tests, 30 total): the shared TARGET/ITEM/TOPIC field is wired to the existing registry-autocomplete singleton via a new _deckTargetSuggestions() source (ui-render.js, Protocol 22 — combines getBestiaryNames() creature names with registrySearch() across the same _CONSULT_CATS CONSULT already searches, deduped and capped at 8) and wireInput('deckTarget', _deckTargetSuggestions) in initRegistryAutocomplete() (ui-saves.js); openToolDeck() (ui-core.js) no longer calls target.focus() on open, since the owner reported the mobile keyboard popping up over the Quick-Draw Holster sockets — the field still focuses on a user tap or via the BIND ▸ flow, which specifically needs it. Cache r51. Owner-reported batch — functional casing lamps + Overseer routing + live VITALS strip (Suite 165: tests — the casing-top PWR/UPLINK/FAULT lamps and the Overseer's [NO CARRIER] tag go from decorative to real `<button>`s (Protocol UI-5): PWR routes to the existing `AmbientRuntime.shutdown()` (Protocol 22, unlit/lit synced from the shutdown-crt observer's onEnter/onExit, no new observer); UPLINK and the Overseer's own resting tag share ONE connection signal, `_isUplinkConnected()` (wraps `_overseerRestState(_overseerRestSignals())`, deliberately the same lighter hasKey/aiEnabled/online check the Overseer already used — not SLOT 05's stricter validated-key board status), both routing to `_openAiUplinkSlot()` (reuses `selectSubsystem('chassis')` then opens/scrolls the `data-sub-id="slot_05_uplink"` sub-panel); FAULT reuses the existing `showErrorLog()`; the scope-tag button is `disabled` except in disabled/offline (native attribute + a defense-in-depth check in `_scopeTagClick()`); `refreshOverseerCarrier()` is the one choke point that now also refreshes the UPLINK lamp and the bezel telemetry strip, and `saveApiKeySilent()` calls it so adding/removing the key live-flips NO CARRIER/LISTENING with no reload; and the bezel `▸ SUBSYSTEM: …` strip gains a live `VITALS <tier> · RAD <n> · CARRIER <ONLINE|OFFLINE>` suffix on every subsystem (not just OPERATOR) via `_vitalsTier()` (HP% plus any crippled la/ra/ll/rl/hd limb, game-agnostic) and `_bezelStatusSuffix()`, both wired through the existing `updateMath()` choke point — zero new campaign-state writes anywhere in the batch). Cache r52. Owner directive — TERMLINK Command Console fully retired (Suite 168: tests — TERMLINK (WU-F9) was the predecessor of the Tool Deck and had become a redundant second way to reach the same six subsystems; removed entirely rather than left unreachable like the earlier [CROSSROADS] retirement — the router entries, the console manifest/launcher/renderer, the COMMAND_REGISTRY entry, and the console CSS are all deleted, [TERMLINK]/[TL] are locked into the Suite 113 RETIRED-macro list, and BIO-SCAN plus the Tool Deck are proven untouched). Cache r56. Owner batch — Style A transcript cleanup + composer autocomplete drop-up + Overseer LISTENING pulse (Suite 169: tests — the transcript's old bounce (right-aligned/border'd user lines, centered italic system lines, italic tight-leading AI lines) is replaced with the approved chat-overhaul mockup's shared cleanup: every line left-anchored + full-width with a 14px hanging indent, voice carried by color/weight/size only, while Style A's OVERSEER tag-above is kept and mirrored by a symmetric TERMINAL tag on user lines (`appendToChat()` reskin only, Protocol 22 — the tag is display-only, never written into `chatHistory`); the composer's (`#chatInput`) registry-autocomplete singleton now drops UP instead of down for that one input via a dedicated `acDropsUp()` gate in `acPosition()` (`ui-saves.js`), since dropping down covered the composer's own toolbar/send button — every other registry-backed input keeps dropping down unchanged; and tapping the Overseer scope's `[ LISTENING ]` tag (now enabled in that state, not just disabled/offline) triggers a transient one-shot amplitude pulse on the waveform (`_scopePulse()`), gated by the exact same `_scopeShouldAnimate()` reduced-motion/dial/hidden/standby check every other scope motion already obeys, never a bespoke carve-out, and never touching `_scopeState` or writing any campaign state (Protocol UI-10). Cache r61. Step 2 (v2.8.0) SETTINGS tab [6] + CHASSIS reorg, SU-1+SU-2 combined (Suite 176: tests, plus Suite 158 extended +tests for the 7th keycap; Suites 119/125/166/123.7 reworked for the new panel homes) — a 7th bezel keycap (`[6] SETTINGS`, sub-label `CONFIG·ACCT`) opens a new tab-gated SETTINGS subsystem, using the identical `switchTab()`/`_NAV_TAB_FOR`/`TAB_TO_SUBSYSTEM` machinery every other subsystem already uses (Protocol 22 — no forked routing): `#accountPanel`, `#securityConfigPanel` (Module Bay + SVC tray), a newly-extracted `#savesPanel` (SAVE ARCHIVE), and a newly-extracted `#campaignConfigPanel` (CAMPAIGN CONFIGS — GAME/PLAYSTYLE/PLAYTHROUGH/COMPLETE-RNG/DANGER-ZONE) all move to `data-tab="settings"`, removed from DATABANK/always-visible; `#campgPanel` keeps only its 3 record sub-panels (campaign status/crossroads/incident log) and is renamed `CAMPAIGN RECORD`. CHASSIS becomes a real tab too (`data-tab="chassis"`, replacing the old scroll-to-Module-Bay hack in `selectSubsystem()`) hosting a new `#systemStatusPanel` (SYSTEM STATUS) that merges the WU-F7 device-telemetry half of the former Overseer's Log with a new `renderSystemStatus()` readout (APP_VERSION, the real active Cache Storage key read via `caches.keys()` rather than duplicating the sw.js `CACHE_NAME` literal, carrier status via the existing `_isUplinkConnected()`, and every `isFeatureEnabled()` flag) plus the FIRMWARE REVISION LOG button (moved out of the SVC tray) and a new ERROR LOG button (`showErrorLog()`); the campaign-stats half of the former Overseer's Log becomes its own `#campaignLogPanel` (CAMPAIGN LOG) on DATABANK. `renderSystemStatus()` is wired into both `loadUI()` and `refreshOverseerCarrier()` (the single carrier/flag choke point, Protocol 22) so it live-updates with the UPLINK lamp and bezel telemetry. The first-visit Module Bay hatch ceremony now fires from a genuine `selectSubsystem('settings')` visit (re-opening `#securityConfigPanel`, which fires its own once-only toggle listener) and never from `initTabs()`'s boot-time restore (Protocol 42 — verified live: a reload with SETTINGS active restores the 4 panels + bezel highlight with the hatch still hidden). `expandPanelForCategory()`'s `config`/`log` categories are re-routed to their new homes (`settings`/CAMPAIGN CONFIGS and `chassis`/SYSTEM STATUS) instead of the retired CAMPG/Overseer's-Log targets. Functional relocation only (Protocol 22/25) — no visual redesign of the moved content (that is separately-gated future work); verified live at 360/412/desktop with zero horizontal overflow across all 7 keycaps + DIR. Cache r70. Step 2 (v2.8.0) SU-4: dynamic ACCOUNT (REG PORT) status words (Suite 177: tests) — the REG PORT / Operator Registry board's diegetic status text is now driven entirely by real auth + connection state instead of a largely-static story: `renderAccount()` (`js/ui-account.js`) reads `window.getAccountState()`, `isFeatureEnabled('googleSignIn')`, and the SAME `_isUplinkConnected()` carrier signal the UPLINK lamp/bezel telemetry/SYSTEM STATUS panel already use (Protocol 22 — no parallel `navigator.onLine` check), across four conditions: signed-out + googleSignIn ON → `NO OPERATOR ON RECORD` / `REG PORT VACANT — LOCAL ARCHIVES ACTIVE · CLOUD ARCHIVE SYNC OFFLINE` + an ESTABLISH GOOGLE UPLINK button; signed-out + googleSignIn OFF → `LOCAL ARCHIVES ACTIVE`, no button; signed-in + carrier connected → `OPERATOR VERIFIED` + name/email + `CLOUD ARCHIVES + CIPHER-KEY SYNC AVAILABLE` + a SEVER UPLINK button; signed-in + carrier disconnected → `OPERATOR VERIFIED · CLOUD ARCHIVE SYNC OFFLINE (NO CARRIER)`, sever-uplink button still present. A new collapsed-state summary line (`#acctSummaryStatus`, styled via a new `.panel-substatus` utility class) sits in the ACCOUNT panel's `<summary>` so the state is visible even while collapsed. `renderAccount()` is now called from `refreshOverseerCarrier()` (alongside `_updateUplinkLamp()`/`_refreshBezelTelemetry()`/`renderSystemStatus()`) — the same single choke point already driving every other connection-derived readout, so the account words, the UPLINK lamp, the bezel telemetry, and SYSTEM STATUS's carrier line can never disagree; sign-in/out/collision (3 call sites in `cloud.js`) and boot (`loadUI()`) already called `renderAccount()` and are unaffected. A Protocol 42 fix landed in the same commit, caught live during render-verify: the summary line is assigned via `.textContent` (never parsed as HTML), so the shared name/email separator was switched from the `·` HTML entity (which rendered as the literal text "·" in the collapsed summary) to the real Unicode `·` character, which is safe in both the `.textContent` summary and the `.innerHTML` board. A Node `vm`-sandbox behavioral test (mirrored via a node-shelled temp-file script in the PowerShell runner, Protocol 42's stdin-corruption-safe transport) actually executes the real `renderAccount()` body across all four conditions and asserts the resulting DOM text, including a regression guard that the summary never contains a raw `·` entity. Zero campaign-state write; game-agnostic (Protocol 38 — generic operator/registry fiction, no game literals). Cache r71. Step 2 (v2.8.0) SU-3: CAMPAIGN CONFIGS modernized (Suites 178: tests; 92.6/92.7 reworked) — the `#campaignConfigPanel` config controls (GAME/PLAYSTYLE/PLAYTHROUGH TYPE/COMPLETE RNG/WIPE TERMINAL) are reskinned into the two-board Module-Bay hardware language matching the Director-Uplink/Module-Bay aesthetic (owner-approved mockup, Protocol 25 sanctioned exception) — RESKIN ONLY, every control keeps its exact id/onchange/onclick (Protocol 22). **CAMPAIGN PROFILE** (green board): GAME becomes two seatable PROGRAM CARTRIDGE buttons (`#cart-fnv`/`#cart-fo3`), PLAYSTYLE becomes a 2-position ENGAGEMENT DOCTRINE rocker (`#rk-any`/`#rk-melee`), PLAYTHROUGH TYPE becomes a decorative rotating dial + 5 direct-pick OPERATIONAL TEMPO detents (one tap per pick, fewer taps than the old `<select>`'s open-then-pick). **RANDOMIZER INTERLOCK · PURGE** (red danger board): COMPLETE RNG becomes a breaker under an amber safety cover in a hazard-striped well — two deliberate taps to arm (lift cover, throw lever) so a stray tap can't arm it — with an always-visible commit-sequence legend (`SAFE ▸ ARMED — reversible ▸ + WIPE ▸ SEALED — permanent`) that lights the current step; SEALED (`rng-locked`) shows a red wire-seal `✕` + lead disc welded over the lever, CSS-gated purely on `data-rng="locked"`. WIPE TERMINAL sits on the same board as its commit point, restyled `.purge-btn`, `wipeTerminal()`'s double-confirm and RNG-armed warning untouched. Every real control (`#gameContextSelect`/`#playstyleInput`/`#playthroughTypeSelect`/`#completeRngToggle`) stays in the DOM as a genuine, keyboard/AT-reachable control via `.bay-visually-hidden-input` — the exact technique already shipped for `#immersionSelect` (Protocol 17/22) — driven by new wrapper functions (`_seatGameCartridge`/`_confirmGameContextChange`, `_setDoctrine`, `_setTempo`, `_interlockLiftCover`/`_interlockThrowBreaker`) that delegate to the unchanged `onGameContextChange`/`changePlaystyle`/`onPlaythroughTypeChange`/`onCampaignModeChange` — one truth, two entry points, never a forked persistence path. New in this unit (owner decision): a game-cartridge swap is now gated behind a diegetic `confirmAction()` — confirm proceeds to the unchanged `onGameContextChange()` (still reboots the terminal); cancel reverts the hidden select to the still-active game and re-syncs the profile UI, never touching `onGameContextChange` at all, so an accidental cartridge tap can no longer reload unprompted. `_syncCampaignProfileUI()`/`_syncInterlockUI()` are the two repaint functions — wired into both `_restoreDevicePrefs()` (boot) and `loadUI()` — that keep the custom cartridges/rocker/detents/dial/breaker/well/cover/seal/sequence-legend/summary-lines in sync with the real, underlying state; both boards get a collapsed one-line status row (`#sum-profile`/`#sum-ilk`) inside their own `<summary>`, matching the `#acctSummaryStatus` pattern from SU-4, plus a small LED (`#profileLed`/`#ilkLed`, the latter pulsing amber when ARMED). A Protocol 42 fix landed in the same commit, caught live during render-verify: `#completeRngToggle` being a real, keyboard-reachable control meant toggling it DIRECTLY (bypassing the breaker button entirely) only updated the ARMED banner and left the breaker/well/cover/seal/title/desc/summary/sequence-legend stale — `onCampaignModeChange()` now also calls `_syncInterlockUI()`, and the now-redundant banner-toggle lines duplicated in the boot-restore and `loadUI()` blocks were removed in favor of `_syncInterlockUI()` owning the whole board's repaint. Suite 92's `.rng-mode-group` guard (WU-C14, which protected a now-hidden visible checkbox label from wrapping) is retired in favor of an equivalent guard on `.seq-step` (the interlock's own short-text legend, which now carries the same nowrap protection). Game-agnostic (Protocol 38 — the doctrine rocker and tempo detent rack carry no game literal; the GAME cartridge itself is the sanctioned per-game picker). Mobile-compact, ≥28px tap targets (cartridges 74px, breaker well 104×118px, rocker/breaker/cover 44px, detents 34px), ≥16px inputs; verified live at 360/412/desktop with zero horizontal overflow, SAFE/ARMED/SEALED interlock states, and the cartridge-swap confirm/cancel round-trip. Cache r72. Step 2 (v2.8.0) SU-3 follow-up — OPERATIONAL TEMPO reworked into a true centered rotary dial (owner-approved mockup: `planning/2.8.0/mockups/tempo-dial.html`; Suite 180: tests) — the previous decorative-dial-plus-vertical-detent-list layout (Suite 178/179) is retired: the knob sits centered as the assembly's centerpiece, the 5 playthroughType positions (STANDARD/MIN-MAXED/COMPLETIONIST/CASUAL/SPEEDRUN) ring it on a −84°→+84° gauge arc (42° apart, 0°=up) with a lit tick per position, and the knob's own rotation (the needle) always points exactly at the active position — never at empty space; only short labels sit on the ring, with the active option's full name + description surfaced on a `.tempo-readout` LCD-style panel directly under the knob (the mobile-legibility fix the mockup solves, verified at 360px). Three input paths all converge on the unchanged `_setTempo()`/`onPlaythroughTypeChange()` (Protocol 22, one truth): dragging the knob (new angle-based `_tempoPointerDown`/`_tempoPointerMove`/`_tempoPointerUp`/`_tempoPointerCancel`, sharing the Immersion dial's drag PATTERN — pointer capture, a moved-flag gating a real drag vs a tap, listeners added on down and removed on up/cancel — but with its own angle math, since the two dials' geometries are genuinely different (3-tier pixel-step vs 5-position radial), never a forked duplicate of unrelated math), tapping a `.detent2` position label directly, or arrow/Home/End keys (the knob is a real `role=slider`). Owner directive: tapping the knob BODY does nothing at all — no onclick attribute, no click listener anywhere — only a drag, a direct position tap, or the arrow keys change the value, a deliberate deviation from the Immersion dial's own tap-to-cycle behavior. `_syncCampaignProfileUI()` remains the single repaint function, now also owning the needle angle, tick/detent lit-state, and readout text — never drifting from the hidden `#playthroughTypeSelect`. Rotation is a plain CSS `transition:` (never `animation:`), so the existing global `prefers-reduced-motion` block neutralises it to an instant snap with no bespoke carve-out; `touch-action:none` is scoped to the knob only, so the surrounding page still scrolls. A Protocol 42 fix landed in the same commit, caught by the gate itself: the dial's own desktop-only size bump initially used a bare `@media (min-width:1000px)` query, which Suite 129 (first-load desktop-layout pointer/hover gate) specifically forbids — any ungated width query risks a touch phone with a mis-measured viewport briefly booting into desktop sizing — fixed to carry the same `(hover:hover) and (pointer:fine)` gate every other desktop-only tweak in the file already does. Game-agnostic (Protocol 38 — the 5 tempo names/descriptions ride the existing `_TEMPO_LABELS`/`_TEMPO_DESC` maps, no per-game literal). Cache r77. Step 2 (v2.8.0) Phase 3 · Piece 2 — OPERATIONS "quartermaster's freight console" reskin (Suite 185: tests — BUS-10–15, dressed as the owner-approved mockup: a LOAD-CELL WEIGH BRIDGE (BUS-10) mirrors real curWt/maxWeight via a new `_paintWeighBridge()` hook inside `updateMath()`, bending a load-beam SVG continuously via a plain CSS `transition: d`/`transition: y` (reduced-motion collapses it to an instant snap, no bespoke carve-out) through nominal/amber-heavy/red-SEIZED tiers driven by the existing `body.weight-heavy/-critical/-over` classes, with a SEIZED stamp/board-LED/bezel-telemetry ('CARGO SEIZED') tie-in; `#c_caps`/`#display_weight` relocated verbatim (same ids, same `updateMath()`/`doBuy`/`doSell` references) off OPERATOR's readback strip onto the bridge, leaving OPERATOR with just MAX AP + LEVEL UP; CARGO MANIFEST (BUS-11) reskins the old flat inventory filter into a six-drawer pull-bank (weapon/armor/aid/mod/misc/ammo, no 'All' drawer — the physical design has no such category), one drawer open at a time via the unchanged `setInvFilter()`, with a live per-drawer count badge, an in-drawer search field, and the open drawer's list bounded to a fixed-height in-panel scroll region (a phosphor scrollbar, no render cap — every item reachable by scrolling) — the AMMO drawer folds the existing AMMO RESERVES tray in place of the item list via the same display toggle; rows are the approved Variant A 'CARGO TAG' punched-freight-tag style with an amber 'IN SERVICE' stamp on the equipped item and no condition indicator (the app never tracked one); two new native write paths close real gaps — `adjItemQty(idx, delta)` (a per-row qty ± stepper, clamped ≥0, removing the row at 0) and `toggleEquipItem(idx)` (the first native EQUIP control, writing `state.equipped.weapon`/`.armor` one-per-slot-family, closing the U10 audit gap — `autoImportState()`'s own AI-write equip path is untouched, Protocol 14/24 additive); FIELD FABRICATION (BUS-12, ex-CRAFTING) gains per-ingredient HAVE/NEED meter-fill bars (short = red) alongside the existing have/need numbers; BARTER UPLINK (BUS-13) leans amber via the existing `--bezel-wire` token (a new `.wireboard` modifier, game-agnostic — falls back to local phosphor for an unauthored game, distinct from SLOT 05's fixed-amber `.uplink`); SQUAD ROSTER (BUS-14, ex-SQUAD STATUS) fixes a real Protocol 38 bug — the ENLIST `<select>` was a hardcoded 8-name FNV companion list shown even in FO3 campaigns — via a new `_populateSquadEnlistOptions()` reading `FALLOUT_REGISTRY.companions` (mirrors `renderCollectibles()`'s registry-read pattern); CURIO ARCHIVE (BUS-15, ex-COLLECTIBLES) is a wrapper-only dress (the tracker-row/tracker-toggle contract, Suite 88, is untouched); every board carries a live `.panel-substatus` 0i summary line; `robco_cargo_drawer` is a new registered MetaStore device pref (Protocol UI-6) restoring the last-open drawer at boot; zero new campaign-state field. Cache r84. Step 2 (v2.8.0) Phase 3 OPERATOR batch 2 — BUS-05/07/08 ground-up reskin (Suite 186: tests): the SKILL MATRIX, STATUS EFFECTS, and FACTION STANDING boards, previously light-framed only, get their own owner-approved ground-up redesigns per planning/2.8.0/mockups/operator-boards2.html — SKILL MATRIX becomes a 13-channel horizontal VU meter array (renderSkills() keeps emitting sk_<key> inputs, channel count from getSkillKeys(), Protocol 38; a new drag-to-set + arrow-key affordance on each channel's track routes through the exact same 0-100 clamp + saveState() the typed field already used, via _skillVuSet()/_onSkillVuInput()/_wireSkillVuDrag(), the last idempotently guarded against re-wiring on re-render); STATUS EFFECTS becomes lit/dark compound lamp tiles color-coded BUFF phosphor/DEBUFF red/NEUTRAL amber with a tick countdown + pip meter and a purge key still calling the unchanged removeStatusEffect(i) (the add-form ids/addStatusEffect() untouched), with a new _statusLampSummary() live 0i board-status line ("N LAMPS LIT . N DEBUFF . NEXT EXPIRY: ...") wired into the existing _syncOperatorTelemetry() choke point, and an empty state of dark standby lamps instead of a bare note; FACTION STANDING becomes one shared INFAMY<>FAME reputation console — a channel keycap selector spanning the FULL getFactionRegistry() (major+minor together, retiring the old collapsed MINOR FACTIONS sub-panel since the console adds no extra disclosure tap, Protocol 25), the selected channel's wide dual-scale meter with the same four adjustFaction() +-5 keys, and an all-faction mini-pin strip so no faction's standing is ever hidden behind the selector, with the last-picked channel persisted via the new robco_faction_channel MetaStore device pref (Protocol UI-6); all three boards keep their exact shipped ids/handlers (Protocol 22/25 reskin, no functionality regression), stay game-agnostic (no hardcoded faction-key arrays or per-game skill-count literals), and use only plain animation: keyframes for the VU fill-in/lamp pulse (auto-neutralized by the existing global reduced-motion block, Protocol UI-9); a Protocol 42 fix landed in the same commit — a live render-check caught the reputation console's pin marker bleeding a couple px past its own track at a 0%/100% fame/infamy extreme, fixed by adding it to the render-check's established harmless-decorative-bleed exclusion list (the same category as the BUS-02 fader cap) rather than clipping it, which would have cut off the pin's intentional vertical protrusion too. Cache r85. Step 2 (v2.8.0) Phase 3 OPERATOR batch 3 — the last five OPERATOR boards, ground-up (BUS-04 POSITION & MISSION CLOCK flip-card mission clock + position-fix plate, BUS-06 PERK LOADOUT numbered loadout-slot rack with a bounded in-panel scroll region + search reusing the CARGO MANIFEST drawer pattern plus FNV FACTORY-BURNED trait chips, BUS-05a SKILL BOOKS promoted from a SKILL MATRIX sub-panel to its own reference-shelf board, BUS-05b SKILL MAGAZINES promoted to its own periodical-rack board, BUS-09 KARMA ALIGNMENT — the stat_karma/karma_label control relocated here from BUS-01 (the #c_caps/BUS-10 precedent) into an EVIL/GOOD swing-needle gauge + tier-lamp strip, now a UNIVERSAL board for every game with the FO3 KARMA CENTER appendix nested inside and gated by usesKarmaCenter); every id/handler the PHASE3_OPERATOR_PLAN.md contract lists stays unchanged (Protocol 22 reskin); a Suite 187 + reworked Suite 85/87/181 lock the new contract; a real render-verify bug (button.tracker-toggle's specificity beating the plain .spine/.mag base rules, leaving every shelf spine/magazine cover rendering as a horizontal chip instead of a vertical spine/cover) was caught live and fixed by bumping both to button.spine/button.mag (Protocol 42). Cache r86. Owner follow-up fix — CARGO MANIFEST row USE button clipping (Suite 188: tests): the pre-existing `.inventory-list li` rule (element+class, specificity 0-1-1) was beating `.mrow`'s bare class (0-1-0) for `padding`/`justify-content`/`border-bottom`, silently collapsing the CARGO TAG row's 30px left clearance back to 0 and pulling the USE button into the tag's pointed `clip-path` corner + eyelet hole, clipping the label (owner screenshot report; root cause confirmed live via `getComputedStyle()` before any fix, Protocol 27). Fixed with a scoped `.tray-list .mrow` override (specificity 0-2-0, beats `.inventory-list li` regardless of source order) restoring the 30px padding + `justify-content: flex-start` + neutralising the leaked `border-bottom`, plus a `.tray-list .mrow .use-btn` rule adding breathing-room margin/padding — the shared, app-wide `.use-btn` class (its 44px `min-height` tap-target convention used elsewhere) is untouched (Protocol 22). CSS/layout only — zero id/handler/markup-order change, verified by re-reading `renderInventory()`'s row template byte-for-byte. Render-verified live at 360/412/desktop (12px clearance between the eyelet and the now-unclipped 'USE' label). Cache r87. Owner follow-up — BUS-08 reputation console MAJOR/MINOR grouping restored (Suite 186 extended to tests): the owner reported the keycap channel selector had flattened all 11 factions into one grid, losing the pre-reskin card layout's MAJOR FACTIONS / MINOR FACTIONS categorization; investigated via git history (the pre-reskin renderFactionRep(), still on `13680f8^`, split `getFactionRegistry().filter(f => f.tier === 'major'/'minor')` — a data-driven registry field already declared on every faction entry in both `FACTION_REGISTRY` (FNV) and `FACTION_REGISTRY_FO3`, Protocol 38) — restored the same `f.tier` split on the console's selector via a new `selectorSection(list, label)` helper building a labeled `.facon-section` (`.facon-section-label` + its own `.facon-selector` row) per tier, plus a defensive `OTHER FACTIONS` bucket for any future faction lacking a recognized tier so nothing is silently dropped; the old collapsible `<details data-sub-id="minor_factions">` disclosure is NOT reintroduced — both sections stay always-visible, so this is zero added taps over the flat selector (Protocol 25), and the shared meter/±5-keys/mini-pin-strip mechanic is untouched. Cache r88. Owner follow-up — BUS-03 RAD EXPOSURE drag actually fixed (Suite 184 extended to tests): the owner reported dragging still didn't work despite the earlier commit's own regression test (184.6) passing — investigated live in a real browser (Protocol 27) rather than trusting the mock-event test, which calls setupRadBarInteraction()'s listener functions directly and so never exercises real browser touch-scroll arbitration or CSS transition timing; found #radDragTrack was the one drag surface in the whole OPERATOR redesign missing `touch-action: none` (every sibling — `.fd-ladder`, `.vu-track`, the tempo/immersion dial knobs — already declares it), meaning a real touch-drag could be reinterpreted as a page-scroll gesture before the touchmove handler ever ran, and separately `.bar-fill.rad` carried a 0.9s cubic-bezier transition (vs `.hp-bar-fill`'s 0.3s) that visibly lagged behind a real-time drag; fixed both (touch-action added, transition shortened to match HP's exactly) and re-verified live via a real elementFromPoint hit-test + mousedown/mousemove/mouseup sequence (not a direct dispatch bypassing hit-testing) proving state.rads/#stat_rads/the bar's width all track the drag continuously and clamp at GAME_DEFS[ctx].maxRads. Cache r89. Step 2 (v2.8.0) Phase 3 · Piece 3 — DATABANK "THE RECORDS BAY" ground-up reskin (BUS-16…21, owner-approved `databank-records-bay.html` mockup; Suite 189: tests) — all six DATABANK boards rebuilt: BUS-16 CARTOGRAPHY TABLE retires the boxed 6×6 CSS grid entirely in favor of "Phosphor Cartography," an inline SVG spatial node map (`renderWorldMap()` rewritten, built with one `map().join('')` bulk assignment) — glyph nodes plot at each zone's real `gridRow`/`gridCol`, fog-of-war (`fog = !isCur && !isVisited`) suppresses the node label entirely rather than hiding a rendered one, a known-route trail connects consecutive discovered zones from `state.locationHistory` (capped at 25 segments), typed signal-return glyphs (★/◆ driven by the existing per-game `collectibleLabel` field, ▲ for `lincolnMemorabilia`) mark uncollected pickups, a `[YOU]` reticle blinks (PLOT-FIX) at the current-zone match, a rotating amber SURVEY-SWEEP overlays the chart, and real per-node keyboard traversal (`_mapNodeKeyNav` — arrow keys to the nearest node by grid coordinate, Enter/Space to zoom) replaces the old click/tap-only grid; the zoom detail becomes a reskinned "sector sheet" (`.loc-row`/`.loc-st`) with the MARK SURVEYED key (renamed from LOG VISIT) still routing through the unchanged `markLocationVisited`→`recordLocationVisit` single source; the dead `_mapAbbrev`/`_MAP_ABBREV` abbreviation table and all now-orphaned boxed-grid CSS (`.map-cell`/`.map-detail-*`/`.map-legend`/`.map-toggle-btn`/`.map-you-marker`) are deleted, keeping only `.map-back-btn`/`.map-collectible-badge` (still reused by the sector sheet). BUS-17 DIRECTIVE REGISTRY reskins `renderQuests()` into numbered directive slots with status lamps inside a bounded `.tray-scrollwrap`, adds an ALL/ACTIVE/COMPLETE/FAILED status drawer bank (a display-only filter, last-open choice persisted via the new registered `robco_databank_qdrawer` MetaStore pref) plus an in-tray search, and adds the owner-locked **⟳ CYCLE key** (`cycleQuestStatus(idx)`) — the ONE new native write path this build ships, cycling a directive ACTIVE→COMPLETE→FAILED→ACTIVE with no AI involved (a real VM-sandbox behavioral proof, not just a static check); `autoImportState()`'s own AI-write quest-status path is untouched (Protocol 14/24 — additive, not a fork). BUS-18 CAMPAIGN CHRONICLE dresses `renderCampaignStatus()` as a tape-spool record (reeled `.spool-list`/`.rec-line` crossroads history, stamped `.incident` milestone entries, a `.stat-head` counter trio, `.stch` faction-standing chips). BUS-19 CATALOG QUERY reskins the DATABANK search panel amber (`.wireboard`, `--bezel-wire`) purely via CSS scoped to `#databankPanel` — the shared `_consultSearch`/`_consultRenderHTML` CONSULT engine is byte-identical, reused (not forked) by both this panel and the CONSULT modal elsewhere. BUS-20 FIELD NOTES adds a bounded scroll + optional in-tray search to `renderCampaignNotes()` (display-only, the auto-log `[T…]` dimming distinction kept). BUS-21 SERVICE TALLY reskins `renderSessionStats()` into a mechanical odometer/counter bank (`_odoTile()` helper, digit-cell tiles) with a ZERO CAMPAIGN COUNTERS key (renamed from RESET CAMPAIGN STATS, same `resetSessionStats()` handler). A new `identity.databank` facet (`mapCaption`/`mapCaptionSub`/`recordsLabel`) extends the DO-K per-game identity keystone (Protocol 38) on all three GAME_DEFS entries — proven the FO3 frame reads CAPITAL WASTELAND vs NV's MOJAVE WASTELAND with zero JS ctx branch. Every id/handler across all six boards is verified preserved by the id-preservation contract (Protocol 22) — `worldMapDisplay`/`questsList`/`databankPanel`/`databankSearch`/`databankResults`/`campaignNotesList`/`newCampaignNote`/`campaignLogPanel`/`sessionStatsList`/`campgPanel`/`campaignStatusDisplay`/`crossroadsDisplay`/`incidentDisplay` and every render/add/remove handler kept exactly. The two heroes (CARTOGRAPHY TABLE + DIRECTIVE REGISTRY) default open; the other four default collapsed with a live `.panel-substatus` 0i summary line each (`dbMapStatus`/`dbQuestStatus`/`dbChronStatus`/`dbCatalogStatus`/`dbNotesStatus`/`dbTallyStatus`). Zero new campaign-state field. Cache r90. Owner polish batch across OPERATOR/SETTINGS/faction (Suite 190: tests) — the VITAL TELEMETRY RAD trace (`#opRadLineWrap`, alongside the HP/GRADE traces which already dragged on real touch) never had ANY drag wiring at all, unlike the SKELETAL HARNESS `#radDragTrack` two earlier fixes had targeted; `setupRadBarInteraction()` now delegates to a shared `_wireRadDragSurface(containerId)` helper wired to BOTH surfaces (Protocol 22), with `#opRadLineWrap` gaining the same `touch-action:none` + `width 0.3s ease` transition fix already proven on the SKELETAL HARNESS bar, verified this time with genuine synthesized touch events (CDP `Input.dispatchTouchEvent`, not a mock function call or a mouse click) against a live-rendered page. `toggleSkillBook()`/`toggleMagazine()` now call `_syncOperatorTelemetry()` immediately after their render (previously only reachable via the next unrelated `updateMath()` call), so the "n/13 READ"/"n/14 CONSUMED" `<summary>` counts update the instant a book/magazine is shelved or unshelved instead of lagging a full render cycle. `.mag.consumed::after`'s CONSUMED stamp gains `white-space:nowrap` (it inherited the ancestor `.panel`'s `overflow-wrap:anywhere`, wrapping to "CONSUM/ED" even though it fit on one line). `selectSubsystem('settings')`'s force-open of `#securityConfigPanel` (Protocol 27 root cause) was unconditional on every SETTINGS visit, silently reopening the Module Bay panel even after the user deliberately collapsed it; it now only auto-opens on a genuine first-ever visit (`robco_bay_opened` not yet `'true'`), after which the panel's own persisted state (`robco_panel_state`, already written unconditionally by `_wirePanelPersistence()`'s toggle listener) is respected like every other panel, across both tab-switch and full reload. BUS-08's MINOR FACTIONS section is now a collapsible `<details class="facon-section sub-panel" data-sub-id="minor_factions_channel">` sitting directly under MAJOR FACTIONS (a new `_wireDynamicSubPanel()` helper in `ui-core.js` re-wires its persistence after every `renderFactionRep()` innerHTML re-render, since a dynamically-rendered sub-panel would otherwise lose its listener and restored state on each repaint — Protocol UI-2); MAJOR FACTIONS stays a plain always-visible section (Protocol 25 — zero added taps there). The all-faction mini pin-strip's `.facon-mini` track was `flex:1 1 60px` (grow+shrink), so its rendered width varied row to row based on how much space the variable-length STANDING label left over; it's now a fixed, non-growing `flex:0 0 90px`, so every row's track renders identically regardless of faction name or standing-word length. Cache r91. Owner-approved BUS-15 CURIO ARCHIVE themed-object redesign (Suite 191: tests) — every collectible now renders as its recognizable Fallout object instead of a plain [ACQUIRED]/[MISSING] text row: NV snow globes as a glass dome + skyline scene on a brass-trimmed base, FO3 bobbleheads as a grinning Vault-Boy figure on a VAULT-TEC base (a plain `@keyframes curioBob` bobble, auto-neutralised by the global reduced-motion block and turned off via `animation: none` on the uncollected state), and FO3 Lincoln relics as per-item typed artifact shapes (rifle/hat/cylinder/figure/poster/book/coin) via a new registry `shape` field on all 9 `lincolnMemorabilia` entries; the object class itself is data-driven (Protocol 38) through a new `GAME_DEFS[ctx].collectibleCategory` token (`snowglobe`/`bobblehead`) consumed by `_curioObjectIconHtml()`, which carries no hardcoded game literal. The objects sit on plank shelves (the BUS-05a skill-books DNA) mounted INSIDE one sealed glass display case — a first pass shipped this as a switchable CASE ⟷ SHELF view toggle, but the owner clarified a display case naturally has shelves inside it, so the toggle (and its `robco_curio_view` MetaStore pref) was retired entirely in favor of ONE unified vitrine: the case's border + dark backlit interior + diagonal glass sheen + `◈ SEALED EXHIBIT` latch plate now wrap the plank-shelf scroll region unconditionally, with the sheen (`::before`, z-index 2) and latch plate (`::after`, z-index 3) painting above the shelves/objects (which set no z-index of their own) so they read as sitting BEHIND the glass. The full pre-existing tracker-toggle contract is preserved unchanged (`collectiblesDisplay`/`lincolnMemorabiliaDisplay` ids, `toggleCollectible`/`toggleLincolnItem`/`setLincolnDisposition` handlers, the `[n/total]` badge, the Lincoln disposition `<select>`), each curio button keeps the `tracker-row`/`tracker-toggle`/`tracker-toggle--active`/`--inactive` classes alongside its new `curio-obj` reskin class (the same dual-class pattern `button.spine`/`button.mag` already established), and the display surface is a bounded, phosphor-scrollbar, edge-faded scroll region (`.curio-caselist`, max-height 430px) — no infinite scroll. Zero campaign-state field change (Protocol 22/25 reskin only). Cache r93. Owner fix — BUS-09 KARMA ALIGNMENT de-duplicated for FO3: the board previously showed BOTH the swing-needle readout (tier lamps + needle) AND the nested KARMA CENTER appendix on a usesKarmaCenter game, presenting karma twice. Fixed by wrapping the needle-face + k-standing/k-val/tier-lamps trio in a new `#karmaNeedleReadout` container (CSS split out of `.kn-wrap` into a new `.kn-readout` class) that `renderKarmaCenter()` now hides exactly when `usesKarmaCenter` is true — the opposite of its existing `#karmaCenterBlock` toggle — so each game shows exactly one readout (NV = needle, FO3 = Karma Center). The `#stat_karma` slider was moved OUTSIDE `#karmaNeedleReadout` (still inside `.kn-wrap`, Protocol 22) so it is the one editable control for EVERY game regardless of which readout is showing — FO3 karma stays changeable, per owner requirement. Suite 187 gained 187.9a (static: renderKarmaCenter references karmaNeedleReadout with the inverse ternary), 187.9b (static: stat_karma sits structurally outside/after karmaNeedleReadout), and 187.9c (behavioral, real DOM stub: usesKarmaCenter=true shows only Karma Center / usesKarmaCenter=false shows only the needle). Cache r94. Owner batch — CURIO ARCHIVE shelf-desync + Lincoln disposition clipping fixes (Suite 191 extended to tests): (1) root-caused (Protocol 27) via live scroll measurement — the plank-shelf background lived on `.curio-caselist` (the scroll viewport), and CSS backgrounds default to `background-attachment: scroll`, which anchors a background to the element it's declared on and does NOT scroll with that element's own overflow content, so the shelf stayed fixed in place while the objects (the actual scrolled content) moved past it; moving the background to the scrolled content box would have fixed the general case, but live measurement showed the Lincoln sub-case's rows are NOT uniform height (a "found" relic's row grows to fit the disposition `<select>`, measured at 158px vs the 126px shelf cycle for other rows), so a single shared repeating-gradient would still drift out of alignment there — the fix instead gives every object its OWN shelf plank via a `button.curio-obj::after` pseudo-element attached to the button itself, immune to both scroll position and row-height variance by construction (proven live: scrolling the case moves the button and its plank by an identical delta, always); `.curio-cell`'s gap widened from 4px to 12px so the plank (which protrudes 11px below the button) can never overlap the Lincoln disposition `<select>` that follows it. (2) The disposition `<select>` was a fixed `width: 88px` (matching the object button above it), clipping the longer labels ("HANNIBAL (FREE SLAVES)", "LEROY WALKER (SLAVERS)") mid-glyph; changed to `width: auto` (sizes to its own content) with `min-width: 88px` and `max-width: 260px` (headroom verified live against the real rendered need, 240px, for every disposition option) — zero clipping at 360/412/desktop, verified live in both games. CSS/layout only, zero handler/state change. Cache r95. Owner follow-up (3rd report) — RAD-drag transition-lag fix (Suite 190 extended, +tests, 17 total): both RAD bars (the VITAL TELEMETRY trace `#opRadLineWrap` and the SKELETAL HARNESS bar `#radDragTrack`) visibly chased a fast real drag even after the touch-action/transition-duration fixes above, because their fills' `width 0.3s ease` transition — needed so a programmatic (AI-driven or resting) rads change still animates smoothly — stayed active DURING an interactive drag too, so every mousemove/touchmove retriggered a fresh tween toward the newest target instead of snapping to it, unlike HP/XP which nothing in the codebase ever suppresses a transition for either (confirmed via live computed-style inspection: all four fills carry the identical `width 0.3s` transition, so the fix isn't a duration mismatch). `_wireRadDragSurface()` now toggles a `'dragging'` class on the container (added on mousedown/touchstart, removed on mouseup/touchend) mirroring the tempo dial's `.knob2.dragging` precedent (Protocol 22); `#opRadLineWrap.dragging i` and `#radDragTrack.dragging .bar-fill.rad` both set `transition: none` so the fill snaps exactly to the pointer every frame during an active drag, while the 0.3s transition still applies to any width change made outside one. Verified live (computed-style before/during/after a simulated drag) that the transition reads `none` only while `.dragging` is present and reverts to `width 0.3s` immediately after release, on both surfaces, with the underlying drag mechanics (state.rads/#stat_rads/width all tracking) unchanged. Cache r96. Owner follow-up — CURIO ARCHIVE Lincoln disposition `<select>` too spacious after the clipping fix: the round-1 fix (`width: auto`) let the control render using the browser/OS's own native form-control chrome (padding, minimum touch height, dropdown-arrow gutter) instead of this rule's own compact box model — that native chrome varies a lot across browsers/devices and can render considerably bigger than specified (measured live: even Chromium alone rendered ~8px wider than an `appearance: none` version at identical content), which is what spread the acquired Lincoln rows apart. `.curio-linc-disposition` now sets `appearance: none` (+ `-webkit-appearance`/`-moz-appearance`) to strip that native chrome entirely, so the box is driven purely by its own tight `padding: 1px 16px 1px 4px` and a from-scratch CSS-drawn dropdown arrow (two small gradient triangles, no extra DOM element) replacing the one `appearance: none` removes; `min-width` dropped 88px → 64px (though the widest-option constraint every browser applies to a `<select>`'s closed box means it renders at the same ~234px width regardless of which disposition is selected — verified live, "UNDECIDED" is not narrower than "HANNIBAL (FREE SLAVES)"). `font-size` stays exactly at the Protocol 17 floor (16px — prevents iOS/Android focus auto-zoom) and `min-height` stays exactly at the Protocol 17 tap-target floor (28px) — neither mobile-baseline rule is loosened to chase compactness; the fix is entirely about removing native-chrome variance, not about violating either rule. Verified live: the box now renders an identical, exact 234×28px footprint for every disposition value, with zero clipping (scrollWidth === clientWidth) for all four options and zero horizontal overflow at 360/412/desktop. CSS/layout only, zero handler/state change. Cache r97. Design Overhaul CHASSIS unit — the self-diagnostic maintenance bay + THE LIVING CORE (Suite 192: tests; Protocol UI-10 adopted a second time). The former single SYSTEM STATUS panel splits into three real `.panel.bay-board` boards — BUS-22 UNIT POWER PLANT (`overseerLogDisplay`/`renderOverseerLog` reskinned as industrial hour meters via the BUS-21 `_odoTile()` digit-wheel helper, reused verbatim rather than a parallel drum-tile implementation), BUS-23 IDENTITY PLATE & BREAKERS (`systemStatusDisplay`/`renderSystemStatus` reskinned as a stamped serial plate + a breaker-lever rack — CARRIER plus every `_SYSTEM_STATUS_FLAGS` entry, read-outs only, users never throw a lever), and BUS-24 SERVICE & FAULT CONSOLE (`btnViewChangelog`→`_svcViewChangelog` and `btnSystemStatusErrorLog`→`showErrorLog` kept verbatim, now dressed as a revision-log spool + an amber fault annunciator reading the client error ring-buffer via a new shared `_readErrorLog()`). The marquee piece is the LIVING CORE (`#chassisCore`, a real button — Protocol UI-5) plus a mini mirror (`#chassisCoreMini`) in the Overseer Uplink header — both share the `.chassis-core-shape` class so ONE choke point, `_coreRefresh()`, paints both from a single snapshot (never two separate ids, so the views can never drift). All 13 owner-picked behaviors wire to REAL signals, reusing the DO-O Overseer's own hooks rather than re-instrumenting `transmitMessage()`/`appendToChat()` (Protocol 22): idle heartbeat/standby dim/shutdown collapse/boot ignition derive from `AmbientRuntime.getState()` via the `runtime.state` bus event; AI-revs and connection reuse `setOverseerState()`'s own choke point (one added line calls `_coreRefresh()`); fault strain reads the same `_readErrorLog()` the fault lamp and BUS-24 console already share; radio-reactive hooks `_updateRadioUI()` (the one function both `startRadio()`/`stopRadio()` already call); save/sync write-pulse is a NEW `'data.write'` bus event emitted from `saveToSlot()` (ui-saves.js) and `saveCurrentToCloud()`/`loadCloudSave()`/`overwriteCloudSave()` (cloud.js, 4 emit sites total); colour-follows-optic is free (the core's CSS reads `var(--robco-green)`, the same var `changeOpticsColor()` already sets); overclock strain counts simultaneously-active signals; tap-to-poke reuses `playChipClick()` (Protocol 7, no forked audio). Gate-stacking (Protocol UI-10) is ONE `_coreShouldAnimate()` function — reduced-motion OR the Immersion dial below Balanced OR `document.hidden` OR the runtime in STANDBY/SHUTDOWN/OFF — that pauses every CONTINUOUS `animation:` (ring spin/heart pulse/fault ring/radio shimmer) as a group via a single `.core-still` CSS rule (`animation-play-state:paused`), while the state classes themselves (colour/opacity) apply instantly regardless, so a state CHANGE always paints one correct frame; one-shot flourishes (level-up flare, the data-write pulse, tap-to-poke) use `transition:` instead of a paused `animation:`, so they still settle correctly even while `.core-still` is present, via the add-then-reflow-then-remove pattern (Suite 135/162 precedent). A "?" `.icon-btn-round` opens `showCoreHelpModal()` (the Suite 103 `showSaveHelpModal()` pattern) with plain-language descriptions of all 13 behaviors. Zero campaign-state write anywhere in the block — every signal is transient/in-memory or MetaStore device telemetry, never `state.*`/`saveState()`/`robco_v8`. Cache r98. Owner follow-up — the mini core mirror moved out of the Director Uplink header (`.ovs-head`, where it read poorly squeezed next to the Overseer waveform) into its own dedicated readout window, `#chassisScreenMini`, built into the always-visible casing-top header (the `ROBCO INDUSTRIES`/`UNIFIED OPERATING SYSTEM` brand plate + PWR/UPLINK/FAULT lamps + backplane vents + uptime line) — pushed to the right of that header via `margin-left: auto` on the same flex row as the brand plate/lamp row/vents, with `.uptime-line`'s own `flex-basis: 100%` still forcing the uptime line onto its own row below regardless of the new sibling. `#chassisCoreMini` keeps the exact same `.chassis-core-shape` class (bumped from 20px to 26px, since it finally has real room) and is still painted by the SAME single `_coreRefresh()` choke point as the full BUS-22 core — no second query, no drift, proven live by toggling `setOverseerState('thinking')`/`_recordError()` and reading identical class lists off both elements simultaneously. The screen frame (`#0d0a04` background + inset box-shadow, matching `.telemetry`'s own CRT-window language) shrinks from 44×34px to 36×28px under the existing `@media (max-width: 480px)` casing-shrink block (alongside the pre-existing `.brand-plate`/`.uptime-line` narrow-viewport rules) so the header never overflows at 360/412px. Suite 192 extended (192.22 reworked + 192.23 added, tests). A Protocol 42 fix landed in the same commit — the gate's populated-save render-check caught a real (if subtle) overflow at the new 26px mini size, where the ring/heart children's percentage-inset + border positioning could round to a couple of stray px past the shape's own box; `.chassis-core-shape` now clips it with `overflow: hidden` (safe — `.core-help-btn` is a sibling inside `.chassis-core-shell`, never a descendant), guarded by 192.24. A second Protocol 42 fix landed in the same commit — the gate's axe scan caught a new `aria-prohibited-attr` violation on `#chassisScreenMini` (a plain `<div>`'s implicit "generic" ARIA role does not support `aria-label`, added by mistake alongside the decorative frame); fixed by dropping `aria-label` in favor of `aria-hidden="true"` (matching `#chassisCoreMini`'s own decorative marker) while keeping `title` as a harmless mouse-only tooltip, guarded by 192.25. A third Protocol 42 fix landed in the same commit, found live during render-verify: `#chassisCoreMini` carries BOTH `.chassis-core-shape` (the shared 96px base) and `.chassis-core-mini` (its own size) — a bare `.chassis-core-mini { ... }` rule ties in specificity with `.chassis-core-shape { ... }`, so whichever sits LATER in the file silently wins regardless of intent, which twice defeated the mini-core's own sizing (the 26px base, then the 20px mobile shrink) depending on where each rule happened to sit; fixed by giving every mini-core sizing rule the compound `.chassis-core-shape.chassis-core-mini` selector (0,2,0), which always beats the shared base (0,1,0) regardless of source order, guarded by 192.26 (tests total). Cache r99. Owner follow-up — CHASSIS mini-core placement re-confirmed + 3D ring burst on stat changes (Suite 192 extended to tests): the casing-top mini-core placement (Option A, from the prior unit) was re-verified live on every subsystem page (not settings-only, since .casing-top carries no data-tab/panel gating), locked by 192.27 (mini core sits inside .casing-top with no tab/panel wrapper) and 192.28 (the Director Uplink header no longer carries a second mirror); new #14 behavior gives the core's rings a real 3D orbital tumble (rotateX/rotateY/rotateZ per-ring `@keyframes chassisCoreOrbitBurst1/2/3` under a `perspective` set on `.chassis-core-shape`, 320px full / 90px mini) triggered as a one-shot burst (`_coreStatBurst()`, reusing the existing gated `_coreOneShot()`, never a bespoke carve-out) on the existing `level.up` RobcoEvents emit AND a new `stat.change` event fired only on a genuine value change via `_emitStatChangeIfDiffers()` (a module-scope old-vs-new cache for the drag-style HP/XP/RAD bar setters and the skill VU meter, which fire on every drag tick and have no other 'before this call' value; `commitStat()`'s onchange path compares `state[k]` directly instead, since it already has one) — the burst is caught by the SAME generic `.core-still .c-ring` gate every other ring animation already obeys, so no second reduced-motion carve-out was needed; both the full BUS-22 core and the casing-top mini core were verified live to run the burst keyframes on a real level-up and a real stat edit; zero campaign-state write (192.32 extends the 192.15 guard). Cache r100. Owner follow-up — prominence pass (Suite 192 extended to tests): the casing-top screen/mini-core now scale across three responsive size tiers (a desktop base, a 401-480px tier, and a <=400px tier for the narrowest phones), each strictly bigger than the last and every tier noticeably bigger than before the pass, verified live at 360/412/desktop with zero horizontal overflow (the casing-top row is flex-wrap, so a bigger screen simply wraps onto its own line rather than ever forcing overflow); the #14 3D ring burst on a stat change was the owner's main target — each ring now completes a full 720deg double rotation (up from a single 360deg swing) over a longer, matching 1.4s CSS/JS duration (up from 900ms), the shell's perspective was tightened (320px/90px down to 200px/55px full/mini) so the depth reads as an obvious tumble rather than a near-flat wobble, and the burst now also flares the heart in step with the ring tumble (the same box-shadow/transform mechanism every other one-shot flourish already used, Protocol 22) so the whole event reads as one unmistakable flourish; every other continuous/one-shot animation (idle pulse, AI-thinking glow, fault-strain ring, radio shimmer, overclock strain, level-up/data-write/tap flourishes) also had its brightness/amplitude bumped for genuine visibility without becoming seizure-inducing; everything still gate-stacks through the SAME _coreShouldAnimate()/.core-still mechanism with no bespoke carve-out, the mini core still mirrors the full core from the SAME _coreRefresh() source, and zero campaign-state write anywhere in the pass. Cache r101. Owner polish batch — tap-highlight kill + CURIO ARCHIVE Lincoln side-by-side grid + OPERATIONAL TEMPO live drag-readout (Suite 193: tests; Suite 180 extended to tests) — three owner-reported fixes in one push: the browser's own default tap/press highlight box (which flashed on the real `<button>` underneath every dressed control, worst on the CHASSIS living core where it washed out its own tap-to-poke pulse) is suppressed via `-webkit-tap-highlight-color: transparent` on a broad selector (`html` plus `button`/`a`/`summary`/`select`/`[role=button]`/`[role=tab]` directly, with `button.chassis-core-shape` getting its own belt-and-suspenders declaration) while `:focus-visible` keyboard rings (Protocol 17) are fully untouched; the CURIO ARCHIVE's Lincoln relics, which stacked one-per-row instead of side by side like the snow globes/bobbleheads because the disposition `<select>` sized itself to its widest OPTION content (width:auto, ~234px — the very fix Suite 191 had shipped, now identified as the Protocol 27 root cause of THIS bug), now render in the same multi-column layout via a fixed compact `.curio-caselist--linc .curio-cell` width (128px, two per row at 360px, `min-width:0` to escape the flexbox min-width:auto trap) with the select sized to its cell (`width:100%`, ellipsis for anything too long) instead of its own content; and the OPERATIONAL TEMPO dial's readout, which previously only refreshed on release, now live-previews the nearest position's name and description on every drag frame via `_tempoPointerMove()` (still committing only on `pointerup` via the unchanged `_setTempo()`/`onPlaythroughTypeChange()`). Cache r102. Owner follow-up — fluidity pass on the #14 3D ring burst (Suite 192 extended to tests): the owner reported the stat-change ring tumble read as choppy/jerky rather than a smooth continuous 3D orbit; root cause (Protocol 27) traced to the previous sparse, unevenly-spaced keyframes (0/40/70/100%, each segment covering a wildly different rotation delta) combined with an overshoot `back`-style `cubic-bezier(0.34, 1.56, 0.64, 1)`, which independently bounced past its own endpoint at every segment boundary — three chained snaps rather than one fluid motion. Fixed by making every burst keyframe evenly spaced (0/25/50/75/100%) with each axis value held EXACTLY proportional to its keyframe percent (constant angular velocity per axis, mathematically gap-free interpolation), swapping the overshoot bezier for a smooth non-overshooting `ease-in-out` applied once across the whole 1.4s span (so the tumble accelerates from rest and decelerates back to rest a single time, with axes staying perfectly in sync so it still reads as one coherent tumble), and adding `will-change: transform` to the base `.c-ring` rule (the ring is always animating — either this burst or the continuous idle `chassisCoreSpin` — so hinting the compositor is the correct use, not the will-change anti-pattern of applying it to a rarely-changing element) so neither loop stutters off the GPU; every keyframe still animates `transform` only, never a layout- or paint-triggering property. Applies identically to both the full BUS-22 core and the casing-top mini core from the same keyframes. Still gate-stacked through the unchanged `_coreShouldAnimate()`/`.core-still` mechanism (verified live: forcing the gate open and emitting `stat.change` resolves `animationTimingFunction: 'ease-in-out'` on both cores, replacing the old overshoot bezier), zero campaign-state write. Cache r103. Owner follow-up — always-on perpendicular 3D ring + a slower burst (Suite 192 extended to tests): the owner asked for perpendicular rings that rotate around in 3D always (not just during the stat-change burst), so a 4th ring was added purely as a `.chassis-core-shape::before` pseudo-element — no HTML markup change anywhere, meaning both the full BUS-22 core and the casing-top mini core pick it up automatically from the SAME shared class (Protocol 22, one source, no drift) — tilted 90deg on the X axis (a plane genuinely perpendicular to the flat r1/r2/r3 rings, which only ever rotateZ) and continuously spun on Y forever via `chassisCoreOrbitPerp` (linear infinite, matching the existing r1/r3 idle-spin convention, never ease-in-out, which would hitch at an infinite loop's seam), reading as a real gyroscope/atom-orbit 3D orbit at all times; `transform-style: preserve-3d` was added to the shared `.chassis-core-shape` base rule so this new ring composes in the same 3D space as the burst rings, and `.core-still::before` extends the SAME Protocol UI-10 gate (reduced-motion/low-immersion/hidden-tab/standby) to this new continuous loop — no bespoke carve-out, verified live by forcing the gate closed (paused) then open (running) and reading `animationPlayState` on both cores. Separately, the #14 stat-change burst was slowed per owner request — 1.4s/1400 -> 1.8s/1800 (CSS `animation-duration` and the matching JS `_coreOneShot()` removal timeout move together) for a more graceful tumble, same 720deg double rotation + heart flare as before. Zero campaign-state write anywhere in this pass. Cache r104. Owner correction — the always-on perpendicular ring's rotation axis (Suite 192.36 updated): the first attempt (rotateX(90deg) + a continuous rotateY spin) was a flat ring spinning around the vertical axis, not what the owner asked for. The owner clarified the actual ask: a ring that tumbles around a DIAGONAL 3D axis, so its two diagonal corner-pairs swap through depth as it turns (upper-left/bottom-right <-> bottom-left/upper-right), the ring's plane visibly tilting edge-on and back into the screen. Fixed by swapping the `chassisCoreOrbitPerp` keyframes to `rotate3d(1, 1, 0, 0deg)` -> `rotate3d(1, 1, 0, 360deg)` — a genuine diagonal-axis tumble, still transform-only + linear infinite + will-change:transform, still riding the same shared `.chassis-core-shape::before` rule (both cores, one source), still stilled by the unchanged `.core-still::before` gate — verified live by forcing the gate closed then open and reading `animationPlayState` on both cores, and by reading the parsed `chassisCoreOrbitPerp` keyframe rule directly from the live stylesheet to confirm the exact rotate3d values. Zero campaign-state write. Cache r105. Owner follow-up — perpendicular ring not visible in the mini core + second screen size bump (Suite 192 extended to tests): the owner reported the diagonal 3D ring "can't even be seen" in the casing-top mini core. Root-caused (Protocol 27) to the mini core's perspective (55px) being disproportionately TIGHTER relative to its own size than the full core's (200px on 96px ~= 2.1x ratio vs the mini's 55px on 44px ~= 1.25x), causing far more aggressive foreshortening for the mini ring on top of it already sharing a tiny circle with three other rings + the heart. Fixed with three changes: (1) loosened the mini perspective 55px -> 130px, no longer disproportionately tighter than the full core's own ratio; (2) gave the mini copy of the ring a bolder, higher-contrast border via the compound `.chassis-core-shape.chassis-core-mini::before` selector (2px/0.8 opacity vs the full core's 1px/0.55); (3) folded in the owner's separately-requested second size bump for the casing-top screen + mini core across all three responsive tiers (desktop 72x54/44px -> 88x66/54px, the 401-480px tier 56x44/32px -> 68x54/40px, the <=400px tier 46x36/27px -> 56x44/33px) — more absolute pixels makes the extra ring easier to distinguish from the others. Verified live: the mini core's perspective/border-width/border-color all resolve to the corrected values, the gate correctly pauses/resumes the ring's animation on both cores identically, the enlarged screen still fits the casing-top header with zero overflow at 360/412/desktop, and the parsed `chassisCoreOrbitPerp` keyframe rule is unchanged (still the diagonal rotate3d tumble). Test 192.33 updated to the new bigger floor thresholds; new Suite 192.37 locks the perspective/border fix. Zero campaign-state write. Cache r106. Owner follow-up — three refinements from a side-by-side screenshot review (Suite 192 extended to tests): (1) the full BUS-22 core had one more ring (r2, a static non-spinning ring at inset 14%) than the mini core ever carried in its own markup, reading as cluttered and visually mismatched — r2 removed entirely from the full core's HTML (index.html), never added to the mini (it never had it), so both cores now render the identical r1/r3/perpendicular-ring set from the shared `.chassis-core-shape` ruleset; the radio-shimmer behavior (#10), which previously targeted r2 exclusively, was retargeted onto r3 as a SECOND, comma-separated `animation` alongside its existing spin (so r3 keeps spinning while it shimmers, rather than the shimmer replacing the spin), and r2's own burst tumble (`chassisCoreOrbitBurst2`) was removed as dead code alongside it — r1 and r3 still tumble independently during the #14 burst. (2) Sync between the two cores was confirmed structural rather than added: both `#chassisCore` and `#chassisCoreMini` already shared every class-based rule (`chassisCoreSpin` durations, the `::before` diagonal tumble) with zero ID-scoped per-core override anywhere, and `_coreShells()`/`_coreRefresh()`/`_coreOneShot()` already update every `.chassis-core-shape` element in one synchronous pass — verified live by reading `animationName`/`animationDuration` off both cores' r1 rings simultaneously (identical: `chassisCoreSpin`/14s) and off both cores' perpendicular rings' `animationPlayState` after forcing the gate open (both `running`). (3) The always-on diagonal ring was widened per the owner's request to make it read more clearly as 3D from the side — a thin stroke collapses to a near-invisible hairline exactly at the edge-on point of its diagonal tumble, which is the moment that's supposed to read as depth; widened both the ring's own size (`inset` 8% -> 6%, a bigger ellipse) and its border stroke (1.5px -> 3px full core, the mini's already-bolder copy 2px -> 4px) so the edge-on silhouette keeps visible width instead of vanishing. New Suite 192.38 locks the widened ring; new Suite 192.39 locks the identical-ring-set-on-both-cores + no-ID-scoped-override guarantee. Zero campaign-state write anywhere in this pass. Cache r107. CHASSIS LIVING CORE — 10 owner-approved new behaviors, batch 2 (Suite 194: tests) — thermal glow (a real activity-derived "temperature", `_coreThermalTick()`, tinting `.c-heart` from green toward amber/red via `filter:hue-rotate()` so it rides whatever optic colour is active rather than a hardcoded rgb), energy sparks (two `.c-spark` dots orbiting via the classic rotate()+translateX() technique, identical on both cores, a second brighter/faster spark revealed only while genuinely busy), spin inertia (`--core-spin-mul-r1`/`-r3` registered via `@property` as interpolatable numbers so a `transition:` eases the ring speed between states instead of snapping — progressive enhancement, unsupported browsers just keep the old instant snap — regression-safe: every per-state multiplier reproduces the exact pre-existing target duration), power-surge ripple (a dedicated `.c-ripple` child, reusing the existing `data.write`/`level.up` signals plus a reconnect edge computed inside `_coreRefresh()`), idle flares (a dial-gated, non-persisted AmbientRuntime observer mirroring the DO-O Overseer idle-blip timer exactly), reactor hum (a second synthesized WebAudio channel, `startReactorHum()`/`stopReactorHum()`, tuned to a perfect fifth — 90Hz — above the CRT hum's 60Hz fundamental so the two tones blend harmonically rather than beat/clash; its own Protocol 7 channel — `robco_reactorhum_muted` MetaStore pref, `AudioSettings.reactorHum`, a 14th SLOT 02 chip CH-14, wired through the existing `toggleAudio()`/`toggleMasterMute()` choke points — louder while the CHASSIS subsystem is the active bezel view via `document.body.dataset.subsystem`), recovery animation (a one-shot stabilize flourish firing exactly on the fault-count >0→0 edge, safely reusing `::after` since `core-fault` is already removed in the same `_coreRefresh()` pass), uptime-milestone pulse (a celebratory pulse firing exactly once per crossed hour of session uptime, from the existing `_tickUptimeClock()`), and tap-and-hold overcharge (`_coreHoldStart()`/`_coreHoldEnd()` wired to `#chassisCore`'s pointerdown/up/cancel/leave, gated by `_coreShouldAnimate()`, reusing the #14 stat-burst tumble keyframes for the release flourish and `playBoardThunk()` for an optional synth kick). #10 (a center readout glyph) was evaluated and DROPPED — it could not be made legible on the small casing-top mini core (33-54px, already sharing its circle with 3 rings + the heart) without hurting the design. Every behavior is painted on both cores from the one shared `_coreRefresh()` snapshot, gate-stacked through the existing `_coreShouldAnimate()`/`.core-still` mechanism with no bespoke carve-out, and writes nothing durable to the campaign anywhere (transient in-memory or MetaStore device telemetry only). Cache r108. Owner audit — LIVING CORE ring visual-parity fix (Suite 195: tests; Suite 192.37/192.38 reworked): the full core and mini core still looked visually different despite the earlier "sync confirmed" claim, root-caused (Protocol 27) to a flat, per-tier-unadjusted mini `perspective` (130px regardless of the mini's own 54px/40px/33px responsive sizes) drifting the perspective-to-size ratio (as low as 3.94x on narrow phones) far from the full core's constant 200px/96px ~= 2.08x ratio — the actual reason the two never matched, not merely "too small to see." Every mini tier now gets its own `perspective` holding that SAME ~2.08x ratio (113px/54px, 83px/40px, 69px/33px), and the diagonal ring's `border-width` is likewise proportionally matched (9px full / 5px, 4px, 3px per mini tier — deliberately thinner in absolute px on the mini, not bolder as the earlier fix had it) at a consistent ~15-17% border-to-diameter ratio. Separately, the owner corrected an earlier misreading: "wider" meant THICKER/genuinely-3D (a torus/sphere-shell band with volume), never bigger in diameter — the previous `inset: 6%` (an 88%-diameter ellipse) was oversized, especially cramped inside the tiny mini screen. The ring's diameter shrank (`inset` raised to 20%, nesting cleanly between r1 at 100% and r3 at 44% diameter) while a new `box-shadow` trio (two inset shadows + an outer glow, never `border-image`, which fights `border-radius` on a circle across browsers) shades the now-thicker border like a lit, rounded 3D tube rather than a flat wide outline. The core "?" help button (`.core-help-btn`) was also nudged from -6px/-6px to -24px/-22px so the r1 ring's own edge (`inset: 0`, a 48px-radius circle) no longer visibly crosses through it — a first attempt at -11px/-10px was live-verified insufficient (nearest corner ~37.5px from center, still inside the 48px ring), so the final offset was computed precisely rather than eyeballed a second time. All three fixes are CSS-only, shared by both cores from the same `.chassis-core-shape`/`.chassis-core-mini` selectors with zero ID-scoped divergence (Suite 195.6), zero campaign-state write, and the existing `.core-still` gate-stacking mechanism untouched. A second issue surfaced during this same unit's OWN live render-verification (Protocol 42): the new per-tier perspective/border-width media-query overrides were initially cascade-dead, since an unconditional rule for each property already existed later in the file and — media queries add no specificity — always won regardless of viewport (the static test suite alone didn't catch this, since it only checked the CSS text existed, not how the cascade actually resolved); fixed with a `body ` ancestor-selector specificity bump on the four tier overrides, re-verified live at 360/412/desktop afterward, and Suite 195.1/195.2/192.37 now assert the "body "-prefixed form explicitly. Cache r109. FEEDBACK ANIMATION WAVE 1 (planning/2.8.0/plans/FEEDBACK_ANIMATION_BUILD_PLAN.md) — the STATUS ANNUNCIATOR + 4 new additive emits + the 8 Tier-S flagship home-panel animations (Suite 196: tests): a casing-top `#statusAnnunciator` `<button>` (role=status, aria-live=polite, tap-to-jump via `_echoJump()` → the existing `selectSubsystem()` router) surfaces a (b)-class event regardless of the active subsystem — `_echoPush()` fires IFF the event's home subsystem != `body.dataset.subsystem` (no redundant echo when the home animation is already on screen), collapses identical consecutive events into a `×N` count, and runs a FIFO queue capped at 6 (drop-oldest); `_echoShouldShow()` is deliberately narrower than `_coreShouldAnimate()` — visible at every Immersion tier and under reduced-motion (only the entrance/exit flourish quiets, automatically, via the existing global `prefers-reduced-motion` block), suppressed only when there is truly no viewer (`document.hidden` / runtime STANDBY-SHUTDOWN-OFF). Four new one-line additive `RobcoEvents.emit()` sites (U7/U8 precedent): `rad.tier` (the existing `_lastRadThreshold` crossing detector in `updateMath()`, reusing `_bioScanCompute`'s own NONE/MINOR/ADVANCED/SEVERE breakpoints), `limb.state` (`toggleLimb()` plus the AI limb-set path in `autoImportState()`, each gated on a genuine OK↔CRIPPLED change), `quest.status` (the native `cycleQuestStatus()` — the one native quest-status write path — plus the AI quest-status-diff in `autoImportState()`, same event both paths), and `location.visited` (the single choke point `recordLocationVisit()` in `state.js`, covering manual mark-visited/arrival/AI-import in one emit); `collectible.acquired` gains a matching AI-path emit (previously native-only). The 8 flagships: #1 FLATLINE WARNING (a one-shot HP-trace EKG stutter on the `hp.critical` crossing plus a continuous red glass-edge vignette for as long as HP stays critical), #4 GEIGER SPIKE / #5 RADAWAY DRAIN (RAD-trace chatter/drain + amber film grain on `rad.tier`), #6 X-RAY FLASH / #7 SPLINT WRAP (a bone-white zone-body inversion + per-zone fracture/wrap on `limb.state`), #9 VAULT-BOY LEVEL CARD — the flagship (a static, always-in-DOM card inside `.crt-mon`, Protocol 22's mini-core static-element precedent, toggled by a one-shot `show` class on `level.up` with a paired XP-bar sweep-shimmer), #14 REPUTATION STAMP, #23 CASE-CLOSED STAMP / #24 FILAMENT DIE, and #22 EXHIBIT LIGHT-UP (three pending-var deferred-consumption flourishes — `_pendingRepStamp`/`_pendingQuestStamp`/`_pendingExhibitLight`, each declared in `state.js` so every environment that loads `api.js` (including `tests/test.html`'s reduced boot chain) also has them defined — a Protocol 27 fix landed in this same commit after the harness caught a `ReferenceError` from the vars originally living in `ui-render.js` only — set at the emit site and consumed+cleared the next time `renderFactionRep()`/`renderQuests()`/`renderCollectibles()` paints, surviving the innerHTML-rebuild race), and #26 SURVEY PING (deferred per the build plan's §5: `_pendingSurveyPing`, state.js, is consumed ONLY when `renderWorldMap()` paints the WORLD GRID view — never the zoomed sector sheet — so the ping is always actually seen). Every animation is a plain reduced-motion-safe `@keyframes` (Protocol UI-9) painting a correct final frame when stilled; zero campaign-state write anywhere (transient DOM classes/module vars only); game-agnostic (Protocol 38 — glyphs/copy ride existing data, no game literals); verified live at 360/412/desktop with zero horizontal overflow. Cache r110. Owner-reported WAVE 1 bug-fix batch (Suite 196 extended to tests): (1) the #23/#24 quest stamp was `position:absolute` at the slot's right edge — the exact spot `.dir-keys` (CYCLE/DELETE) also settles to via `margin-left:auto` — so the stamp landed on top of the buttons; fixed by making `.dir-stamp` a normal-flow flex child pinned to its own line above everything else in the slot (`order:-1` + a full flex-basis) instead of absolutely positioned, so it can never re-overlap the controls at any width (196.28). (2) CRITICAL — clicking USE could dim the whole screen and lock out all interaction with no recovery; root-caused (Protocol 27, reproduced live by forcing a throw) to a PRE-EXISTING gap in `transmitMessage()` (predates Wave 1, untouched by the feedback-animation work): the screen-dim/pointer-events-none is applied well before the network `try`/`finally` that is supposed to undo it, so a throw anywhere in the unguarded setup zone in between (image/VATS-scan setup, `generateSyncPayload()`, `apiContents` building) left the dim stuck forever. Fixed by extracting the `finally` block's full reset logic into a shared `_resetTransmitUI()` helper and wrapping the setup zone in its own `try/catch` that calls the SAME helper on failure (Protocol 22 — one reset path, never duplicated) plus posts a clear error to chat instead of silently leaving the screen dimmed; a real Node `vm`-sandbox behavioral proof executes the actual `transmitMessage()`/`_resetTransmitUI()` bodies with a forced setup-phase throw and asserts `#uiPanel` is fully restored (196.29). Cache r111. Owner-reported DATABANK CARTOGRAPHY TABLE bug-fix batch (Suite 196 extended to tests): (1) the app-wide CRT glass overlay (`.crt-overlay`, a direct child of `.glass-frame` at z-index:9999) spans the FULL scrollable page height on a single-column layout, not just one screen's worth, so its periodic refresh-bar sweep passes over every panel's content — the cartography table's unusually dark/clean board background (`#02120c`) made that normally-subtle sweep read as "a bright line crossing the text," specifically hiding the `.kbd-hint`/`.survey-legend` caption. Root-caused via a live opaque-probe test (temporarily disabling `pointer-events:none` on `.crt-overlay` and reading `document.elementsFromPoint()`, confirmed the overlay painted above the legend before the fix and below it after) rather than fixed blind. Fixed by giving `.table-frame`/`.survey-legend`/`.kbd-hint` `position:relative; z-index:10000` — comfortably above the overlay in the SAME stacking context (verified no intermediate ancestor between `#worldMapPanel` and `.glass-frame` establishes its own isolating context) — without touching the global overlay itself, which stays exactly as it is everywhere else (196.30). (2) `zoomMapToZone()`/`resetMapZoom()`/`markLocationVisited()` each replace `#worldMapDisplay`'s entire innerHTML, destroying whatever DOM node currently holds focus (the tapped node, or the "< SURVEY CHART" back button) — a well-known cross-browser trigger for an unwanted scroll jump. Fixed by routing all three through a new shared `_rerenderMapPreservingScroll()` helper that captures the scroll offset via the SAME `_scrollElFor()` lookup `switchTab()`'s per-subsystem scroll memory already uses (Protocol 22, registered as a new eslint.config.mjs cross-file global), re-renders, then restores it, so the cartography table visibly stays exactly where it was (196.31). (3) the route segment leading to a freshly-surveyed node rendered already fully drawn by the time the (correctly Wave-1-deferred) ping ring showed — the user never watched it draw. Fixed by moving the `pingZone` computation earlier (before `routeSegs` is built, was after) so the newly-added segment can be identified and given a one-shot `stroke-dashoffset` "draw itself" reveal (`--route-len`, a per-element unitless custom property used via `calc(...*1px)`) that settles into the exact same static "5 4" dasharray every other route line uses once fully drawn — deferred by the SAME `pingZone`/`_pendingSurveyPing` consumption gate as the ping ring itself, never a second mechanism (196.32/196.33). All three verified live at 360/412/desktop with zero horizontal overflow; the scroll-preserve fix is a deterministic capture+restore (correct by construction) even though the specific headless test harness didn't reproduce a dramatic jump on its own. Cache r112. FEEDBACK ANIMATION WAVE 2 (Suite 197: tests) — the 9 Tier-A animations plus the one new additive `item.added` emit (`addItem()`'s manual path, `doLoot()`'s LOOT-apply path, and a pre-merge-`Set`-diffed AI inventory-merge in `autoImportState()`, the collectible/quest AI-path precedent): #10 XP CHUNK FILL (a 2×-brightness fill flash + a floating "+N XP" ticker on a genuine XP increase), #11 SERVO RECALIBRATE (the changed S.P.E.C.I.A.L. channel's `.fd-cap` overshoots and seats back, its `.fd-letter` flashing once, paired with the existing `playChipClick()` SFX — no new audio channel), #12 INK STAMP (a shared `_playInkStamp()` helper stamps the skill-book/magazine button only on the unread→read/consumed transition, never the reverse), #17 CAPS ODOMETER SPIN (`#c_caps` digit-rolls on `stat.change(caps)` — a new call to the existing `_emitStatChangeIfDiffers()` helper inside `updateMath()`, since caps has no dedicated drag setter of its own — with a bottle-cap glyph additionally arcing in on `trade.bought`/`trade.sold`), #18 MANIFEST PUNCH (the type-keyed `[data-dcount]` drawer badge blips and the matching `.mrow` freight-tag row slides in when its drawer is open), #20 WELD SPARKS + TAG (a spark flicker on `#craftPanel`'s board LED plus a transient claim-tag DOM node on `craft.completed`, never persisted markup), #30 CLOCK SPIN-DOZE (the CRT scanline collapses and the MISSION CLOCK's flip-card drums spin on `sleep.completed`), #2 CRT FLINCH (a one-shot horizontal tear across `.crt-mon` when `stat.change(hp)` shows a genuine decrease, distinct from the continuous WAVE 1 FLATLINE vignette), and #31 HOLOTAPE COMMIT (a holotape glyph slides into `#savesPanel` on `data.write`, with cloud kinds adding a carrier ripple — home-only by owner decision, no annunciator echo, since the existing mini-core write-pulse already signals off-panel). XP/SERVO/CRT-FLINCH/CAPS/MANIFEST-PUNCH additionally get an annunciator echo push in `_wireFeedbackEchoSubscribers()`; INK STAMP/WELD SPARKS/CLOCK SPIN-DOZE/HOLOTAPE COMMIT are home-only and deliberately carry none. Every animation is a plain `@keyframes` rule (Protocol UI-9, auto-neutralized by the existing reduced-motion block); zero campaign-state write anywhere in the addition. Cache r113. FEEDBACK ANIMATION WAVE 3 (Suite 199: tests) — the final Tier-B/C animations + the last 5 new additive emits (`karma.tier`/`item.equipped`/`effect.applied`/`effect.expiring`/`weight.seized`), completing all 33 slate items (`#5 RADAWAY DRAIN`/`#7 SPLINT WRAP` having already shipped as free companions of `#4`/`#6` in WAVE 1): #3 STIM FLUSH, #8 BRIDGE CLANG, #13 CARD SEAT, #15 NEEDLE KICK, #16 SCALES TIP, #19 IN-SERVICE STAMP, #21 PART DROP, #25 DIRECTIVE FILED, #27 TRIANGULATE, #28 TUNGSTEN WARM-UP, #29 GUTTERING LAMP, #32 FAULT (polish), and #33 QTY DIGIT FLIP — six of these ([home + echo]) additionally push an annunciator echo via `_wireFeedbackEchoSubscribers()`; the remaining seven are home-only per the build plan's routing table. Zero campaign-state write anywhere in the addition (Protocol 22, transient DOM classes/pending module vars only — `_pendingQuestFiled`/`_pendingPerkSeat`/`_pendingEffectWarmup` join the existing `_pendingRepStamp`/`_pendingQuestStamp`/`_pendingExhibitLight`/`_pendingSurveyPing` siblings in `state.js`, Protocol 27). Cache r116. Step 2 (v2.8.0) Native USE + TERMINAL stat edits (Suite 200: tests — the CARGO MANIFEST's USE button is now fully deterministic: gated to `cat==='aid'`, `_computeAidUse()`/`_durationToTicks()` parse the active game's `getChemsTable()` Effect/Duration columns (game-agnostic, both games, zero hardcoded item names) into heal/rad/limb-heal/timed-BUFF/addiction-clear/poison-clear, applied through the shared A.2 native setters (`_nativeSetHp`/`_nativeSetRads`) and the extracted `_applyStatusEffect()` (Protocol 22, reused by `addStatusEffect()` too); `getChemsTable()` gained a `duration` field in both `db_nv.js`/`db_fo3.js` to drive the BUFF decision, and clause-splitting uses whitespace-bounded `" / "` rather than a bare `/` so a food's own `"+N HP/s for Ss"` internal slash survives intact; `nativeUseItem()` decrements qty by exactly 1 only when an effect was genuinely applied, and USE no longer calls `transmitMessage()` at all. Suite 201: tests — TERMINAL mode now edits every stat: a generic `"<stat> <N>"` SET grammar, both delta forms (`"+N <stat>"`/`"<stat> +N"`), and a `"level up"`/`"leveled up"` phrase, all resolved via `_resolveStatToken()` (static scalar/SPECIAL alias maps — universal Fallout mechanics, not per-game data — plus `getSkillKeys()` for the per-game skill set, so FNV `guns` vs FO3 `small_guns`/`big_guns` resolve correctly with zero hardcoded skill list) and applied through the SAME A.2 setters; `commitStat()` now delegates to a new `_nativeSetSpecial()` so the DOM onchange path, Native USE, and TERMINAL edits share one clamp; the UNRECOGNIZED hint, the `#chatInput` autocomplete (`_statTokenSuggestions()`), and a new `STAT EDITS — TERMINAL, OFFLINE` `COMMAND_REGISTRY` group all stay in lock-step. Zero AI on every new path — both parts are fully offline, deterministic, and player-authoritative (Protocol 24). Cache r117. Bezel fidelity pass — Protocol 8 Sonnet-stage implementation of the Opus-authored `planning/2.8.0/plans/BEZEL_FIDELITY_PLAN.md`, closing the fidelity gap between the shipped device chrome and the approved `nv-machine-mockup.html`/`campaign-configs.html` reference (Protocol UI-7/DO-N, Protocol 25 reskin — same layout, no relocation): `.machine` gains a subtle pinstripe texture + inset hairline at all breakpoints, with the full outer drop-shadow frame + rounded casing corners added desktop-only inside the existing `(min-width:1000px) and (hover:hover) and (pointer:fine)` gate (Suite 129); `.navkey` keycaps deepen from a 2px to a 3px raised relief (matching 2px `:active` travel), with a desktop-only size bump to 52px while the base rule keeps its Suite-158.11-guarded 44px floor; a new GOVERNOR dial (`.bezel-side.left`/`.bezel-dial`) and field-kit flavor line (`.bezel-side.right`) are absolutely-positioned, `pointer-events:none`, desktop-only decorative flourishes anchored to a newly `position:relative` `.bezel`; a new serial plate (`.serial`, top-right of the casing-top header) and the field-kit text are both game-agnostic via `[data-game]`-toggled static spans — the same mechanism `.chassis-flavor` already uses (Protocol 38, never a JS ctx branch) — with the mini-core's `margin-left:auto` swapped for a fixed `margin-left:14px` inside the desktop gate only (so the row's one auto-margin, now on `.serial`, isn't split between two siblings); `.glass-frame` gains a new `::before` sheen + recessed-vignette pseudo-element (the `::after` is already `.sweep`'s) delivering the mockup's glass depth with zero DOM restructure, plus a desktop-only radius/padding bump; and a low-opacity `.bezel::before` hazard strip is added desktop-only. Mobile (360/412px) keeps the disciplined edge exactly as before — dial/serial/field-kit/hazard-strip/`.nk-sub` all hidden — with zero new horizontal overflow. Suite 158 extended (158.21–158.27, +tests, 2686 total) locks every new selector + the game-agnostic toggle + the Suite-129 gate discipline. Cache r118. Mobile immersion pass (owner follow-up — the desktop bezel fidelity pass's mobile complement): mobile (≤999.98px) gets its own scaled-down casing depth inside the existing mobile media query — `.container.machine` gains rounded top corners (`10px 10px 0 0`) plus a soft outer shadow (`0 0 16px`, deliberately smaller than desktop's 60px glow since mobile body padding is only 10px vs desktop's 20px), and the fixed bottom keycap dock (`.bezel`) picks up the same brushed-steel pinstripe texture `.machine` already carries plus matching rounded top corners, so the dock reads as cut from the same physical casing as the header instead of a separate flat bar. The glass sheen (`.glass-frame::before`) and the pinstripe (`.machine`'s own background) already applied at every breakpoint unconditionally from the desktop pass, so no change was needed there. An optional compact serial/flavor micro-line was evaluated and deliberately skipped — the approved `nv-machine-412.png` mobile reference shows zero decorative text on mobile, so omitting it keeps the disciplined mobile edge faithful to the golden reference. Both new rules are frame-only (box-shadow/border-radius never affect layout width or `scrollWidth`), verified live at 360/412/desktop with zero horizontal overflow and the desktop gate completely untouched. Guarded by Suite 158.28–158.30 (the Node runner, +tests, 2689 total); Protocol 42 also caught and fixed a stale Suite 158 header-comment test count that had drifted since the bezel fidelity pass. Cache r119. AI→native survey Part C.1 — three more AI round-trips converted to deterministic, offline native commands (Suite 202: tests, 2701 total): `[GPS]`/`[MAP]` now route through `NATIVE_COMMAND_ROUTER` to `_nativeOpenMap()` (ui-core.js), which reuses the existing `expandPanelForCategory('map')` panel-nav path to jump straight to the native CARTOGRAPHY TABLE — the AI directive's modal-trigger list and `"type":"GPS"` schema example are retired accordingly; `nativeLevelUp()` (already a fully native, AI-free +1-per-press level control) now also computes the same `10 + floor(INT/2)` skill-point pool the Director used to auto-award, reports it via `appendToChat()`, and jumps the view to SKILL MATRIX via `expandPanelForCategory('skills')` — allocation itself stays player-driven through the existing skill VU setters / TERMINAL `"<skill> +N"` grammar, never auto-assigned; the AI directive's skill-point-award-on-level-up instruction is retired to a "defer to the native calculator" note (the BIO-SCAN/THREAT/VATS/LOOT retirement pattern). A brand-new native lookup, `[PERKS]`/`[PK]` → `renderEligiblePerks()` (ui-render.js), reads the active game's `FALLOUT_REGISTRY.perks` via a pure `_computeEligiblePerks()` core and lists every level-gated ("regular") perk at or under the current level that isn't already owned — condition-gated ("special", level:0) perks are never surfaced, and the perk is never auto-granted (read-only, mirrors CONSULT); the AI's generative build-goal-aware `[ROADMAP]` is untouched (still AI, a separate hybrid feature per the survey). All three are wired into `COMMAND_REGISTRY` under NATIVE TERMINALS — OFFLINE, NO AI (moved out of NAVIGATION & WORLD STATE for GPS/MAP) and guarded zero-fetch/zero-transmitMessage. Cache r122. Native TRAVEL HERE map control (Suite 203: tests — the DATABANK CARTOGRAPHY TABLE sector sheet gains a TRAVEL HERE <button> beside MARK SURVEYED, letting the player set the tapped location as CURRENT (moves the [CURRENT] marker + records it visited) directly, with no AI round-trip; travelToLocation() (js/ui-render.js) routes through the SAME shared onLocationChange(overrideLoc) setter the #stat_loc field itself already uses (Protocol 22 — no forked setter, no direct state.loc write), and — since onLocationChange() owns its own single renderWorldMap() call — wraps that call with a capture/restore of the #worldMapPanel scroll anchor (reusing the identical _scrollElFor('databank') lookup _rerenderMapPreservingScroll() uses, Protocol 22) so tapping the button from inside the zoomed sector sheet does not trigger the innerHTML-replaces-the-focused-node scroll jump (WU-F11/Suite 196/198). Suppressed only on the already-current row (isYou) — shown on both surveyed and unsurveyed rows, unlike MARK SURVEYED which also hides on already-visited rows, since fast-traveling to a known location is the point. A real <button class="mark"> (reusing the existing ≥28px tap-target CSS, no new class) with a literal aria-label ("Travel to <loc> — set as current location", Protocol UI-3/UI-5), apostrophe/XSS-safe via escapeHtml(loc), and game-agnostic (Protocol 38 — no game literal, operates on the loc string via the registry-driven zone list). Cache r123. Step 2 (v2.8.0) owner request — LOCATION CONFIRMATION CARD (Suite 204: tests — a top-right toast confirming the player's CURRENT location changed, reusing the single onLocationChange() choke point every location-change path already routes through: a guarded location.current RobcoEvents emit (fires only on a genuine, case-insensitive change, never a same-value re-set) consumed by one subscriber (_wireLocationCardSubscriber(), wired from window.onload) that shows #locationCard — a body-level, position:fixed top-right element (placed before .container.machine so the ambient-runtime filter/transform containing-block trade-off documented above the fixed bezel dock never applies to it) — with the escaped new location name, then auto-dismisses via plain @keyframes slide-in/out (Protocol UI-9, reduced-motion-safe by construction); retires the older inline "ARRIVED" annunciator push that lived in onLocationChange() (#27 TRIANGULATE's echo half, Suite 199.25 reworked accordingly) so the player sees ONE clean location confirmation instead of two competing toasts, while the map's own "you" reticle pulse (TRIANGULATE's home half) and the SURVEYED annunciator reaction (location.visited, new-discovery-only) are both untouched; zero campaign-state write anywhere in the addition (transient DOM/module timer only). Cache r124. Step 2 (v2.8.0) Visual Upload OCR Unit 3 — hybrid wiring + kill-switch, completing the feature (planning/2.8.0/plans/VISUAL_UPLOAD_OCR_PLAN.md §4): the composer `[+]`/`triggerImageUpload()` → `handleImageSelection()` attach flow now runs the on-device OCR pipeline (Units 1/2) as the PRIMARY path the instant a screenshot is picked — `routeVisualUpload(file)` (new, `js/ocr.js`) is the one entry point, called right after the existing `attachedImageData`/`attachedImageMimeType` stash (Protocol 22, unchanged). Two fail-open flags (`visualOcr`/`visualAiVision`, `js/cloud.js` `_featureFlags`, Protocol 32/33) gate the hybrid: `visualOcr` off, or an OCR load/recognize failure, hands off to `_tryAiVisionFallback()`, which reuses the EXISTING, untouched `transmitMessage('[VISUAL UPLOAD]')` inlineData branch (api.js) verbatim — never forked — gated on `_aiVisionAvailable()` (the `visualAiVision` flag AND the real `_isUplinkConnected()`, ui-core.js, Protocol 22, no second key/online check). An OCR failure also calls the real `window._recordFeatureFailure('visualOcr', …)` (cloud.js), auto-disabling OCR into the fallback after `FAIL_THRESHOLD` (3) failures for the rest of the session (Protocol 35 client analogue) — no reload. The preview modal's "NOTHING DETECTED" empty state (Unit 2, ui-render.js) gains a TRY AI VISION button alongside MANUAL ENTRY, setting a `_visualParseRoutingToAiVision` flag before `closeModal()` so the modal's `onClose` callback skips clearing the image stash on that one path (every other close reason — CONFIRM/CANCEL/MANUAL ENTRY/Escape — clears it via the new `_clearVisualUploadStash()`, the one release point for both globals). When neither path is available (both flags off, no key, or offline), `_visualUploadDeadEnd()` clears the stash and posts a plain-English "add items via CARGO MANIFEST" message — never a dead screen (Protocol 33). The AI directive's old "Visual Upload Override" imperative (api.js `_directiveCoreTracking()`) is reframed to "Visual Upload Fallback" framing (native OCR is primary; the AI only ever sees an image here via the fallback), with the STRICTLY FORBIDDEN no-clobber rule preserved verbatim (Protocol 14) — the Suite 131 golden-master SHA-256 hashes were regenerated for the new directive text across the full 11-point state matrix (the Node runner). New Suite 207 (tests, the Node runner) covers the flag registration, the routing order, the fail-open kill-switch read, the FAIL_THRESHOLD auto-disable (against the real cloud.js function), and a deferred end-to-end behavioral proof of `routeVisualUpload()` across OCR-success/OCR-failure-with-fallback/flag-off/fallback-unavailable — the same self-re-emitted-header pattern Suite 137.6 established was applied to this proof too (Protocol 42 — a real cross-suite async-ordering flake in the Node runner's own harness, not a product bug, caught while wiring this suite: two suites' deferred `_pendingAsync` proofs can resolve in either order, so each must announce its own header immediately before its own assert line rather than relying on whichever suite the file happens to re-emit last). No new campaign-state field (Protocol 4 not triggered) — every write still rides `state.inventory`/the existing native setters via Unit 2's `applyVisualParse()`, untouched by this unit's routing layer. This completes the Visual Upload → on-device OCR conversion: native OCR primary, Director vision fallback, fully hybrid and kill-switched. Cache r127. Ceremony Moments Wave 1 (M1–M5, Suite 208: tests) — five small ceremony/boot beats, none touching campaign state: M1 CAMPAIGN IGNITION replaces `wipeTerminal()`’s two bare chat lines with a short (~2.5s), skippable commissioning sequence (`_runCampaignIgnition()`, ui-core.js — a self-rescheduling `setTimeout` chain, never `setInterval`, since ui-core.js retired its standalone-timer setIntervals at Suite 148.6 in favor of the AmbientRuntime heartbeat) that dims the glass (`.ignition-dim`/`@keyframes ignitionDim`), types 4 commissioning lines, fires the existing CHASSIS `_coreFlare()` ignition flare (Protocol 22), and posts the Director’s own greeting as the campaign’s first line; any tap/keydown mid-sequence flushes every remaining line at once instead of blocking input. M2 DIRECTOR ON THE WIRE (`_maybeGreetOverseer()`) finally consumes the DO-K `identity.overseer.greeting` block authored for all three games back at the Director Uplink unit but never rendered until now — a session-scoped `_overseerGreeted` flag fires it at most once, the first time the UPLINK subsystem genuinely activates with a live carrier (`selectSubsystem()`’s uplink branch and the `#go=comm` PWA deep link both call it; the boot-time `initBezelSubsystem()` bezel-highlight restore deliberately never does, the Module Bay hatch-ceremony precedent), rendering via the same un-persisted `appendToChat(...,true)` convention the DO-O idle-blip already used. M3 FIRMWARE FLASH adds a registered MetaStore pref `robco_last_seen_version` (state.js) read/compared/set inside `runBootSequence()` right next to the existing `robco_booted_before` write (`_checkFirmwareFlash()`, ui-audio.js) — an absent prior value is treated as already-seen so no existing device fires the flash on the commit that ships the key, only a genuine version change on a device that has already recorded one splices a `FIRMWARE FLASH DETECTED` POST line (the same “just before the final line” convention WU-T3’s identity line already uses) plus a one-shot amber glint on the casing serial plate and a REV LOG button pulse (`_fireFirmwareFlashFlourish()`, fired only from the boot-completion callback). M4 LONG-ABSENCE RECALIBRATION adds an additive `lastFlushAt` field to the existing WU-F7 Overseer’s Log MetaStore blob (`_flushOverseerLog()` stamps it on every 30s/hidden/pagehide flush; `_readOverseerLog()`’s zeroes-safe parsing covers it on both the JSON.parse and catch paths) — `_checkLongAbsence()` reads it BEFORE the current session’s own `initOverseerLog()` write touches it, so a ≥3-day gap (`LONG_ABSENCE_DAYS`) splices a “UNIT IDLE N DAYS” POST line and sets a `_longAbsenceBoot` flag consumed only by an additive branch in `startCrtHum()` (a marginally slower hum ramp-in; every other boot keeps the exact pre-existing instant gain assignment, byte-identical). M5 formalizes **SEAT**, the third Protocol UI-9 motion verb (after SWEEP): a 220ms settle (`.seat`/`@keyframes seatSettle`, deliberately filter/box-shadow only, never `transform`, since two of its four adoption sites — the program-cartridge stack-peek offset and the Tool Deck’s own `deckUp` slide-in — already own `transform`/`animation` for their own purposes and a competing rule would silently replace rather than compose with them) triggered by a `_motionSeat(el)` helper mirroring `_bezelSweep()`’s remove/reflow/add restart exactly, adopted at the phosphor tube pick (`_seatOpticsTube()`), the Sonic Processor board reseat (`toggleMasterMute()`’s un-mute branch only, never eject), the program-cartridge tap (`_seatGameCartridge()`, fired independently of the subsequent reload-confirm dialog so it never touches the reload path), and the Tool Deck open (targeting the `.deck-grip` bar, not `#toolDeck` itself, for the same animation-collision reason); textured per game via a CSS-only `[data-game='FO3']` keyframe override reading `identity.motionTexture.seat`, never a JS branch (Protocol 38); documented as adopted in Protocol UI-9 above. Cache r128. Mobile Density Standard, Tier-1 (planning/2.8.0/plans/MOBILE_DENSITY_PLAN.md Section 2/3, owner-approved Tier-1 only; Suite 209: tests) — a small mobile-only spacing-token scale (`--d-board-pad-block`/`-btm`/`-inline`, `--d-board-gap`, `--d-section-gap`, `--d-subtitle-mb`) plus the 8 concrete F1-F8 fixes (shared board shell padding + inter-board gap across all ~30 boards, the `.bay-part-no` header-subtitle margin, FACTION STANDING keycaps + meter wrap, STATUS EFFECTS lamp tiles, PERK LOADOUT/SKILL BOOKS/SKILL MAGAZINES slot rows, SKILL MATRIX VU rows, DIRECTOR UPLINK transcript + composer padding, and inner readout/light rows), all scoped to `@media (max-width: 480px)` only — desktop stays byte-identical and every trimmed interactive tile stays at or above the 28px Protocol 17 tap-target floor (the `.facon-chan` keycap trims to exactly 34px). Tier-2 items (bezel padding, the custom SETTINGS hardware controls, the signature fader/karma instruments, default-collapsing boards) are explicitly owner-declined and untouched. The whole block is deliberately placed at the true end of `terminal.css` — every one of its selectors already has an earlier, unconditional base rule elsewhere in the file, and CSS resolves equal-specificity ties by source order, so an override placed before its own base rule would be silently beaten by it; living at the end guarantees the mobile density block always wins the cascade. Diagnostic Shell U1 — registry spine + two-signal gate (Suite 210: tests — planning/2.8.0/plans/DIAGNOSTIC_SHELL_PLAN.md, Protocol 8 Sonnet stage): the Developer Console (Suite 149) is re-founded on a data-driven `DIAGNOSTIC_SHELL_TOOLS` registry (`js/test-console.js`) rendered through one pipeline, `_renderShell()`, gated by two signals — `_shellVisible()` (a thin alias over the unchanged `_devConsoleUnlocked()`, existence only) and `_shellTier()` (`'staging'` ONLY on a positive `_isStagingEnv()` signal, else the restrictive `'prod'` tier, fail-safe on any throw); `_toolVisible(tool, tier)` is the one filter every registry entry passes through, and `_mountConsole()` strips every tier-filtered tool's anchor from a DETACHED clone of `<template id="testConsoleTemplate">` before it is ever appended to the document, so a `tier:'staging'` tool has no path to a production player — proved both structurally and behaviorally (a real leak-proof execution of `_toolVisible()` against the live registry). `_invoke()` auto-wraps every `destructive:true` tool's action in the existing `confirmAction()` helper (Protocol 22/34), verified via a Node `vm` sandbox across confirm/cancel/no-confirm-fn cases. All 9 pre-existing controls (RUNTIME STATE, FORCE TRANSITION, REBOOT, WAKE → ACTIVE, IMMERSION TIER, REGISTERED OBSERVERS, REPLAY HATCH, both OCR test boards) are migrated as registry entries pointing at their existing markup anchors (`data-dsh-anchor` attributes on the template's `.input-group` blocks) — zero behavior change, except REPLAY HATCH, the one pre-existing destructive-leaning control, which now correctly gains the confirm gate the registry tiers it for (`tier:'staging'`, `destructive:true`); collapsible category sections (TRIGGERS/INSPECT/ENVIRONMENT & UNLOCK populated in U1; STATE SETUP/RESETS/RESILIENCE & INFRA/FIXTURES reserved for later units) reuse the existing `_wireDynamicSubPanel()` persistence helper, and a live search filter narrows tools by label/sub-label. A Protocol 42 fix landed in the same commit, caught only by live-clicking the button in a real browser (Suite 210's own automated proofs used a synthetic tool object and never exercised the real registry entry): the `replay-hatch` registry entry was originally missing its `action` field entirely, so `_invoke()`'s `typeof tool.action !== 'function'` guard silently no-opped the button — no confirm dialog, no action, no thrown error — fixed by adding `action: _replayHatch` and locked by Suite 210.14 (a real behavioral proof against the actual registry, the Node runner). U3 (triggers catalog + Protocol 44), U4 (state/resets/fixtures/inspect), and U5 (infra + minigame unlock) remain future units. Diagnostic Shell U2 — mobile overlay + identity + icons (Suite 211: tests): U1's document-flow `<details class="panel" id="testConsolePanel">` (which shoved the machine down on mobile whenever the console mounted) is replaced by three body-level `position:fixed` elements — a floating toggle (`#dshFab`, the shared `DEV_MARKER` gear glyph), a scrim (`#dshScrim`), and the drawer itself (`#testConsolePanel`, now a `role="dialog" aria-modal="true"` `<div class="dsh-drawer">` sliding in from the right) — all mounted as siblings of `.container.machine`, the same body-level placement `#locationCard` already uses, so `#testConsoleMount` (unchanged location) contributes zero layout height whether the drawer is open or closed and the FAB/drawer stay pinned through every ambient-runtime `filter`/`transform` state; `_mountConsole()` appends the FAB and scrim alongside the drawer but still reaches the document via the exact `mount.appendChild(panel)` call after `_renderShell(panel)`, so the U1 filter-before-DOM-insertion ordering (Suite 210.7) is unchanged. `_openShell()`/`_closeShell()`/`_shellKeydown()` implement a self-contained Tab focus-trap + Escape-close mirroring the shape of the existing `#sysModal` trap without forking it (Protocol 23), proved via a Node `vm`-sandbox synthetic-DOM behavioral test that the FAB/scrim/close-button/Escape wiring actually opens and closes the drawer; the slide-in is a plain `@keyframes dsh-slide-in` animation (Protocol UI-9, auto-neutralized by the global reduced-motion block). `_renderShell()` now also injects a per-category icon into every section `<h3>` and a per-tool icon (the registry `icon` field) into every migrated control's `.optics-label`, both falling back to the one shared `DEV_MARKER` glyph (reserved for the U4 inline dev-reset buttons). Zero campaign-state write anywhere in the addition (Suite 211.11); a regression guard (211.13) locks that the U1 document-flow shape can never silently come back. Cache r131. Diagnostic Shell U3 — TRIGGERS catalog + Protocol 44 (Suite 212: tests) — ~45 new registry entries under `category:'triggers'`: fire any of the 33 feedback animations (a `fire-anim-<event>` tool per unique bus-event name, plus a `fire-pending-<name>` tool per pending-var animation — `_pendingSurveyPing`'s trigger resets `_mapActiveZone` to the WORLD GRID view first, planning §12), force each Living Core state (`setOverseerState`) plus flare/burst, force each boot flavor (`window.__robcoBootFlavor` + a real reboot), and replay ceremonies M1–M5 (`_runCampaignIgnition`/`_maybeGreetOverseer`/the M3 flourish called directly, never `_checkFirmwareFlash`, which would consume the real flag/a synthetic `_checkLongAbsence` override restored immediately after one real `runBootSequence()` call/`_motionSeat`) without ever clearing a real persisted view-once flag. None of these had pre-existing markup, so `_renderShell()` gained a synthesis path: an anchor-less tool becomes a real `<button>`, grouped by the optional `group` field into a labeled sub-heading + flex-wrap grid — "one more registry entry" is now literally true even for a brand-new trigger with zero `index.html` changes. **Protocol 44** (new, below) requires every future hard-to-trigger feature to register a trigger in the same commit, enforced by a gate suite that cross-references every `RobcoEvents.emit('<name>')` string literal (scanned live from `js/*.js`, excluding `test-console.js` itself) and the 3 known view-once MetaStore flags against the union of every tool's `triggers:[...]` array, with a curated allowlist (`runtime.state`) for deliberately-internal events and a negative-proof test confirming the guard actually catches a synthetic uncovered event. A Protocol 42 finding landed in the same commit (Protocol 8 Sonnet-stage plan review): 7 of the ~23 bus events (`level.up`/`collectible.acquired`/`craft.completed`/`craft.scrapped`/`trade.bought`/`trade.sold`/`sleep.completed`) have a REACTIVE `state.js` U8 auto-log subscriber that appends to the campaign event log on every fire, contradicting the plan's own "firing a bus animation is inherently non-destructive" premise for those seven specifically — corrected to `tier:'staging'` + `destructive:true` (auto-confirm-gated) rather than the plan's literal `'prod'`, confirmed by reading every one of the ~23 event's subscriber bodies (Protocol 27), not assumed. Cache r132. Diagnostic Shell mobile chrome fixes, owner report (Suite 213: tests) — five presentation fixes on top of U1-U3, mobile-only (the exact inverse of the `(min-width:1000px)...` Suite 129 desktop gate) except FIX 4, which is universal: FIX 1 turns `#testConsolePanel` from a right-side full-height drawer into a resizable partial-height BOTTOM SHEET (`--dsh-sheet-h`, default 50vh, `_setSheetVh()` the one choke point a pointer-drag handle `#dshDragHandle` — role=slider, ArrowUp/ArrowDown keyboard support — and an expand/collapse toggle `#dshExpandToggle` both go through) so the terminal stays visible AND usable above it; `#dshScrim` goes fully inert (`transparent`, `pointer-events:none`) on mobile only, so a "tap outside to dismiss" gesture no longer applies once outside IS the now-interactive terminal — desktop's scrim keeps dimming + click-to-close, byte-identical to U2. FIX 2 makes `#dshFab` draggable anywhere on screen (`_wireFabDrag()`, the Immersion-dial tap-vs-drag pointer pattern in two dimensions, gated on a new `_dshIsMobile()` helper so desktop's bubble stays click-only) with its dropped position persisted via a new registered MetaStore device pref (`robco_dsh_fab_pos`, Protocol 4/UI-6), re-clamped to the current viewport on every restore. FIX 3 drives the bubble's highlighted look on mobile off a dedicated `.dsh-fab--open` class (toggled by `_openShell()`/`_closeShell()`) instead of the touch-sticky-`:hover`-prone `[aria-expanded]` rule desktop still uses untouched. FIX 4 centers the gear glyph inside the bubble (`line-height:1` on the span, a pure font-metrics correction, every breakpoint). FIX 5 (`terminal.css` only) pins the fixed bottom bezel dock's mobile `.telemetry` strip to a deterministic 36px height + `-webkit-line-clamp:2`, so the dock can no longer grow/shrink with the live `▸ SUBSYSTEM` status text length across subsystems/campaign states. Two Protocol 42 regressions were caught and fixed during this unit's own live verification (213.7/213.8): a `left:0;right:0;width:100%` content-box/box-sizing conflict that silently overflowed the sheet 28px past the viewport edge at 360px (fixed with `box-sizing:border-box`), and an id-vs-class CSS specificity mismatch that kept `#dshDragHandle`/`#dshExpandToggle` at `display:none` on mobile regardless of source order (fixed by matching the hide/show rules to the same id-selector specificity). Cache r133. Diagnostic Shell U4a — collapsible groups + INSPECT build-out (Suite 214: tests) — every registry `group` (not just the top-level CATEGORY) is now its own collapsible `details.sub-panel` nested inside its category's, persisted through the SAME `_wireDynamicSubPanel()` helper the category itself uses (`_buildGroupDetails()`, `js/test-console.js`) — defaulting OPEN except the FIRE ANIMATION family (~28 buttons across its 3 tier-split groups), which defaults COLLAPSED via `_dshGroupDefaultOpen()` so the shell opens compact; Suite 212's own `.dsh-tool-subhead`/`curGroup` checks were updated to match (the retired flat label div is gone). INSPECT is rebuilt into a read-only "system diagnostics" readout grouped VITALS & CAMPAIGN SUMMARY / DEVICE & SYSTEM / CONNECTION / FLAGS and moved to the LAST category in `CATEGORY_ORDER` — every field is a labeled, human-readable line built from the CHASSIS SYSTEM STATUS board's own `_chassisIdRow()`/`_chassisBreaker()` row helpers (Protocol 22, reused, never a raw `JSON.stringify` dump anywhere, in staging or prod); the readable summary (7 tools, including the relocated `inspect-runtime-state`/`inspect-observers`) is `tier:'prod'` (safe on a minigame-unlocked production build), while the 2 genuinely dev-only internals (Service Worker registration detail, the raw feature-flag LKG cache) stay `tier:'staging'`, still rendered as labeled text; a COPY DIAGNOSTICS button copies the rendered readable `textContent` via `navigator.clipboard.writeText`, never a fresh state dump. INSPECT is fully read-only — nothing in it ever writes campaign or device state. Cache r134. Diagnostic Shell U4b — STATE SETUP + RESETS + FIXTURES + submenu visual hierarchy (Suite 215: tests — 80 new tier:'staging'+destructive:true registry entries: 63 STATE SETUP cheats across VITALS/PROGRESSION/SPECIAL/SKILLS/KARMA/ECONOMY/INVENTORY/FACTIONS/COLLECTIBLES/PERKS-TRAITS-BOOKS/QUESTS/MAP/CONDITIONS/COMPANIONS/TIME/MEGA PRESETS groups, 13 RESETS (VIEW-ONCE/DEV FLAGS + CAMPAIGN DATA), 1 FIXTURE (LOAD NV TEST CAMPAIGN), and 3 INLINE on-panel dev-reset buttons — bringing the registry to 141 tools; every cheat routes through an existing native setter (`_nativeSetHp`/`_nativeSetRads`/`_nativeSetSpecial`/`_nativeSetSkill`/`_nativeSetLevel`/`_nativeSetXp`/`_nativeSetKarma`/`_nativeSetCaps`/`toggleLimb`/`adjustAffinity`/`adjustFaction`/`recordLocationVisit`/`onLocationChange`/`_applyStatusEffect`, Protocol 22/24) so a cheat can never push a stat past its real clamp; `_renderShell()` gained a parameterized-tool synthesis path (`control:'input'|'select'|'select-input'` — a select/input + GO button row) and `_invoke(tool, arg)` gained an optional arg passthrough forwarded unchanged into `tool.action(arg)`; `_mountInlineResets()` renders the 3 on-panel DEV-MARKER (⚙) reset buttons at STATIC panel anchors (`#worldMapPanel`/`#questLogPanel`/`#opsManifestPanel` `.bay-part-no`), re-checking `_shellTier()==='staging'` itself so they stay leak-proof by construction even on a future prod-minigame-unlocked build; the LOAD NV TEST CAMPAIGN fixture (`_dshLoadNvTestCampaign`) is guarded on the ACTIVE game context genuinely being FNV BEFORE its first `state.*` write — `FALLOUT_REGISTRY` is whichever per-game file the `GAME_FILES` boot manifest loaded at page load, not swappable at runtime, so populating the fixture during an FO3 session would silently write FO3 data under a mislabeled `gameContext:'FNV'` (a state-corruption risk caught during this unit's own design review, Protocol 27) — a no-op + chat hint fires instead; a CSS fix (`#dshSections details.sub-panel details.sub-panel`) gives a nested group sub-panel a left indent + dimmer heading so it visibly reads as nested under its category (the owner-reported OPTICAL SCAN TEST/RESILIENCE & INFRA same-level complaint); and two pre-existing Suite 149/210/tests were rescoped rather than weakened — 149.9/149.13's Hard-Boundary/game-agnostic checks now scope to the pre-U4b portion of the file (the U4b cheat apparatus is a documented, narrow exception proved correct by Suite 215 itself), and 212.2's tool-count check is scoped to `category:'triggers'` so U4b's 80 tools can never leak into U3's 45-tool count. Two Protocol 42 regressions landed in the same commit, both found only by this unit's own LIVE Playwright verification (never by the VM-sandbox proofs above, which stub `saveState()`/DOM as no-ops and so couldn't exercise either): (1) `_dshLoadNvTestCampaign()`/`_dshPresetFreshStart()` set `state.lvl`/`caps`/`hpMax`/`hpCur`/SPECIAL/`karma`/`rads`/`skills` directly, but `saveState()` calls `syncStateFromDom()` first, which reads those same fields BACK out of their `#stat_*`/`#s_*`/`#sk_*` DOM inputs — `loadUI()`'s own dirty-check does not reliably repaint a backdoor `state.*` write, so the fresh values were silently reverted; fixed with a new `_dshSyncDom(id, val)` helper that writes both `state` and the DOM input together (the same idiom every `_nativeSetXxx()` setter already uses), called for every DOM-synced field in both functions. (2) `_invoke()`'s auto-confirm-gate opens the shared `#sysModal` (via `confirmAction()`), which normally sits at z-index 2000 — BELOW the Diagnostic Shell's own `.dsh-scrim`/`.dsh-drawer`/`.dsh-fab` (20000-20002), a deliberate U1/U2 choice so the shell stays reachable over an already-open app modal (e.g. the first-visit patch-notes popup) — which silently made every one of U4b's 77 `destructive:true` tools' own confirm dialog render behind the shell's scrim and genuinely unclickable (confirmed via `document.elementFromPoint()` landing on `#dshScrim`, not the button); a blanket z-index swap was tried and reverted (it broke the original patch-notes-popup-over-FAB case); fixed instead with a scoped `body.dsh-modal-elevate` class that `_invoke()` toggles for exactly the duration of its own `confirmAction()` call, elevating `#sysModal` above the shell only then — every other `#sysModal` use is untouched. Suite 215 extended 14→tests (215.15/215.16) to lock both fixes. Step 2 (v2.8.0) Diagnostic Shell U5 — RESILIENCE/INFRA + the minigame unlock ceremony + the FINAL leak-proof audit, completing the Diagnostic Shell (Suite 216: tests): 18 new registry entries close out the plan's tool catalog. Eight `flag-<key>` tools (one per remote kill-switch flag — `cloudSync`/`googleSignIn`/`aiChat`/`keySync`/`saveMigration`/`offlineQueue`/`visualOcr`/`visualAiVision`) gain a genuinely new `_renderShell()` synthesis path, `control:'toggle'` — a live ON/DEFAULT/OFF-style readout plus three buttons (ON/OFF/DEFAULT) that route through a brand-new `window._setFeatureFlagOverride(key, valOrNull)` / `window._getFeatureFlagOverride(key)` seam in `cloud.js`: a staging-only, per-device LOCAL shadow layer (`_localFlagOverrides`, persisted via the new `robco_dsh_flag_overrides` MetaStore key) that `isFeatureEnabled()` now checks FIRST, ahead of the pre-existing auto-disable/remote-flag logic — so a feature's disabled/fallback path can be exercised on demand without touching the real Firebase `/config/flags` doc, and `valOrNull=null` cleanly reverts to remote/default. The setter re-checks `_isStagingEnv()` itself (defense-in-depth, Protocol 33 fail-safe direction) even though its only caller is already tier-gated. Two TRIP tools (`sim-fail-aichat-trip`/`sim-fail-visualocr-trip`, tier:'staging', destructive:true) drive the REAL `window._recordFeatureFailure()` to `FAIL_THRESHOLD` (3) in one click — the genuine auto-disable machinery, never a synthetic imitation — plus a non-destructive `sim-ai-response-preview` select tool posting a clearly dev-labeled chat line describing each of 401/403/429/TIMEOUT/MALFORMED's real fallback behavior. Three cache/SW controls land: `sw-force-update-prompt` reuses the REAL blocking update modal via a new `window._dshForceUpdatePrompt()` seam in `index.html` (Protocol 22 — never a duplicate modal implementation; it drives a genuine waiting worker when a real deploy is staged, or a synthetic no-op-`postMessage` worker otherwise, for pure UI testing), while `sw-clear-caches`/`sw-unregister` (both destructive:true) call `caches.keys()`/`caches.delete()` and `navigator.serviceWorker.getRegistrations()`/`.unregister()` respectively — cache revision and SW registration detail themselves stay covered by the pre-existing U4a INSPECT tools, deliberately not duplicated. One new RESETS entry, `reset-changelog-seen`, clears the `robco_version` seen-changelog flag (the one view-once flag U4b's RESETS hadn't already covered) so the first-visit patch-notes popup replays. The unit's marquee deliverable is the MINIGAME-UNLOCK SEAM itself: `_devConsoleUnlocked()` now ALSO returns true on a production build when the persisted `robco_dsh_minigame_unlocked` flag (new META_MANIFEST key, owner `test-console.js`) reads `'true'` — set only by a future in-game hacking minigame once built, or by two new staging-only ENVIRONMENT & UNLOCK test triggers (`minigame-unlock-test`/`minigame-lock-test`) that flip the exact same flag purely to rehearse the flow. Critically, `_shellTier()` is completely untouched by this seam — it stays staging-signal-only — so a minigame-unlocked production build shows the shell but renders ONLY `tier:'prod'` tools, proved both structurally (216.5, a sandbox with no `MetaStore` defined at all) and via the FINAL LEAK-PROOF AUDIT (216.6/216.18) — a from-scratch behavioral re-proof of the Suite 210.6/212.5 invariant against the COMPLETE, final 159-tool registry, not a sampled subset. A new, previously-unenforced plan guard (§8.6) also lands: 216.7 statically confirms every `tier:'prod'` tool's own inline `action()` source text contains no direct campaign-state write pattern (`saveState(`/`state.<field>`/`robco_v8`/`pushToCloud`) — every prod tool only ever delegates to a named helper. `_fireUnlockCeremony()` is the short in-fiction "RESTRICTED ACCESS GRANTED" flourish — opens the drawer, flashes the env banner via a new `.dsh-unlock-ceremony`/`dshUnlockGlow` plain `animation:` (Protocol UI-9, auto-neutralized by the existing global reduced-motion block), then settles back through the existing `_paintEnvBanner()` — writing nothing durable anywhere in its own body; a third, `tier:'prod'` `unlock-ceremony-replay` tool fires it standalone (no flag mutation), mirroring the M1-M5 ceremony-replay convention. Registry total: 159 tools (141 after U4b + 18 U5), and every prior suite with a hardcoded registry-total assertion (212.1, 214.8, 215.1) was updated in the same commit to the new true count — the established precedent for this doc's own self-consistency. This completes the Diagnostic Shell: all five planned units (U1 registry spine → U2 mobile overlay → U3 triggers catalog → U4a/U4b inspect + state/resets/fixtures → U5 infra + unlock) are shipped. Mobile UX polish batch (Suite 217: tests — owner report): `#btnSaveToCloud` (the existing additive addDoc cloud save) moves from a small, easy-to-miss blue-bordered row above the saves list into the same flex row as EXPORT SAVE at equal 11px/38px-tall weight — id/onclick/`renderAccount()` sign-in gating all untouched (Protocol 22); a full transient-vs-persistent pop-up inventory found no live full-width "banner" popup outside the already-shipped compact top-right toasts (`#locationCard`/`#statusAnnunciator`) — `.update-banner`/`.fo3-warning-banner` remain inert `<template>`-only markup, and `.rng-banner`/CARGO SEIZED's `#opsSeizedStamp`/`#opsSeizedNote` remain persistent, state-driven ternaries (never a timed auto-dismiss) — so nothing needed converting; the audit is locked as a regression guard (a banner-selector allowlist plus direct-ternary checks on both persistent indicators). NV_OVERHAUL_DESIGN_AUDIT must-fix batch (Suite 218: tests): MF-1's mobile subsystem-content gate (`.panel[data-tab]{display:none}`/`.tab-visible{display:block}`) was live-verified (local static build + the deployed Cloudflare staging site) to already correctly hide every inactive-subsystem panel at mobile widths — no override was actually winning, so the fix is an explicit, un-overridable `@media(max-width:999.98px) .panel[data-tab]:not(.tab-visible){display:none!important}` hardening of the existing invariant rather than a repair of a reproduced break; MF-3a retires the dead, unreachable-by-contract AI GPS modal branch (`mType === 'GPS'`) from `api.js` plus its orphaned `.modal-grid-map`/`.grid-row`/`.grid-cell` CSS (GPS/MAP is native-only via `NATIVE_COMMAND_ROUTER` → `_nativeOpenMap()`, matching the retired AI TRADE modal precedent); MF-3b replaces the two Protocol-38 `ctx === 'FNV' ? … : 'Fallout 3'` literals in `ui-core.js` (which mislabeled any non-FNV game, including the FO4 design-only stub, as "Fallout 3") with the game-agnostic `GAME_DEFS[ctx].label` lookup already used by `renderCartDeck()`. MF-2 (the legacy SETTINGS account/saves panels) was owner-deferred, left untouched. Cache r138. v2.8.0 Hotfix — CHASSIS SYSTEM STATUS breaker readout single-sourced (Suite 219: tests): the IDENTITY PLATE & BREAKERS board's feature-flag readout drove itself off a separately-hardcoded array (`_SYSTEM_STATUS_FLAGS`, `js/ui-core.js`) that was never updated when the Visual Upload OCR unit added `visualOcr`/`visualAiVision` to the real flag set (`js/cloud.js` `_featureFlags`), so the board silently showed 6 of the 8 real flags; fixed by having `js/cloud.js` export `window.getFeatureFlagKeys()` (`Object.keys(_featureFlags)`, Protocol 22 single source) and `renderSystemStatus()`'s new `_systemStatusFlagKeys()` helper read it first, falling back to a literal array (`_SYSTEM_STATUS_FLAGS_FALLBACK`) only when that accessor is unavailable (a reduced test harness); Suite 219 adds a DRIFT GUARD that fails if the fallback's key set ever again diverges from the real `_featureFlags` keys, plus a behavioral proof (vm sandbox, Node; structural mirror, PowerShell — no JS eval available there) that a live `window.getFeatureFlagKeys()` always wins over the fallback. A Protocol 42 fix landed in the same commit: shipping this hotfix's own net test-count change (2938→2945) exposed a latent Suite 28 gate defect — its "canonical test count" reader took only the FIRST `Tests: N/N` header in `CHANGELOG.md` (always the frozen `[Unreleased]` header, per the hotfix model), which happened to equal the current count on every prior push but diverged from the just-hotfixed `[v2.8.0]` header's real, higher count here; fixed (the Node runner) to take the MAXIMUM across every header, locked by 219.7. Cache r3 (APP_VERSION unchanged at 2.8.0, Protocol 2a hotfix model).
Suite 222 — FO3 PIP-BOY BUILD U0: locks the FO3 landscape Pip-Boy build's board-id + rails-data seam — GAME_DEFS.FO3.identity.rails groups 22 board ids into three subsystems worth of named sub-tabs (STATUS/SPECIAL/SKILLS/PERKS/GENERAL under OPERATOR, MANIFEST/CRAFT/BARTER/SQUAD/CURIO under OPERATIONS, MAP/QUESTS/NOTES/LOG/QUERY under DATABANK), verified against the real state.js object in a VM sandbox; asserts NV and FO4 identity carry no rails/orientation/statusStrip keys at all (Protocol 38 — the axis stays fully dormant because the data is simply absent); and derives the complete id→data-tab map straight from index.html to prove the no-rail allowlist (the weigh bridge plus the 3 CHASSIS and 4 SETTINGS boards) is exhaustive, so a future un-ided or un-allowlisted board cannot silently fall through the cracks of both lists. Also fixed a harness-only false positive in Suite 181.20 (Protocol 42): its state-field guard originally string-matched the WHOLE state.js file for board-id-shaped substrings, which collided the instant this unit legitimately started referencing those same ids as rails DATA; narrowed to check only the declared `state` object's field keys via extractStateKeys(), which is what the guard always actually meant to check. Added 222.4d (Protocol 36b escape-ratchet, r16): manifest.json's orientation must never lock to portrait while any game's identity.orientation is landscape-primary — the r15 incident shipped exactly that contradiction (design/plan/build/audit all verified in a browser tab, where the manifest orientation lock doesn't apply, so an installed PWA silently couldn't reach the landscape Pip-Boy at all); demonstrated failing against the reverted manifest and passing again against the fix before landing. Cache r13 (then r16 for 222.4d). Suite 223 — FO3 PIP-BOY BUILD U1: behaviorally proves the second nav axis mechanism (_applyRailGrouping/selectSubtab/_applyRails, js/ui/ui-core-nav.js) against a minimal fake DOM + MetaStore harness built around the REAL extracted function bodies — axis-inert-for-NV (a complete no-op, no throw, zero DOM/MetaStore/render writes, when identity.rails is absent), rails-drive-visibility (selectSubtab toggles .subtab-active onto exactly the right boards), subtab-persistence (robco_fo3_subtab_<subsystem> restores the last sub-tab on a simulated reload, with a safe fallback to the rail's first sub-tab when the persisted value is stale), and that switchTab() — not selectSubsystem(), correcting the build plan's literal call-site wording — calls _applyRails() after _syncBezelNav() so the boot-time restore path (initTabs() -> switchTab() directly) is covered too. Also locks that U1 ships zero new CSS (the #fo3SubtabRail container stays hidden via a plain HTML attribute) and that the sub-tab preference lives only in META_MANIFEST, never state. Cache r14. Suite 233 — SAVE_LAYER3 read-side fail-loud: locks the save-integrity Layer 3 unit — the inert readFaultBannerTemplate (mirrors 217.7b); capture-before-remove in _quarantineCorruptContainer with both hydrate catches routed through it and no bare removeItem left (deletion can never silently return); the split-try (migration helpers fail-soft OUTSIDE the parse try — 233.12 behaviorally proves in a VM sandbox that a throwing window._migrateEventLog on a VALID save keeps the container byte-identical, proven RED against the pre-change code which deleted it); never-overwrite-an-unresolved-quarantine (absence-gated setItem + stamped IDB overflow swept to newest 3 + in-memory fallback); the strict three-part eviction signature (no v8 AND no v7 AND window._bootMarkerRecovered set only inside _reconcileMetaFromIdb recovery loop) with 233.13 behaviorally proving fired-vs-silent; the _coldWriteObj {ok,idbOk,lsOk} contract + a repo-wide no-bare-truthiness-call scan (the one real regression trap); META_MANIFEST registration of robco_read_fault/robco_eviction_detected; the 6 SAVE INTEGRITY Diagnostic Shell tools with Protocol 44 tiering (banners prod/display-only, plant + degraded-seam staging/destructive); the once-per-session degraded-write notice latch + both wordings; the QUARANTINED RECORD saves-list row with EXPORT + confirmAction-gated PURGE (Protocol 34); 233.11 behaviorally proving corrupt bytes are preserved exactly; and the Layer-2 denied-noidb tail rider plus the L2 one-getItem cost bound. Browser-level proof (real banners at 412px, real IndexedDB eviction round-trip, real chat lines, once-per-session dedup) lives in the save-survival LAYER3 sections (21 checks). Cache r42. Suite 235 grew by 1 at the P8 unit: 235.15 asserts scripts/gate.js clears test-artifacts/ before step 1 so "files present ⇒ last run failed" is a true signal. Suite 239 — LIVE CONTAINER DURABILITY MIRROR / P8: locks the live-campaign IDB shadow that closes the last save-durability gap (the live robco_v8 container was the only cold-store datum without an IndexedDB mirror, so an Android localStorage eviction dropped everything since the last rolling backup). 10 static guards — mirrorLiveContainer is fire-and-forget + IDB-only writing 'campaign'/'live' from window.robco_v8; saveState() fires it AFTER the debounced localStorage write + dirty-check; restore is recovery-only (returns false without reading the mirror while localStorage holds robco_v8 — the "localStorage wins" anti-clobber guard, Protocol 34); restore reads getRaw and gates on container shape AND checksum; window.onload awaits _restoreLiveContainerFromIdb() BEFORE _hydrateStateFromStorage(); the boot phase is bounded (Promise.race vs _LIVE_RESTORE_BUDGET_MS, Protocol 33); _wireUnloadFlush fires the mirror on visibilitychange→hidden (the reliable mobile path); and a staging/destructive Diagnostic Shell trigger (save-simulate-eviction-recovery → _dshSimulateEviction, Protocol 44). 6 behavioral in a VM sandbox against the REAL mirror/restore + a faithful mock IdbStore — mirror write; RED→GREEN eviction recovery (localStorage cleared + IDB intact → campaign recovered into localStorage, not booted empty); deep round-trip equality; the mandatory anti-clobber (a newer localStorage container is never replaced by an older mirror); the shape gate (junk record refused); and the integrity gate (tampered checksum refused). Cache r51.
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).