- 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.
9.5 KiB
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()inapp.jswritesdocument.documentElement.style.setProperty('--sheet-h', panel.offsetHeight+'px')— shim-guarded (var h = panel && panel.offsetHeight; if (h)), called on init (final statement), on.panel-htoggle, on scrim toggle, onresize(if (window.addEventListener)guarded), and at the end ofupdatePanel()(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-navis the Back row above#step-body, so a bottom-sticky.step-navalone 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)
- 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. - "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;syncSheetHnull/guard-checked;window.addEventListenerguarded;#lifeexp-lblassignment 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:#eef2f8added to:root(the spec's.swipestyle 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 (flexmin-width:autowould stretch the caption; desktop is unchanged per the constraint). - D3 Chart h3/captions moved from
index.htmltoCHART_HDRinapp.jsverbatim — required by the spec's own design ("render the span … via app.js chart() helper"; every re-render must keep the header).#lifeexp-lblis now regenerated by the g4 header fromstate.a.lifeExp(verified live: renders the answer value). - D4
#btn-nextsticky pin added alongside the spec's.step-navpin. 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-bodyand 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'spadding:6px 0grows 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-navrule 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.pyscrolls withbehavior:'instant'+ 350 ms settle before measuring; the pre-fix transcript's@mid/@maxrows (scrollY 0) are the artifact this fixed. - Hard constraints honored:
site/engine/*.jsand 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/.swipebase, 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).