- 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.
106 lines
7.4 KiB
Markdown
106 lines
7.4 KiB
Markdown
# 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`
|