RELEASE
planning/2.8.5/audits/AUDIT_dock_occlusion.md
sha256 188e4e94b0c0ac5e · 11813 bytes ·
original held in the private archive
# BOTTOM-DOCK OCCLUSION — PROTOCOL 8 STAGE 3 (INDEPENDENT AUDIT)
**Auditor:** Opus, stage 3 — independent audit. **No code changed. No commit. No push.** (One temporary red-then-green CSS edit was made to prove the guard bites, then reverted with `git checkout` — tree verified clean.)
**Commit audited:** `ebb1549` on `dev`. **Predecessor:** `planning/DOCK_OCCLUSION_PLAN.md` (stage 1).
**Method:** started from the diff and the pixels, not the implementer's report. Read every changed hunk (CSS, nav JS, both test files). Rendered NV + FO3 myself at 360/412/desktop/landscape with Playwright; hit-tested the actual controls at landing AND scrolled; ran render-integrity clean AND with the app-shell neutralized (red-then-green); triggered the live registry autocomplete; ran the 3272-test gate and the map render-check. Evidence screenshots in `planning/mockups/dock/`.
## VERDICT: **PASS.** Ship it.
---
## THE ONE-LINE ANSWER (for the phone)
**The bottom bar no longer covers anything, on both games, at both phone sizes — and I proved it three ways, not by trusting the report.** The screen content now lives in its own scrolling box that stops right above the bar, so the last rows (the S.P.E.C.I.A.L. attributes, the cargo drawers, the quest and search boxes) sit above the bar and scroll up cleanly instead of hiding behind it. Desktop and the sideways Fallout 3 screen are untouched. The safety-check that catches this bug is now a real guard: I temporarily undid the fix and it went red naming the exact hidden controls, then green again once restored. Autocomplete dropdowns show fully. The map "tap a place, tap back" keeps your scroll spot exactly. Nothing was quietly loosened to make the tests pass.
---
## 1 ⭐ THE 5 BOARDS CLEAR THE DOCK — 360 AND 412, BOTH GAMES — ✅ CONFIRMED
The fix makes `#fo3BoardScroll` a **bounded internal scroller that ends above the fixed 112px dock**, so board content is clipped-and-scrollable, never painted under the dock. Verified by rendering, not by reading:
- **render-integrity (the real gate check), full 12-config matrix, NO filter: 0 occlusion failures.** All 5 formerly-failing boards (NV STAT/S.P.E.C.I.A.L., NV+FO3 INV, DATA/DATABANK, CAMPG) at 360 and 412, populated + empty, pass.
- **Worst case — NV STAT @412 (the whole S.P.E.C.I.A.L. block):** at landing the scroller box is `y=246..793`, the dock top is `y=803` — the scroller ends **10px above the dock**. `#s_l` (the last LUCK input) sits at `y=840`, i.e. **clipped below the scroller fold, not painted under the dock** (`s_l_clippedByScroller: true`). Scroll it into view and it lands at `y=494`, **above the dock, and `elementFromPoint` at its centre returns `INPUT#s_l` itself** (`hitsSelf: true, inDock: false`). Screenshots: `planning/mockups/dock/nv-412-stat.png` (landing — S.P.E.C.I.A.L. above the dock) and `…-scrolled.png` (scrolled — bottom of the board, limbs/RAD/skills/karma, all bounded above the dock).
- **No horizontal overflow** at 360 or 412 (`scrollWidth === innerWidth` on every board; `overflow-x:clip` on the scroller prevents a stray x-scrollbar).
**Honest note on method:** a naïve hit-test that clamps a control's centre to the viewport reports the below-fold controls as "hitting the dock" — because it probes a pixel where the control is *clipped away* and finds the dock behind the empty region. That is a probe artifact, not a defect: those controls are genuinely inside the bounded scroller (reachable by scrolling), which is exactly why render-integrity's `visiblePortion()` (which intersects each element against its scroll-ancestor boxes) correctly reports them as not-on-screen rather than occluded. I confirmed the distinction directly (clipped-at-landing → visible-and-self-hitting-when-scrolled), so the PASS is on the correct behaviour, not on the check looking away.
## 2 ⭐ DESKTOP + FO3-LANDSCAPE UNCHANGED — NO STYLE LEAK — ✅ CONFIRMED
All new rules live inside `@media (max-width:999.98px)`. Computed-style probes prove they do not reach the wider layouts:
- **Desktop (1440×900), NV + FO3:** `.bezel` computes `position:relative` (NOT fixed), `#fo3BoardScroll` computes `overflow-y:visible` (NOT the mobile `auto`) — the two mobile-only rules are absent. The container *does* compute `display:flex` at desktop, but that is a **pre-existing** desktop rule (`css/25-toolbar.css:1404`, the `min-width:1000px` block that already made desktop a flex app-shell with `body{overflow:hidden}`), not a leak: the decisive tell is `height` — my new rule sets `height:100dvh` (=900px at this viewport), yet the container measures **860px** (`height:100%` from the desktop rule), so the mobile rule is provably gated out.
- **FO3 landscape (780×360):** container computes `position:fixed` + its own grid shell, `.casing-top` is `display:none`, `#fo3BoardScroll` keeps FO3's own `overflow-y:auto` — FO3-landscape's ID-specificity rules (`css/60-fo3-pipboy.css`) win over the new plain-selector base exactly as the plan intended; no divergent property leaks (the base mirrors UPLINK's property set). FO3-landscape's own reachability gesture checks still pass in the render-check.
- **No horizontal overflow introduced** at any width.
## 3 QUARANTINE REMOVAL HONEST + GUARD BITES (red-then-green) — ✅ CONFIRMED
- **Before:** `filterKnownPreexisting()` + the `RI_NO_ALLOWLIST` switch + `pageProbe()`'s `hitInBezelDock` capture masked exactly 14 flat-view dock occlusions. All three are **deleted** in the diff (verified by reading the hunks, not the report).
- **Now passes with NO filter** (§1).
- **The guard is not merely green — it bites.** I temporarily neutralized the app-shell CSS (`.container.machine → height:auto;display:block` + `#fo3BoardScroll → overflow-y:visible`) and re-ran render-integrity: it went **RED with 14 failures**, naming the exact bezel-dock overlaps — `INPUT#s_s`…`#s_l (hit DIV#bezelTelemetry)`, `#invFilterMods`/`#invDrawerSearch (hit BUTTON#navkey-*)`, `#newQuestObjective (hit DIV#bezelTelemetry)`, `#databankSearch`, `#newItemName` — at NV 360/412 and FO3 portrait 360, populated + empty. Restored → green. So any future dock overlap fails the gate normally at the exact viewports that failed before.
- The 12-config matrix (`buildMatrix()`) is intact — not reduced.
## 4 AUTOCOMPLETE / DRAWER NOT CLIPPED (plan §5.2) — ✅ CONFIRMED
Typed into the INV add-item name field on NV @360 and triggered the live registry autocomplete: the dropdown computes `position:fixed` and is a **direct `document.body` child** (`parentIsBody:true`), so the new `overflow-y` scroller cannot clip it. It renders fully at `y=474..575` — below the input, above the dock top (688), all on-screen. The INV filter drawer, WEAPONS/APPAREL/AID drawer chips, and the "REQUISITION SLIP" add-item row all render correctly. Screenshot: `planning/mockups/dock/nv-360-autocomplete.png` (Stimpak/Auto-Inject/Super Stimpak list fully visible). The implementer also correctly declined to add `overflow:hidden` to `.container` (plan §5.2), keeping the belt-and-suspenders invariant against an ambient filter/transform state.
## 5 MAP SCROLL FIX + SUITE 166 NOT WEAKENED — ✅ CONFIRMED
- **`_scrollElFor()`** now returns `#fo3BoardScroll` for the flat mobile shell (gated on the same `max-width:999.98px` query), so the one scroll-detection lookup the map's scroll-preservation and per-subsystem scroll memory both read follows the new bounded scroller. The render-check proves it: **"map node-back scroll preservation — panel stays at the same viewport position (delta 0px)."**
- **Suite 166 (5f/15/19) was corrected to the new truth and STRENGTHENED, not weakened** (read the assertion diffs): 166.5f now additionally requires the `#fo3BoardScroll` + `(max-width:999.98px)` branch (two more mandatory matches); 166.15 now also asserts `window.scrollY` is left **untouched** on the flat path; 166.19 now also asserts `#uiPanel` **and** `window` are left untouched (two extra negative assertions). These are added guarantees, not relaxed ones. The old assertions described behaviour the fix deliberately changed (window/`scrollY` was the old scroller), so keeping them would have been wrong.
---
## OTHER CHECKS
- **Test count:** `ALL 3272 TESTS PASSED` (ran the runner myself). Count synced in README/ARCHITECTURE/RULES/CLAUDE per the in-suite guards.
- **No assertion covertly weakened.** The one non-obvious test-logic change I scrutinized is render-integrity's `activeRoot` selector (now "first *displayed* tab-visible panel" instead of the old subtab-active-first chain). It feeds **only** assertion 2b (`onLandingVisible`), **not** the occlusion guard (assertion 1, which is logic-unchanged and proven to bite in §3). The rewrite is a justified correction for the bounded-scroller model (the old selector "passed" only because the unbounded scroller spanned the whole document) with a clear in-code rationale — it mildly reduces 2b's discrimination on the flat view (a top-of-scroller board always intersects), but 2b was always a coarse "is anything on screen" sanity check, and the real dock guard is untouched. Not a dodge.
- **224.13 repurposed correctly** — from "assert `.closest('.bezel')` exists" to "assert the quarantine machinery is gone," matching on **code signatures** (`function filterKnownPreexisting` / `process.env.RI_NO_ALLOWLIST` / `hitInBezelDock:`) rather than bare words, since the file's own header legitimately names the deleted code in prose. Precise, not a rubber stamp.
- **Protocols:** Cache r40→r41 (`sw.js`, served CSS/JS changed — Protocol 1 satisfied). APP_VERSION 2.8.0 (patch-level fix, no bump needed). Protocol 17 (no 360 overflow, tap targets/inputs untouched — layout-container change only), Protocol 25 (dock stays put, no relocation/tap-count change), Protocol 22 (reuses the UPLINK/FO3-landscape app-shell + the single `#fo3BoardScroll` scroller and the single `_scrollElFor` lookup — no parallel system). **Data files untouched** — the diff is CSS + nav JS + tests + `sw.js` + `CHANGELOG.md` only (verified via `git show --stat`). Changelog entry is plain-English and accurate (Protocol 21). Clean tree, no junk (Protocol 41).
- **Deploy:** `ebb1549` is the tip of local `dev` (matches `origin/dev` per the task framing). This is unreleased work on `dev` — correct per Protocol 43; it reaches prod only at the next `dev → main` release.
---
## PUNCH-LIST (all non-blocking — not a stage-2 bounce)
1. **10px reserve slack:** `--dock-h` is 122px vs the measured 112px dock (accounts for body's 10px bottom padding), leaving a ~10px band between the glass bottom and the dock top. Not visible as an ugly gap in the screenshots (the dock's telemetry strip + keycaps read as one unit below the glass), but if a future change tightens the casing, confirm it still reads clean.
2. **`activeRoot`/assertion-2b discrimination** (see above): 2b is now near-trivially true on the flat view. Harmless today (assertion 1 is the real guard), but if 2b is ever meant to catch a genuinely-mislaid landing board on the flat view, it would need a stronger target than "first displayed panel."
3. **Option A (dock reflowed into flow)** was deliberately not taken (Option A′ keeps the dock `position:fixed`), so the documented ambient-runtime fixed-dock degradation trade-off (`css/10-chrome.css` — dock drops to page-bottom under `rt-idle`/`time-night`/`rt-shutdown` transform) still exists on the flat view. Out of scope here; worth a future look if that ambient interaction is ever reported.
*End of Stage 3. No code changed, no commit, no push. The temporary red-then-green CSS edit was reverted (`git checkout`); working tree verified clean. Evidence screenshots in `planning/mockups/dock/` (gitignored).*
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).