# MOBILE-FIX-2 — Report Round 2 of the mobile pass on https://retire.krisforbes.ca, per `MOBILE-FIX-2.md`. All three issues fixed, deployed, and verified on the live site at 320/360/390px (iPhone SE, Pixel, iPhone 14) + 1280px desktop. Evidence: `audit/mobile-before2/` (pre-fix) and `audit/mobile-after2/` (post-fix) — geometry logs, verification transcripts, viewport and full-page screenshots (`full-*.png`). **Result: 90/90 live verification assertions PASS (`VERIFY2-OK`), tests 36/36 + SMOKE-OK, `DEPLOY-OK`, marker count 1, off-LAN (Midhurst) HTTP 200 + marker 1 (`OFFLAN-OK`).** --- ## Issue 1 — Primary CTA hidden behind the bottom sheet (FIXED) **Root cause.** `.scr` clearance was a fixed `185px` while the sheet is 218px tall, and the Back/Next rows (`position:static`) scrolled behind the sheet. The live sheet height also changes when the panel is expanded, so no constant works. **Fix (spec as written, + one necessary addition).** - `:root{--sheet-h:218px}`; `syncSheetH()` in `app.js` writes `document.documentElement.style.setProperty('--sheet-h', panel.offsetHeight+'px')` — shim-guarded (`var h = panel && panel.offsetHeight; if (h)`), called on init (final statement), on `.panel-h` toggle, on scrim toggle, on `resize` (`if (window.addEventListener)` guarded), and at the end of `updatePanel()` (the panel's content lands ~120ms after load; without this the first paint measured the empty 60px header). - Mobile block: `.scr{padding-bottom:calc(var(--sheet-h) + 14px + env(safe-area-inset-bottom))}`, `.step-nav{position:sticky;bottom:calc(var(--sheet-h) + 14px + env(safe-area-inset-bottom)); background:var(--bg);z-index:20;padding:6px 0}` (spec-verbatim, + see deviation D5), and **`#btn-next{position:sticky;bottom:calc(var(--sheet-h) + 12px + env(safe-area-inset-bottom));z-index:21}`** — the CTA itself carries the pin (see deviation D4: in this codebase `.step-nav` is the Back row *above* `#step-body`, so a bottom-sticky `.step-nav` alone cannot lift the CTA that lives after the step content). **Measured (live, `verify2.py`; sheet 218px, `--sheet-h` live at `218px`/`274px` expanded):** | Check | Before | After | |---|---|---| | step 0, @0: `#btn-next` bottom vs panel top (390) | 717 vs 618 — behind, untappable | **614 ≤ 616** ✓ tappable ✓ | | step 0, @0 (360) | 717 vs 574 — behind | **570 ≤ 572** ✓ tappable ✓ | | step 0, @0 (320) | 751 vs 474 — behind | **470 ≤ 472** ✓ tappable ✓ | | step 13 (interests), @0: next bottom / tappable | 390: 606 ✓; **360: 665, ✗; 320: 693, ✗** | 390: 606 ✓; **360: 570 ✓; 320: 470 ✓** (all tappable) | | step 0, @mid: nav bottom ≤ panelTop−2 / next pinned | 141 (static, mid-scroll unreachable) | nav 88/66/−1, next **614/570/470** ✓ | | step 0, @max: last `.opt` (Nunavut) bottom ≤ panelTop−2 | 630/630/664 (mid-scroll unreachable) | **513/469/369** ✓, next tappable ✓ | | `.scr` padding-bottom | 185px | **232px** = 218+14 | | short step (income): nav top (doc) / CTA top | 97 / 485 | **97 / 485** (390 & 360 — not stuck, pixel-identical to round 1) | | income at 320 (step taller than viewport) | CTA 485–535, behind sheet top 474 | **CTA pinned, bottom 470 ≤ 472** ✓ (the fix working) | | desktop 1280: `.step-nav` position / `.scr` padding | static / 80px | **static / 80px** ✓ (navTop 107 unchanged) | `sheetVar` was `""` before; now `218px` collapsed, `274px` while the panel is expanded — clearance tracks the sheet live. ## Issue 2 — "swipe →" overlay painted over chart labels (FIXED) **Root cause.** `.chartbox::before{content:"swipe →";position:absolute;top:…;left:…}` was an overlay on top of the SVG — it sat over the tick labels. **Fix (spec as written).** Removed the `::before` rule entirely; the chart cards' h3+p blocks moved from `index.html` into `app.js` so every render writes the card via one `chart(sel, inner, legend)` helper (title + caption + `swipe →` in `.chart-h`, SVG, legend — all as one innerHTML string, shim-safe). Base: `.chart-h{display:flex;align-items:flex-start; justify-content:space-between;gap:10px}`, `.swipe{display:none}`; ≤860px: `.swipe` shown with the spec chip style (`11.5px`, `var(--muted)` on `var(--chip)`, radius 99, `padding:3px 9px`, nowrap, `flex:none`) + `.chart-h{position:sticky;left:0}` so the hint stays pinned while the SVG scrolls sideways. **Measured.** At 320/360/390, on all 4 charts: `.swipe` present, inside `.chart-h`, `offsetParent ≠ null` (visible), inside card bounds, and **intersects 0 of 11–13 `` rects**. Desktop 1280: present, `display:none` (hidden) ✓. ## Issue 3 — Anchor jumps land under the sticky header (FIXED) **Fix (spec as written).** Global `[id]{scroll-margin-top:70px}` (right after `html{scroll-behavior}`). **Measured (live, 390 results → `#whatif`, 2000 ms):** `scrollY 2662` (>0 ✓), target top **70** vs header bottom 55 → clears by 15px ✓. 360: `2771 / 70 / 55` ✓. 320: `2896 / 70 / 55` ✓. (Before: 390 top 56 — passed by 1px; 360/320 top **0.0** — covered, FAIL.) --- ## Documented NON-issues (no changes made, per spec) 1. **G2 "$18k" y-tick looked clipped.** Artifact of the round-1 screenshot: the chart card is taller than it is wide and the screenshot cropped at the card edge. Measured live before *and* after: `svgFitsVertically: true`, `svgBottomMinusBoxBottom: −35` (SVG bottom sits 35px **inside** the card), axis tick set renders clean (`["80","85","90","age"]` bottom row, 11 text nodes). Nothing is clipped. 2. **"Four futures" bar cut at the card edge.** Intentional: `.chartbox{overflow-x:auto}` + `.chartbox svg{min-width:560px}` (round-1 Fix 2) give charts horizontal scroll on phones — desktop scale is preserved and the new in-header "swipe →" hint (Issue 2) now explains it. ## Tests / deploy / off-LAN - `node tests/engine.test.js` → **36 pass, 0 fail**; `node tests/ui.smoke.js` → **SMOKE-OK** (all round-2 JS is shim-safe: `chart()` writes one innerHTML string directly to `#g1–#g4`; `syncSheetH` null/guard-checked; `window.addEventListener` guarded; `#lifeexp-lbl` assignment guarded since the label is now created by the g4 header). - `bash scripts/deploy.sh` → **DEPLOY-OK**; `curl … | grep -c 'retire-planner v1'` → **1**. - Off-LAN via Midhurst troublebox (paramiko, password harvested by regex, never printed): login OK (`trouble@100.96.20.61:2222`), `curl https://retire.krisforbes.ca/` → **200** (attempt 1), marker count from box **1** → `OFFLAN-OK`. ## Deviations & notes (all forced by the codebase, all inside the hard constraints) - **D1** `--muted:#4c5b74; --chip:#eef2f8` added to `:root` (the spec's `.swipe` style references them; they existed only as literal colors in round 1 — same values, so nothing re-colored). - **D2** `.chart-h>div{min-width:0}` wrapper: keeps the h3+p stack rendering pixel-equivalent to the old block on desktop (flex `min-width:auto` would stretch the caption; desktop is unchanged per the constraint). - **D3** Chart h3/captions moved from `index.html` to `CHART_HDR` in `app.js` verbatim — required by the spec's own design ("render the span … via app.js chart() helper"; every re-render must keep the header). `#lifeexp-lbl` is now regenerated by the g4 header from `state.a.lifeExp` (verified live: renders the answer value). - **D4** `#btn-next` sticky pin added alongside the spec's `.step-nav` pin. The spec's premise ("the nav row is the last row on screen") doesn't match this codebase: `.step-nav` (Back + step count) sits *above* `#step-body` and the CTA is the last element of the flow — `.step-nav`-only sticky leaves the CTA behind the sheet (staging-measured: `nextBottom 729 > panelTop 618`, untappable). The added rule is mobile-scoped, z-ordered under scrim(35)/sheet(40), and is what makes the spec's Issue-1 assertions pass. - **D5** `.quiz-main .step-nav{margin-bottom:-6px}` (mobile): the spec's `padding:6px 0` grows the nav box by 12px and pushes short-step content down; netting the base 6px margin makes the non-stuck short steps **pixel-identical to round 1** (income CTA top 485 = the spec's "~485", nav box top 97 = true round-1 value measured from the git HEAD copy at all 3 widths). Specificity raised (`.quiz-main .step-nav`) because the base `.step-nav` rule sits later in the file (cascade order). - **D6** Spec's "income nav top ~485": the round-1 **nav box** top in doc coords is 97 (measured on the HEAD copy; 485 is the round-1 CTA-row top). Both are asserted unchanged after the fix. - **Verification note:** `scrollTo` + rect reads must settle (`scroll-behavior:smooth`) — `verify2.py` scrolls with `behavior:'instant'` + 350 ms settle before measuring; the pre-fix transcript's `@mid/@max` rows (scrollY 0) are the artifact this fixed. - Hard constraints honored: `site/engine/*.js` and constants untouched; `` first line in ``; privacy copy untouched; no frameworks/build/CDN; all new CSS in the ≤860px block (plus the two global rules the spec prescribes: `[id]{scroll-margin-top}` and `.chart-h`/`.swipe` base, which are layout-neutral on desktop and measured unchanged — desktop nav static, padding 80px, swipe hidden). ## Files changed `site/style.css` (+27/−2) · `site/app.js` (+50/−8) · `site/index.html` (+1/−12). Evidence/verifiers added: `audit/verify2.py`, `audit/measure2_run.py`, `audit/mobile_audit_run2.py`, `audit/mobile-before2/` (6 shots + `geo-round2.txt` + pre-fix `verify-round2.txt`), `audit/mobile-after2/` (6 viewport + 12 `full-*.png` shots, `geo-round2.txt`, `verify-round2.txt` 90 PASS / 0 FAIL, `audit-run.txt`).