Files
krisf 1b194b6f92 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

7.4 KiB
Raw Permalink Blame History

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