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