11 KiB
MOBILE PASS — 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. The user said: "do another pass to ensure mobile is a good experience and have DSH fix any issues you find."
Evidence base (before-state, already collected by Hermes)
Mobile audit screenshots (Playwright, real Chromium, mobile emulation) are in
/home/ubuntu/.hermes/cache/scratch/mobile-audit/ — devices: iPhone-14 390×844@3x,
Pixel 360×800@2.75x, iPhone-SE 320×700@2x. Re-run anytime with:
python3 /home/ubuntu/.hermes/cache/scratch/mobile_audit.py
Measured geometry (390px quiz): sticky header 126px tall (wraps to ~3 rows);
.ghost buttons 32px tall (below the 44px touch minimum); G1 SVG renders at
336px wide → scale 0.525 → 11px axis text renders at ~5.8px (unreadable);
year table scrollWidth 595 vs clientWidth 326 (1.8× horizontal scroll);
vision card renders gradient fallback on direct load into results (race, see #6).
Vision-checked findings (from the screenshots):
- Header eats ~15% of the phone screen — logo + privacy pill + buttons wrap to 3 rows.
- Chart axis/legend/annotation text unreadable on phones (~6px at 390px, worse at 320px; the "retire" marker label is nearly illegible).
- Bottom sheet collapsed state looks broken — it peeks 58px and clips chip labels mid-glyph at the screen edge ("SAVINGS AT…" cut off). Expanded sheet covers the Next button with no scrim/close affordance.
- Year-by-year table needs 1.8× horizontal scroll; 11 columns; no fade/hint that it scrolls.
- Touch targets: Back/Start over/Print/Share ghost buttons are 32px tall.
- Vision card race: on direct load into results (shared link
#p=…or saved state),renderVisionruns beforefetch('vision/manifest.json')resolves → permanent gradient fallback even though the images exist. - Share button lies: if
navigator.clipboardis missing/fails (older mobile Safari, non-secure context), it still shows "✅ Link copied". - Minor: no
theme-colormeta; legend 12.5px;.opt small11.5px.
Fixes (all in site/ — index.html, style.css, app.js)
Fix 1 — single-row compact header on mobile
In style.css, add inside a @media(max-width:860px) block:
.hdr-in{flex-wrap:nowrap;padding:6px 10px;gap:8px}and.hdr{padding-top:env(safe-area-inset-top)}.logo{font-size:15px}; shrink header.ghosttopadding:6px 9px;font-size:13px- Hide
.hdr .pillat ≤700px (the privacy pill is repeated on the intro screen and the footer — no information is lost). - Hide the header
#btn-printat ≤860px on mobile (the results page keeps its own Print button in.btn-row; header print is a desktop affordance). - Result: header ≤ ~52px tall, one row, at 390/360/320px. Desktop header unchanged.
Fix 2 — readable charts on phones
Two parts:
- Bump SVG base fonts in
app.js(affects all viewports, small improvement everywhere):axes()tick labels 11→13, axis-title/"age" 11→12; theretireannotation 10.5→12;runs out ≈12→13; G3shortfall from ~12→13; G4 bar value labels 12.5→13.5 and bar name labels 10.5→12. - Horizontal scroll on mobile: in
style.css@media(max-width:860px):.chartbox{overflow-x:auto;overflow-y:hidden}and.chartbox svg{min-width:560px}. Add a subtle "swipe →" hint: CSS-only via.chartbox::after{content:"swipe →";…}shown only ≤860px (position it top-right of the chartbox, tiny, muted). Optionally hide it after the first scroll with ≤3 lines of JS — not required. - Keep
width:100%;height:autoso desktop is unchanged.
Fix 3 — bottom sheet redesign (mobile only; desktop panel untouched)
Current: fixed sheet, transform:translateY(calc(100% - 58px)) peek, chips+insight inside, tap header toggles.
New (≤860px only):
- Chips always visible in the collapsed sheet (this is the user's core "building feedback" requirement — on mobile it must not hide behind a tap).
.insight{display:none}by default on mobile;.panel.open .insight{display:block}. - Remove the
transformpeek trick entirely on mobile — the sheet height is just its content (header + 2×2 chips ≈ 150px; +insight when open). - Keep the
.panel-htap-to-toggle (existing document click handler) — it now toggles the insight line. Keep the ▲/▼ affordance. - Add a scrim: add
<div id="scrim" hidden></div>tosite/index.html(inside<main>, after#scr-quizis fine — it isposition:fixed). CSS ≤860px:#scrim{position:fixed;inset:0;background:#16233b33;z-index:35}shown only when the panel has.open(use#scrim.on{display:block}— toggle a class, nothidden, or use[hidden]{display:none}carefully; simplest: JS toggleshidden+ a class). Panel z-index stays 40 (above scrim), header 30 (below). JS: clicking#scrimremoves.openfrom#panel(and hides scrim). On desktop the scrim never shows (media query display:none). - Safe areas + content clearance:
.panel{bottom:calc(8px + env(safe-area-inset-bottom))}and on mobile.scr{padding-bottom:calc(185px + env(safe-area-inset-bottom))}so the step content (incl. the Next button) clears the always-visible sheet. - Verify with screenshots at 390/360/320: collapsed sheet shows header + 4 chips with values fully visible (no mid-glyph clipping); Next button reachable; tapping the sheet header reveals the insight; tapping the scrim closes it.
Fix 4 — touch targets + tap zoom
style.css: .ghost{min-height:44px} (harmless on desktop) and button,input{touch-action:manipulation} (kills the 300ms double-tap zoom delay on mobile).
Fix 5 — year-by-year table on small screens
In style.css @media(max-width:560px): hide the two least decision-relevant columns — th:nth-child(4),td:nth-child(4) (Non-reg) and th:nth-child(9),td:nth-child(9) (GIS) — with display:none. Keep the Unfunded column (depletion rows are also highlighted red via tr.dep, but the explicit shortfall number is the most decision-relevant). Bump table{font-size:13px} on mobile. Add a right-edge fade on .tbl-wrap (CSS ::after gradient overlay, pointer-events:none) so the horizontal scroll is discoverable. Keep the sticky first column (td:first-child,th:first-child sticky left) and sticky header row — verify in the after-screenshots that the Age column stays visible while scrolled right.
Fix 6 — vision card race (app.js)
Current: fetch('vision/manifest.json')…then(m => visionManifest = m) at load; renderVision uses visionManifest if set, else falls back to gradient permanently for that render.
Fix: after the fetch resolves (and on fetch failure), if the results screen is currently shown and the vision card is in gradient mode, re-render the vision card with the manifest. Minimal implementation: keep a visionReady promise; in its .then/.catch, set visionManifest and call renderVision(state.a) again (guard with if (state.screen === 'results')). Keep the img.onerror → grad() fallback. Also add a 2.5s timeout fallback so a hung fetch can't leave the card gradient forever if images actually exist — actually simpler: on timeout just leave the gradient (images may genuinely be missing); the important fix is re-rendering once the manifest lands. Verify: load https://retire.krisforbes.ca/#p=<shared-link> fresh (or set localStorage screen=results and reload) → the vision card shows the real image, not the gradient. The audit script's results screenshots must show a real photo.
Fix 7 — honest share fallback (app.js)
In #btn-share handler: if navigator.clipboard.writeText succeeds → "✅ Link copied". Otherwise (missing API or rejection) → window.prompt('Copy your link:', url) and set the button text to "🔗 Link shown — copy it" for 1.8s. Never claim "copied" when it wasn't.
Fix 8 — polish
site/index.html: add<meta name="theme-color" content="#f7f9fc">in<head>.style.css:.legend{font-size:13px},.opt small{font-size:12px}.- Optional (only if trivial): in
renderStep, fortype:'number'steps add a keydown listener on#f1: Enter → if Next is enabled, click it. Skip if it complicates the smoke shim.
Hard constraints
- Do not touch
site/engine/engine.js,site/engine/canada.js, any tax/benefit constants, or engine logic. This pass is UI/CSS only (plus the vision re-render and share fallback inapp.js). - Keep the marker
<!-- retire-planner v1 -->as the first line inside<head>(deploy.sh greps for it). - Keep the privacy statement text untouched.
- No frameworks, no build step, no CDN — vanilla files only.
- Both test suites must stay green:
node tests/engine.test.js(36/36) andnode tests/ui.smoke.js(SMOKE-OK). If a change breaks the smoke shim, extend the shim minimally — do NOT weaken assertions. - Desktop (≥861px) layout must remain visually unchanged except the small font bumps in Fix 2/8.
Verification steps (DSH must do all, capture evidence)
node tests/engine.test.js→ 36/36 PASS;node tests/ui.smoke.js→ SMOKE-OK.- Preserve before-screenshots:
mkdir -p audit/mobile-before && cp /home/ubuntu/.hermes/cache/scratch/mobile-audit/*.png audit/mobile-before/ - Deploy:
bash scripts/deploy.sh→ ends withDEPLOY-OK. - Origin check:
curl -s http://192.168.0.123:8103/ | grep -c 'retire-planner v1'→ 1. - Re-run the mobile audit:
python3 /home/ubuntu/.hermes/cache/scratch/mobile_audit.py(needs the playwright install already on this box +/home/ubuntu/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome). Thenmkdir -p audit/mobile-after && cp /home/ubuntu/.hermes/cache/scratch/mobile-audit/*.png audit/mobile-after/ - Programmatic geometry re-check on 390px (extend the audit script or a small inline Playwright snippet): header height ≤ 56px; every
.ghost/.opt/.cta≥ 44px tall; chart svgmin-width560px on mobile (scrollWidth > clientWidth inside.chartbox); vision card contains an<img>on fresh results load; collapsed sheet shows all 4 chip values fully (no clipping — compare chip bounding boxes vswindow.innerHeight). - Off-LAN spot check from the Midhurst troublebox (paramiko; password candidates live in
~/projects/*.pylines containingb1tvvt, host100.96.20.61, trytroubleport 2222 first):curl -s -o /dev/null -w '%{http_code}' https://retire.krisforbes.ca/→ 200, and one vision image → 200. - Commit + push everything to
gitea.krisforbes.ca/krisf/retire-planner(main): site changes +audit/mobile-before/+audit/mobile-after/+ this spec asMOBILE-FIX.md+ a shortMOBILE-FIX-REPORT.mdmapping each numbered issue → fix → verification evidence.
Definition of done
- All 8 fixes implemented; tests green; deployed; origin + off-LAN 200; after-screenshots show: single-row header, readable charts (min-width scroll), always-visible chips sheet with scrim, 44px+ touch targets, table with hidden Non-reg/GIS columns + fade, real vision image on fresh results load.
MOBILE-FIX-REPORT.mdcommitted + pushed.- Your FINAL output line must be exactly:
MOBILE_PASS_DONE