RELEASE
planning/2.8.0/plans/BEZEL_FIDELITY_PLAN.md
sha256 cc6f691b9deb81d5 · 30637 bytes ·
original held in the private archive
# BEZEL / DEVICE-CHROME FIDELITY PLAN
**Type:** Protocol UI-7 device-chrome / DO-N bezel **fidelity** pass — Protocol 25 **reskin** (same layout, no relocation).
**Stage:** Opus diagnose+plan (Protocol 8). A Sonnet session implements after; this doc is the spec-lock.
**Branch:** `dev` (HEAD `b8c6615`). Files: `css/terminal.css`, `index.html` (markup for a few decorative spans), test runners for guard updates, `sw.js` cache bump, docs.
---
## 0. The problem (owner, side-by-side screenshots)
The **shipped** device bezel/casing is much **flatter and less immersive** than the **approved mockup**. Bring the shipped casing up to the mockup's fidelity **without rearranging** — KEEP:
- the bottom-docked tab keycaps at the **bottom**,
- the screen / casing-top header at the **top**,
- the top-right mini-core where it is.
This is a **look** upgrade only. No behavior, wiring, DOM order, or layout relocation changes.
### Golden reference (the two mockups agree — identical casing chrome)
- `planning/mockups/nv-machine-mockup.html` — the canonical DO-N bezel machine (OPERATOR dressed). **Primary reference.**
- `planning/mockups/campaign-configs.html` — the **SETTINGS** variant of the *same* machine; byte-identical `#machine` / `.casing-top` / `.glass` / `.bezel` / `.serial` / `.tape` / `.bezel-side` / `.bezel-dial` / `.navkey` CSS. Confirms the reference is consistent.
- Screenshots verified: `nv-machine-desktop.png` (full framed device, serial plate top-right, GOVERNOR dial bottom-left, raised keycaps, field-kit line bottom-right), `nv-machine-412.png` (disciplined mobile edge — bordered brand plate, lamps, vents, thin glass frame, bottom keycap dock; **no** governor dial, **no** serial, **no** nk-sub).
### Shipped structure today (verified)
```
.container.machine#uosMachine
.casing-top
.brand-plate (ROBCO INDUSTRIES + .uos subtitle w/ .chassis-flavor--generic/--fnv)
.lamp-row (PWR / UPLINK / FAULT — real <button>s)
.vents
.status-annunciator#statusAnnunciator
.chassis-screen-mini#chassisScreenMini > .chassis-core-shape.chassis-core-mini (mini core) ← top-right, KEEP
.uptime-line
.bezel
.telemetry#bezelTelemetry
.nav-row
nav.nav-cluster[role=tablist] > 6× .navkey (operator/operations/databank/uplink[wirekey]/chassis/settings[setkey])
button.navkey.dirkey#navkey-directory
.glass-frame
.crt-overlay (scanlines + refresh bar, z-index 9999, absolute inset:0)
.carrier-strip#carrierStrip (mobile Overseer strip)
.main-grid (.col-left#uiPanel / .col-right)
```
Desktop shell: `@media (min-width:1000px) and (hover:hover) and (pointer:fine)` reorders via CSS `order` → casing-top(1) / glass-frame(2) / bezel(3), so the keycaps sit visually at the bottom while DOM/tab order is preserved. Mobile: `.bezel` is `position:fixed` at the true viewport bottom (`@media max-width:999.98px`).
**This structure is correct and stays.** The gaps are all in the CSS *finish* (depth, texture, keycap relief, glass framing) plus **two missing decorative elements** (governor dial, serial plate) that are absolutely-positioned/desktop-only and add zero layout risk.
---
## 1. Enumerated gaps — mockup HAS / shipped LACKS (verified in CSS)
| # | Gap | Mockup (source) | Shipped (source) | Verdict |
|---|-----|-----------------|------------------|---------|
| **1** | Framed rounded casing with depth/chrome | `#machine`: `max-width:1560px`; steel gradient **+ vertical pinstripe** `repeating-linear-gradient(90deg, rgba(255,255,255,.014) 0 2px, transparent 2px 5px)`; **`box-shadow: 0 0 60px rgba(0,0,0,.9), inset 0 0 0 1px rgba(255,255,255,.05)`**. `.casing-top` adds `box-shadow: 0 1px 0 rgba(255,255,255,.04)` + `text-shadow` (mockup L84–L109) | `.machine`: plain steel `linear-gradient` only, **no pinstripe, no box-shadow frame, no inset hairline** (css L513–520). `.casing-top`: border-bottom only, **no depth shadow/text-shadow** (css L522–531) | **REAL** — the "flat dark rectangle" complaint |
| **2** | Physical **raised** keycaps | `.navkey`: **`box-shadow: 0 3px 0 #0c0e0a, inset 0 1px 0 rgba(255,255,255,.09)`**; `min-height:52px`; width `clamp(58px,15vw,96px)`; `:active{translateY(2px)}` (mockup L480–501) | `.navkey`: **`box-shadow: 0 2px 0 #0c0e0a`** (1px shallower relief); `min-height:44px`; `flex:1 1 60px; max-width:110px`; `:active{translateY(1px)}` (css L770–801) | **REAL** — keycaps read flat/small |
| **3** | GOVERNOR dial (bottom-left of bezel) | `.bezel-side.left` + `.bezel-dial` (34px radial-gradient knob w/ tick) + "GOVERNOR" label, absolute `bottom:14px;left:18px`; desktop-only (mockup L517–543, markup L1322–1325) | **absent entirely** — no `.bezel-side`/`.bezel-dial` in css or index.html | **REAL** — missing element |
| **4** | Serial plate (top-right) | `.serial` "UNIT RBC-DT-4077 · SALVAGE REFURB — MOJAVE EXPRESS, GOODSPRINGS" + `.scratch` italic "property of courier six — hands off"; `margin-left:auto`; desktop-only (mockup L130–144, markup L1003–1006) | **absent entirely** — no `.serial` element in index.html (only in comments) | **REAL** — missing element; MUST be game-agnostic (Protocol 38) |
| **5** | Fuller glass framing / vignette | `.glass-frame` radius **22px** + `.glass` inner (radius 16px) w/ **inset vignette** `inset 0 0 42px rgba(0,0,0,.85)` + `.glass::before` **sheen** (radial top-left highlight + edge vignette). Two nested rounded layers = recessed screen (mockup L166–203) | `.glass-frame` radius **14px**, single layer, shallow `inset 0 2px 6px` + faint phosphor tint; **no sheen, no recessed inner vignette** (css L669–677). `.crt-overlay` supplies scanlines + a corner `inset 0 0 100px` only | **REAL** — screen reads flat, no glass depth |
### Additional gaps found during the CSS diff (beyond the owner's known list)
| # | Gap | Mockup | Shipped | Verdict |
|---|-----|--------|---------|---------|
| **6** | Bezel hazard strip | `.bezel::before` amber hazard stripe along the bezel top edge (`left:10%;right:10%;height:3px`, mockup L450–456) | none | REAL (minor, desktop flavor) |
| **7** | Field-kit line (bottom-right) | `.bezel-side.right` "MOJAVE FIELD KIT · PATCH 12.5" (mockup markup L1326–1328) | none | REAL — desktop-only; game-agnostic like #4 |
| **8** | Field-repair tape patch | `.tape` decorative tan tape on casing-top (mockup L146–155) | none | **OPTIONAL / SKIP by default** — heavy NV-diegetic flourish; recommend deferring unless owner wants it (adds clutter on mobile; the owner's 5 gaps don't include it). Documented for completeness. |
| **9** | Casing-top gradient depth on brand plate & vents parity | mockup vents 90×16, casing text-shadow | shipped vents 60×12 (fine) | Cosmetic; leave as-is (mini-core/annunciator occupy that row in shipped — do NOT enlarge vents). |
---
## 2. Per-gap fidelity design (desktop = full framed device; mobile = disciplined edge)
**Global rule (Suite 129):** every desktop-only embellishment goes **inside the existing** `@media (min-width: 1000px) and (hover: hover) and (pointer: fine)` block. **Never** add a bare `@media (min-width:1000px)` — Suite 129 forbids it. Mobile keeps the disciplined edge exactly as the mockup's `@media (max-width:999.98px)` does (hides governor dial / serial / field-kit / nk-sub, thinner glass frame).
### Gap 1 — framed casing depth + pinstripe
- **`.machine` (base, css ~L513):** add the pinstripe texture layer to the existing gradient and an inset hairline:
```css
.machine {
background:
repeating-linear-gradient(90deg, rgba(255,255,255,0.014) 0 2px, transparent 2px 5px),
linear-gradient(180deg, var(--bezel-steel-3), var(--bezel-steel-1) 35%, var(--bezel-steel-2));
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); /* hairline — subtle on all breakpoints */
}
```
- **Desktop only (gated block, ~L3051 `.container`):** add the outer drop shadow + keep `max-width:1600px` (already there). The `body{padding:20px}` already reserves room for the shadow:
```css
/* inside @media (min-width:1000px) and (hover:hover) and (pointer:fine) */
.container.machine {
box-shadow: 0 0 60px rgba(0,0,0,0.9), inset 0 0 0 1px rgba(255,255,255,0.05);
border-radius: 10px; /* the outer casing corners — desktop only */
}
```
- **`.casing-top` (base):** add the shipped-safe depth (do NOT copy mockup's text-shadow onto the mini-core row — it's fine, `.chassis-flavor` etc. inherit `--bezel-etch`): add `box-shadow: 0 1px 0 rgba(255,255,255,0.04);`. Keep existing border-bottom.
- **Mobile:** no outer rounded frame or 60px shadow (matches mockup: `#machine{min-height:100vh}` edge-fill). The pinstripe + hairline are lightweight and fine at all widths.
- **Map:** mockup `#machine` L84–95 → shipped `.machine` + gated `.container.machine`.
### Gap 2 — raised keycaps
- **`.navkey` base (css L770):** deepen the relief and strengthen the top-light gradient/highlight. **KEEP `min-height:44px` in the base rule** (Suite 158.11 asserts `.navkey{min-height:4[0-9]px}` — a 52px base would FAIL the regex) and **KEEP `.nav-cluster{flex-wrap:nowrap}`** (Suite 160.1). Change only the shadow/gradient:
```css
.navkey {
/* … unchanged: flex, min-height:44px, radius, colors … */
background: linear-gradient(180deg, #363b30, #1d201a 58%, #14170f); /* slightly brighter top for relief */
box-shadow:
0 3px 0 #0c0e0a, /* 2px → 3px raised depth (the flatness fix) */
0 4px 6px rgba(0,0,0,0.5), /* soft cast shadow under the cap */
inset 0 1px 0 rgba(255,255,255,0.10);
}
.navkey:active {
transform: translateY(2px); /* 1px → 2px travel to match the deeper relief */
box-shadow: 0 1px 0 #0c0e0a, inset 0 1px 0 rgba(255,255,255,0.06);
}
```
- **Desktop only (gated block):** bump the cap size to the mockup's fuller feel **without touching the base min-height** (so 158.11 still passes):
```css
/* inside the desktop gate */
.navkey { min-height: 52px; padding: 6px 4px 5px; }
.navkey.dirkey { min-width: 52px; }
```
- **Mobile:** base 44px keycaps with the deeper 3px relief — reads as physical without stealing height. `.nk-sub` already hidden on mobile? **No** — shipped keeps `.nk-sub` on mobile; the mockup hides it (`@media max-width:999.98px .nk-sub{display:none}`). **Recommend adding `.nk-sub{display:none}` to the shipped mobile block** so keycaps aren't cramped at 360px (matches mockup). Verify no overflow after.
- **Map:** mockup `.navkey` L480–501 → shipped `.navkey` L770–801 + gated bump.
- **Guard note:** Suite 158.11 (`min-height:4[0-9]px` on base) and 160.1 (`flex-wrap:nowrap`) — both stay satisfied by the design above. No test change needed for keycaps.
### Gap 3 — GOVERNOR dial (desktop-only, absolute, decorative)
- **Markup (`index.html`, inside `.bezel`, after `.nav-row`):** add
```html
<div class="bezel-side left" aria-hidden="true">
<span class="bezel-dial"></span><span>GOVERNOR</span>
</div>
```
- **CSS:** port `.bezel-side` + `.bezel-dial` from mockup L517–543. **`.bezel` MUST get `position: relative`** on the base rule so the absolute dial anchors to it (shipped `.bezel` currently has no position; mobile already sets `position:fixed`, which also anchors — fine). Add `pointer-events:none` to `.bezel-side` (decorative, never intercepts a keycap).
```css
.bezel { position: relative; } /* add to base (L720) */
.bezel-side { position:absolute; bottom:14px; display:flex; align-items:center; gap:10px;
color:var(--bezel-etch-dim); font-size:8px; letter-spacing:1.5px; pointer-events:none; }
.bezel-side.left { left:18px; } .bezel-side.right { right:18px; }
.bezel-dial { width:34px; height:34px; border-radius:50%; border:2px solid rgba(0,0,0,.7);
background:radial-gradient(circle at 35% 30%, #383c30, #15170f 75%);
box-shadow:0 2px 3px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.08); position:relative; }
.bezel-dial::after { content:''; position:absolute; left:50%; top:3px; width:2px; height:10px;
background:var(--bezel-etch); transform:translateX(-50%); }
/* HIDE on mobile (disciplined edge) */
@media (max-width:999.98px) { .bezel-side { display:none; } }
```
- **Desktop bezel side room:** inside the gated block add `.bezel { padding: 10px 24px 16px; }` so the centered nav-cluster clears the flanking dial/field-kit. At 1000px the centered cluster (≤~770px) leaves ample side room — no collision.
- **Ambient-transform caveat:** the dial is `position:absolute` relative to `.bezel` (a normal-flow flex child on desktop). The documented `body.rt-idle/.time-night/.rt-shutdown .container` transform caveat bites only **`position:fixed`** descendants — the absolute dial moves with its bezel and is unaffected. No new caveat.
### Gap 4 — SERIAL PLATE (top-right, desktop-only, **game-agnostic**)
**Approach: `[data-game]`-toggled static spans — the EXACT pattern the shipped `.chassis-flavor--generic/--fnv` subtitle already uses (index.html L255–256, css L546–555).** Protocol UI-7 explicitly prescribes "two static spans toggled by `[data-game]`, never a JS ctx branch." **No `state.js`/JS change** — cleaner and lower-risk than adding an `identity.serial` field.
- **Markup (`index.html`, in `.casing-top`, before `.chassis-screen-mini` / after `.vents`; `margin-left:auto` on `.serial` pushes it right — but the mini-core also uses `margin-left:auto`. Place `.serial` with its own right-alignment and let the mini-core follow; verify the flex row at 1000–1600px):**
```html
<div class="serial" aria-hidden="true">
<span class="serial-flavor serial-flavor--generic">UNIT RBC-DT-0000 · REFURBISHED FIELD UNIT</span
><span class="serial-flavor serial-flavor--fnv">UNIT RBC-DT-4077 · SALVAGE REFURB — MOJAVE EXPRESS, GOODSPRINGS</span>
<span class="scratch scratch--fnv">"property of courier six — hands off"</span>
</div>
```
- **CSS (port mockup L130–144 + mirror the `.chassis-flavor` toggling):**
```css
.serial { font-size:8.5px; letter-spacing:1px; text-align:right; line-height:1.5; opacity:.85; }
.serial .scratch { display:block; font-style:italic; color:rgba(230,215,175,.5);
transform:rotate(-0.8deg); letter-spacing:.5px; }
/* per-game toggle — same mechanism as .chassis-flavor (css L546-555) */
.serial-flavor--fnv, .scratch--fnv { display:none; }
[data-game='FNV'] .serial-flavor--generic { display:none; }
[data-game='FNV'] .serial-flavor--fnv { display:inline; }
[data-game='FNV'] .scratch--fnv { display:block; }
```
Generic (FO3/FO4/any future game) shows only the neutral `UNIT RBC-DT-0000 · REFURBISHED FIELD UNIT` and no courier-six scratch — never a misattributed NV borrow (Protocol 38 / UI-7). FO3/FO4 can author their own `--fo3`/`--fo4` spans later; the generic fallback holds until then.
- **Row-fit concern (must verify live):** the shipped `.casing-top` row already carries brand-plate + lamps + vents + annunciator + **mini-core (`margin-left:auto`)** + uptime-line (`flex-basis:100%`, wraps to its own line). Adding `.serial` (also wanting the right side) means two `margin-left:auto` siblings. **Resolution:** give `.serial` `margin-left:auto` and let the mini-core sit to its right with a normal `margin-left`, OR group serial+mini in the right cluster. Simplest: put `.serial` right after `.vents` with `margin-left:auto`, drop the mini-core's `margin-left:auto` to a small `margin-left:14px` **only inside the desktop gate** where the serial exists. **Verify at 1000/1280/1600px** that serial + mini-core + lamps never overflow or overlap; the row is `flex-wrap`, so worst case it wraps cleanly.
- **Mobile:** hide entirely — `@media (max-width:999.98px){ .serial{display:none} }` (matches mockup L932). Mini-core keeps its `margin-left:auto` on mobile (unchanged).
### Gap 5 — fuller glass framing / vignette (NO DOM restructure)
The mockup nests `.glass-frame > .glass`. **Do NOT wrap `.main-grid` in a new `.glass` element** (risky DOM surgery, Protocol 22 says extend). Instead deliver the recessed-glass depth with the existing `.glass-frame` + one new `::before` pseudo-element (the `::after` is taken by `.sweep`; `.crt-overlay` child stays for scanlines):
- **`.glass-frame` base (css L669):** deepen the frame — richer gradient + multilayer shadow; bump radius (desktop fuller):
```css
.glass-frame {
border-radius: 14px; /* mobile base (unchanged) */
padding: 6px;
background: linear-gradient(160deg, #131509, #05060a 42%, #0d0f0a);
box-shadow:
inset 0 2px 8px rgba(0,0,0,0.92),
inset 0 -1px 2px rgba(255,255,255,0.045),
0 1px 0 rgba(255,255,255,0.04);
}
```
- **New `.glass-frame::before`** — the glass **sheen + recessed inner vignette**, inset by the frame padding so it sits exactly over the screen (recessed look), `pointer-events:none`, z-index below `.crt-overlay` (9999) and above content:
```css
.glass-frame::before {
content:''; position:absolute; inset:6px; /* = frame padding → sits over the screen only */
border-radius:10px; z-index:8; pointer-events:none;
background:
radial-gradient(ellipse 130% 60% at 22% -12%, rgba(255,255,255,0.045), transparent 45%),
radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.55) 100%);
box-shadow: inset 0 0 46px rgba(0,0,0,0.85); /* recessed inner vignette */
}
```
Note `.glass-frame` is already `position:relative; overflow:hidden; z-index:1` (css L682–693). z-index 8 keeps the sheen above `.main-grid` (in `.container` z-index:10 context? — the sheen is inside `.glass-frame`'s own stacking context, so 8 sits above main-grid siblings within the frame and below the z-9999 `.crt-overlay`). **Verify text legibility is untouched** (sheen is translucent, over content, inert — Protocol UI-7 "never distort text").
- **Desktop only (gated block):** fuller framing to match `nv-machine-desktop.png`:
```css
.glass-frame { border-radius: 20px; margin: 0; padding: 7px; }
.glass-frame::before { inset:7px; border-radius:14px; }
```
(Desktop `.glass-frame` is `order:2; flex:1 1 auto` in the shell — margins would fight the flex column; keep margin 0 and let padding/radius carry the frame. The outer casing shadow from Gap 1 supplies the surrounding depth.)
- **Mobile:** thinner frame (radius 14px, the base) — matches mockup mobile (`.glass-frame{border-radius:14px}`). The `::before` sheen is lightweight and fine.
- **Interaction w/ `.crt-overlay`:** unchanged. `.crt-overlay` keeps scanlines + refresh bar + its own `inset 0 0 100px` corner darkening at z-9999. The new `::before` adds the *glass sheen highlight* the mockup has and a tighter recessed vignette. Two complementary layers, no conflict.
### Gap 6 — bezel hazard strip (desktop flavor, optional-but-cheap)
Port mockup `.bezel::before` L450–456. **Caution (Suite 160 "stray pin"):** a `repeating-linear-gradient` at 115° rarely divides evenly → a stray partial dash at the end. Use `background-repeat:round` won't apply to a gradient; instead keep the strip short/centered (`left:12%;right:12%`) and low-opacity so a partial dash is invisible, OR skip it. **Recommend: include it, low opacity 0.4, desktop-only** (it's behind the centered keycaps, visually minor). Requires the `.bezel{position:relative}` already added for Gap 3. Hide on mobile.
### Gap 7 — field-kit line (desktop-only, game-agnostic)
Same `[data-game]` toggle pattern as Gap 4:
```html
<div class="bezel-side right" aria-hidden="true">
<span class="fieldkit fieldkit--generic">FIELD KIT · PATCH 2.7.0</span
><span class="fieldkit fieldkit--fnv">MOJAVE FIELD KIT · PATCH 2.7.0</span>
</div>
```
CSS toggle mirrors `.serial-flavor`. Version string is static text — **note Protocol 2a**: if it hardcodes `APP_VERSION`, it must be synced on every bump. **Recommendation: drop the version number** (`FIELD KIT` / `MOJAVE FIELD KIT`) to avoid a new sync obligation, OR the implementer accepts the sync. Desktop-only; hidden on mobile with `.bezel-side`.
### Gap 8 — field-repair tape — **SKIP** (default)
Documented, not implemented unless the owner asks. Adds diegetic clutter, wants a mobile hide, and isn't in the owner's 5-gap list.
---
## 3. Plan-audit: every breakpoint × state enumerated (Protocol 8 / 26)
| Surface / state | Behavior after change | Verified how |
|---|---|---|
| **Desktop ≥1000px (hover+fine)** | Full framed device: outer casing shadow + rounded corners + pinstripe; recessed glass (radius 20px + sheen/vignette); 52px raised 3px keycaps; GOVERNOR dial bottom-left; serial plate top-right; field-kit bottom-right; hazard strip. | render at ≥1000px; `scrollWidth===innerWidth`; visual vs `nv-machine-desktop.png` |
| **Mobile 360 / 412px** | Disciplined edge: casing-top header (brand/lamps/vents/annunciator/mini-core/uptime), thin glass frame (radius 14px + sheen), fixed bottom keycap dock with 44px 3px-raised caps, `.nk-sub` hidden. **No** governor dial / serial / field-kit / hazard strip. | `tests/render-check.mjs` at 360/412 — **must** stay `scrollWidth===innerWidth`; visual vs `nv-machine-412.png` |
| **Tablet 768px** | Same as mobile branch (< 1000px) — disciplined edge, fixed bottom dock. | render at 768px, no overflow |
| **`data-game='FNV'`** | Serial = courier-six/Mojave text; field-kit = "MOJAVE FIELD KIT"; amber `--bezel-wire` accents already present. | toggle `data-game` |
| **Non-FNV (FO3/FO4/generic)** | Serial = neutral "REFURBISHED FIELD UNIT"; field-kit = "FIELD KIT"; no courier scratch. | set `data-game='FO3'` |
| **`body.rt-idle` / `.time-night` / `.rt-shutdown`** (container gets filter/transform) | Fixed **bezel** already documented to degrade to page-bottom (Suite 160 caveat — pre-existing, unchanged). New desktop dial/serial are `position:absolute` in normal-flow ancestors → **unaffected** (caveat bites only fixed descendants). | reason + spot-check idle state on desktop |
| **`.glass-frame.sweep`** (subsystem change) | `::after` sweep unchanged; new `::before` sheen is a separate layer, no conflict. | trigger a subsystem switch |
| **`prefers-reduced-motion`** | No new *animations* added (dial/serial/frame are static; hazard strip static). Existing global block still covers lamp-pulse/sweep (Suite 158.14). | reduced-motion emulation |
| **high-lumen / prefers-contrast** | Casing chrome is decorative; the high-lumen head-script + class untouched. Verify the sheen `::before` doesn't dim text in high-lumen (it's over the screen). | toggle high-lumen |
| **Reload / cache-clear** | Pure CSS + static markup; no JS/state. Nothing to migrate. | reload |
| **PWA standalone / offline / SW** | CSS chrome only, no shell change — installable/offline intact (Protocol UI-7). `sw.js` cache bump required (served files change). | PWA install check post-deploy |
---
## 4. Guard / test audit (what could break, what to update)
| Guard | Assertion | Impact | Action |
|---|---|---|---|
| **Suite 158.11** | `.navkey { … min-height: 4[0-9]px }` on the **base** rule | Design keeps base `min-height:44px`; desktop bump is in the gated block. | **No change** — stays green. Do NOT put 52px in the base rule. |
| **Suite 158.13** | `.nav-cluster { justify-content: center }` | unchanged | No change |
| **Suite 160.1** | `.nav-cluster { flex-wrap: nowrap }` | unchanged (do NOT copy mockup's `wrap`) | No change |
| **Suite 158.14** | global reduced-motion block covers bezel animations | no new animations added | No change |
| **Suite 61** | mobile overflow guards (`.main-grid minmax(0,1fr)`, `.panel overflow-wrap`, mobile `overflow-x:clip`) | untouched | No change; verify still passes |
| **Suite 129** | desktop shell gated on `(min-width:1000px) and (hover:hover) and (pointer:fine)`; no ungated `@media (min-width:1000px)` | **CRITICAL** — all desktop embellishments MUST live in the existing gated block | Enforce in implementation |
| **`tests/render-check.mjs`** | no horizontal overflow at 360/412 (+ populated-save inner-overflow) | new mobile CSS must not overflow; sheen `::before` is `inset`+clipped (no scrollWidth); dial/serial are desktop-only (not in mobile check) | Run after implementation; the definitive check (Protocol 10) |
| **Suite 90 / Protocol 39** | no UTF-8 mojibake in `index.html` | serial/field-kit text has `·` and `—` em-dashes | **Write markup via the Edit tool or Node `fs` — NEVER PowerShell** (double-encodes `—`→`â€"`) |
| **Protocol 1 (cache bump)** | `sw.js CACHE_NAME` when served files change | `css/terminal.css` + `index.html` both change | Bump `-rN` (currently r117 → r118) |
| **New static guard (recommended, Protocol 20 / 36b)** | Lock the new chrome so a refactor can't silently drop it | Add small assertions to Suite 158 (both runners, parity Protocol 15): (a) `.bezel { position: relative }` exists; (b) `.navkey { box-shadow: 0 3px 0` (raised relief present); (c) `.bezel-dial` rule exists; (d) `.serial-flavor--fnv` + `.serial-flavor--generic` `[data-game]` toggle present; (e) `.glass-frame::before` sheen rule exists. Plus index.html: `.bezel-side.left` + `.serial` markup present, both `aria-hidden`. | Add ~6–8 assertions to Suite 158 in **both** `robco-diagnostics.js` and `.ps1`, sync all counts per Protocol 2a. |
**No behavior/wiring guard is touched** — every keycap keeps its exact `selectSubsystem()`/`role=tab`/`aria-selected`/`#go=`/hotkey wiring (DO-N). This is a pure CSS + decorative-markup reskin.
---
## 5. Files touched
1. **`css/terminal.css`** — `.machine`, `.casing-top`, `.navkey`(+`:active`), `.glass-frame`(+`::before`), `.bezel`(+`position:relative`, `::before` hazard), new `.bezel-side`/`.bezel-dial`, `.serial`/`.scratch`/`.serial-flavor` toggles, `.fieldkit` toggles; desktop gated-block additions (`.container.machine` shadow, `.glass-frame` radius/padding, `.navkey` size, `.bezel` padding); mobile block additions (`.nk-sub{display:none}`, `.bezel-side`/`.serial`/`.fieldkit` hide).
2. **`index.html`** — add `.serial` (top-right, in `.casing-top`), `.bezel-side.left` (GOVERNOR) + `.bezel-side.right` (field-kit) inside `.bezel`. All `aria-hidden`, decorative. **Edit tool / Node fs only (Protocol 39).**
3. **`tests/robco-diagnostics.js` + `.ps1`** — new Suite 158 static assertions (parity, Protocol 15), count sync (Protocol 2a everywhere).
4. **`sw.js`** — `CACHE_NAME` bump `-r118`.
5. **Docs (Protocol 2):** `CHANGELOG.md` [Unreleased] entry (plain English), `ARCHITECTURE.md` (bezel/chrome note if needed), test-count sync across all Protocol 2a locations if suite count changes.
**No changes to:** `state.js`, any JS wiring, `js/ui-core.js` `selectSubsystem`, DOM order, layout structure, the mini-core, the annunciator, `.main-grid`, panels.
---
## 6. Acceptance criteria (Protocol 26 — verify on the real rendered artifact)
1. Desktop (≥1000px) renders a **framed rounded casing with visible depth** (outer shadow + pinstripe + inset hairline), matching `nv-machine-desktop.png` in feel; `scrollWidth===innerWidth`.
2. Keycaps read as **physically raised** (3px relief + cast shadow + top highlight), press down 2px on `:active`, at both 44px (mobile) and 52px (desktop).
3. **GOVERNOR dial** present bottom-left of the desktop bezel; hidden on mobile.
4. **Serial plate** present top-right on desktop, showing courier-six/Mojave text under `data-game='FNV'` and neutral text otherwise; hidden on mobile; **no** JS ctx branch.
5. **Glass** reads recessed with a sheen highlight + inner vignette; **text is not distorted or dimmed** (Protocol UI-7).
6. Mobile 360/412: `tests/render-check.mjs` green (no horizontal overflow, no focus-zoom); disciplined edge only (no dial/serial/field-kit); matches `nv-machine-412.png`.
7. Every keycap still routes via `selectSubsystem()` with hotkeys `[1]`–`[6]`/`[0]`, `role=tab`/`aria-selected`, `#go=` deep-links intact (DO-N).
8. PWA installable/offline/standalone unaffected; SW cache bumped.
9. Full gate green in **both** runners at parity; new Suite 158 guards pass; counts synced (Protocol 2a).
---
## 7. Verification steps (for the Sonnet implementer)
1. `npm run lint && npm run format`.
2. `git commit` → pre-commit fast gate (both runners at parity, 2679+ new-count).
3. **`node tests/render-check.mjs`** (Protocol 10 — the definitive pixel/overflow check) at 360 & 412 → assert no overflow, no focus-zoom.
4. Live render at **360, 412, and ≥1000px** (Protocol 10): confirm the five gaps closed, desktop framed, mobile disciplined edge, `document.documentElement.scrollWidth === window.innerWidth` at every width.
5. Toggle `data-game` FNV ↔ FO3 → serial/field-kit swap correctly, no NV borrow on FO3.
6. Spot-check `body.rt-idle` desktop → dial/serial stay put (absolute, not fixed).
7. Pre-push full gate (Playwright boot-smoke + render-check + a11y) green.
8. Protocol 11 deploy-verify post-push.
---
## 8. Risks & mitigations
- **Casing-top row overflow when `.serial` is added** (two right-aligned siblings w/ mini-core) → resolve the `margin-left:auto` conflict (serial gets auto, mini-core gets fixed left margin in the desktop gate); **verify live at 1000/1280/1600px**; the row is `flex-wrap` so worst case wraps, never overflows.
- **Suite 158.11 regex** (`min-height:4[0-9]px` on base) → keep base at 44px, desktop 52px in the gate. Do not violate.
- **Suite 129** → all desktop CSS in the existing hover+fine gate; never a bare `min-width:1000px`.
- **Protocol 39** → em-dash/`·` in serial text: Edit-tool/Node writes only.
- **Glass `::before` z-index** → must sit above `.main-grid` content but below `.crt-overlay` (9999) and be `pointer-events:none`; verify no input is blocked and text isn't dimmed.
- **Version string in field-kit** → recommend dropping the number to avoid a Protocol 2a sync obligation.
---
## 9. Explicit non-goals (do NOT do)
- Do NOT relocate the keycaps, header, or mini-core (owner constraint).
- Do NOT wrap `.main-grid` in a new `.glass` DOM element (use `.glass-frame::before`).
- Do NOT touch any keycap wiring / router / hotkeys / ARIA / `#go=`.
- Do NOT copy the mockup's `.nav-cluster{flex-wrap:wrap}` (Suite 160 wants nowrap) or its 52px base `.navkey` min-height (Suite 158.11).
- Do NOT add a JS `ctx ===` branch for serial/field-kit flavor (Protocol 38 — use `[data-game]` spans).
- Do NOT port the field-repair `.tape` unless the owner asks (Gap 8, skipped).
- Do NOT write non-ASCII via PowerShell (Protocol 39).
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).