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

144 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).