Files
retire-planner/MOBILE-FIX-2-REPORT.md
T
krisf 65863c0302 Round 2 mobile pass: CTA pinned above bottom sheet, swipe hint in chart header, anchor scroll-margin
- Issue 1: --sheet-h CSS var (live sheet height via syncSheetH: load/toggle/resize/
  panel-content), .scr clearance and sticky #btn-next/.step-nav above the sheet;
  income short step pixel-identical to round 1 (CTA top 485).
- Issue 2: chart() helper renders .chart-h (h3+p + .swipe span) into #g1-#g4;
  .chartbox::before overlay removed; hint is flex sibling in header, sticky left.
- Issue 3: [id]{scroll-margin-top:70px} so #whatif anchor lands below sticky hdr.
- G2 tick clip and Four-futures scroll documented as non-issues in report.
- verify2.py: 90/90 PASS live; before/after evidence in audit/mobile-{before,after}2/.

Spec: MOBILE-FIX-2.md. Report: MOBILE-FIX-2-REPORT.md.
2026-10-09 06:44:08 -04:00

9.5 KiB
Raw Blame History

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 + <span class="swipe">swipe →</span> 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 <text> 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; <!-- retire-planner v1 --> first line in <head>; 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).