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

BEZEL_FIDELITY_PLAN.md



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).