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.
This commit is contained in:
krisf committed 2026-10-09 06:44:08 -04:00
1 parent 53315e03e8
commit 1b194b6f92
37 files changed
+1007 -21

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
# MOBILE PASS ROUND 2 — fix spec for https://retire.krisforbes.ca
**Spec v1 — written 2026-10-09 by Hermes (GLM). Execution: DSH (local 27B), all steps, zero user interaction.**
Round 1 (commit 666b6c5) shipped 8 fixes. Hermes then re-audited the live site with vision-checked
screenshots + DOM geometry and found **3 remaining issues** (below). Fix exactly these; do not
re-litigate round-1 decisions.
## Evidence (measured by Hermes against the LIVE site, 2026-10-09)
Geometry script: `/home/ubuntu/.hermes/cache/scratch/measure2.py` (Playwright, mobile emulation,
390×844 / 360×800 / 320×700). Screenshots: `/home/ubuntu/.hermes/cache/scratch/mobile-audit2/`.
### Issue 1 — Primary CTA hidden behind the bottom sheet on tall steps (all 3 widths)
Step 0 (province grid, 13 options) at initial scroll (`scrollY=0`):
| Device | Next button (viewport y) | Sheet top | Next behind sheet? | Next tappable? | Nunavut bottom vs sheet top |
|---|---|---|---|---|---|
| 390×844 | 667–717 | 618 | **yes, fully** | **false** | 630 > 618 (behind) |
| 360×800 | 667–717 | 574 | **yes, fully** | **false** | 630 > 574 (behind) |
| 320×700 | 701–751 | 474 | **yes, fully** | **false** | 664 > 474 (behind) |
Root cause, two parts:
- `.scr{padding-bottom:185px}` (mobile) is **smaller than the sheet obstruction** (sheet 218px tall +
8px bottom offset + safe-area = 226px+). At max scroll (70px on 390w) the Next bottom lands at
viewport y=647, still 29px **below** the sheet top (618). The padding must be ≥ sheet obstruction + gap.
- On tall steps the Back/Next row sits below the fold at load — the primary CTA of step 1 of 14 is
invisible until the user scrolls.
**Fix (all rules scoped to `@media(max-width:860px)`):**
1. Maintain a CSS var with the live sheet height: on load, on `resize`, and whenever the panel
toggles `.open`, run `document.documentElement.style.setProperty('--sheet-h', panel.offsetHeight + 'px')`
(guard for the smoke-test shim: `const h = panel && panel.offsetHeight; if (h) …`). Default
`--sheet-h:218px` on `:root` so the first paint is correct.
2. `.scr{padding-bottom:calc(var(--sheet-h) + 14px + env(safe-area-inset-bottom))}` (replaces the 185px).
3. `.step-nav{position:sticky;bottom:calc(var(--sheet-h) + 14px + env(safe-area-inset-bottom));background:var(--bg);z-index:20;padding:6px 0}`
— Back/Next pin above the sheet on tall steps; on short steps (e.g. income) the nav is naturally
above the stick point so nothing changes visually. Keep it below the scrim (z 35) and sheet (z 40).
Give the stuck bar a solid background so content scrolling under it doesn't show through.
**Verify (programmatic, all 3 widths, step 0 AND step 12/interests):**
- At `scrollY=0`: `#btn-next` bottom ≤ panel top − 2, and `document.elementFromPoint(nextCenter)`
is inside `#btn-next` (tappable).
- While scrolling (scrollY = maxScroll/2): nav bottom stays ≤ panel top − 2 (pinned).
- At max scroll: last `.opt` bottom ≤ panel top − 2 (every option reachable above the sheet).
- Short step (income, step 3): nav top unchanged vs round 1 (~485), i.e. no visual shift when not stuck.
- Desktop (1280px): `.step-nav` computed `position` is `static`, `.scr` padding-bottom back to 80px.
### Issue 2 — "swipe →" pill overlaps the chart's top y-axis tick
Vision-confirmed at 320px: the pill (`.chartbox::before` overlay, top-left of the card) sits on top of
the topmost y-tick ("$1.05M"). At 390px it grazes the tick area too.
**Fix:** remove the `.chartbox::before` overlay entirely. Instead render the hint in the chart card
header: in `app.js`'s `chart()` helper, append `<span class="swipe">swipe →</span>` inside the
`.chart-h` div (which already holds the `<h3>`+`<p>`). CSS: `.chart-h{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}`
and `.swipe{display:none}` by default; ≤860px: `.swipe{display:inline-block;font-size:11.5px;color:var(--muted);background:var(--chip);border-radius:99px;padding:3px 9px;white-space:nowrap;flex:none}`.
The hint then lives in the header row and can never overlap SVG content. (The `.chart-h` flex change
is safe on desktop — the span is `display:none` there.)
**Verify:** at 320/360/390, for each of #g0–#g3: the `.swipe` rect does not intersect any `<text>`
rect of that chart's SVG; `.swipe` is visible (offsetParent ≠ null); hint present on all 4 charts.
### Issue 3 — anchor targets can hide behind the sticky header
The results page has in-page anchors (`#whatif`, `#years` details). The header is `position:sticky`,
so a native anchor jump can land the target underneath it. (Hermes' measurement was inconclusive —
re-measure with a 2s wait; regardless, the fix is one line and correct on desktop too, where the
header is also sticky.)
**Fix:** `style.css` (global, not media-scoped): `[id]{scroll-margin-top:70px}`.
**Verify:** on results at 390px: `location.hash='#whatif'`, wait **2000ms** (the page has
`scroll-behavior:smooth`), assert `window.scrollY > 0` AND
`document.querySelector('#whatif').getBoundingClientRect().top >= document.querySelector('.hdr').getBoundingClientRect().bottom - 1`.
### Verified NON-issues (document in the report, do not "fix")
- G2's bottom y-tick "$18k" looked half-clipped in a viewport screenshot — measured: the SVG fits
vertically (`svgFitsVertically:true`, svg bottom 35px above the box bottom). It was a screenshot
crop artifact.
- "Four futures" bar chart's third bar is cut at the card's right edge — that is the intentional
horizontal scroll container working as designed (swipe hint present).
## Hard constraints (same as MOBILE-FIX.md)
- Do not touch `site/engine/*.js` or any constants. UI/CSS/JS-flow only.
- Keep `<!-- retire-planner v1 -->` as first line in `<head>`; keep privacy text untouched.
- No frameworks/build/CDN. Desktop (≥861px) visually unchanged (sticky nav + bigger padding are
≤860px-only; `scroll-margin-top` is a global improvement, not a regression).
- `node tests/engine.test.js` → 36/36; `node tests/ui.smoke.js` → SMOKE-OK. Shim-safe JS only
(guard `panel.offsetHeight` etc.).
## Verification + delivery steps
1. Tests green (both suites).
2. `mkdir -p audit/mobile-before2 && cp /home/ubuntu/.hermes/cache/scratch/mobile-audit2/*.png audit/mobile-before2/`
(round-2 before evidence; also copy `audit/mobile-before2/geo-round2.txt` — write the measure2
output there: run `python3 /home/ubuntu/.hermes/cache/scratch/measure2.py > audit/mobile-before2/geo-round2.txt 2>&1`).
3. Deploy: `bash scripts/deploy.sh` → `DEPLOY-OK`; origin grep marker → 1.
4. Re-run `python3 /home/ubuntu/.hermes/cache/scratch/measure2.py` (after state) and save output +
screenshots: extend measure2 usage — copy its output to `audit/mobile-after2/geo-round2.txt` and
its PNGs to `audit/mobile-after2/`. Also re-run `python3 /home/ubuntu/.hermes/cache/scratch/mobile_audit.py`
and copy those PNGs to `audit/mobile-after2/full-*.png` (prefix them so they don't collide).
5. All Issue-1/2/3 verification assertions above must pass; record the numbers.
6. Off-LAN (Midhurst troublebox paramiko, `trouble@100.96.20.61:2222`, password candidates in
`~/projects/*.py` lines containing `b1tvvt`): `https://retire.krisforbes.ca/` → 200.
7. Commit + push to `krisf/retire-planner` main: site changes + `MOBILE-FIX-2.md` +
`MOBILE-FIX-2-REPORT.md` (issue → fix → measured evidence, incl. the non-issues section) +
`audit/mobile-before2/` + `audit/mobile-after2/`.
8. Your FINAL output line must be exactly: `MOBILE_PASS_2_DONE`