Files
retire-planner/MOBILE-FIX.md
T

11 KiB
Raw Blame History

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):

  1. Header eats ~15% of the phone screen — logo + privacy pill + buttons wrap to 3 rows.
  2. Chart axis/legend/annotation text unreadable on phones (~6px at 390px, worse at 320px; the "retire" marker label is nearly illegible).
  3. 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.
  4. Year-by-year table needs 1.8× horizontal scroll; 11 columns; no fade/hint that it scrolls.
  5. Touch targets: Back/Start over/Print/Share ghost buttons are 32px tall.
  6. Vision card race: on direct load into results (shared link #p=… or saved state), renderVision runs before fetch('vision/manifest.json') resolves → permanent gradient fallback even though the images exist.
  7. Share button lies: if navigator.clipboard is missing/fails (older mobile Safari, non-secure context), it still shows "✅ Link copied".
  8. Minor: no theme-color meta; legend 12.5px; .opt small 11.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 .ghost to padding:6px 9px;font-size:13px
  • Hide .hdr .pill at ≤700px (the privacy pill is repeated on the intro screen and the footer — no information is lost).
  • Hide the header #btn-print at ≤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; the retire annotation 10.5→12; runs out ≈ 12→13; G3 shortfall 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:auto so 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 transform peek trick entirely on mobile — the sheet height is just its content (header + 2×2 chips ≈ 150px; +insight when open).
  • Keep the .panel-h tap-to-toggle (existing document click handler) — it now toggles the insight line. Keep the ▲/▼ affordance.
  • Add a scrim: add <div id="scrim" hidden></div> to site/index.html (inside <main>, after #scr-quiz is fine — it is position: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, not hidden, or use [hidden]{display:none} carefully; simplest: JS toggles hidden + a class). Panel z-index stays 40 (above scrim), header 30 (below). JS: clicking #scrim removes .open from #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, for type:'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 in app.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) and node 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)

  1. node tests/engine.test.js → 36/36 PASS; node tests/ui.smoke.js → SMOKE-OK.
  2. Preserve before-screenshots: mkdir -p audit/mobile-before && cp /home/ubuntu/.hermes/cache/scratch/mobile-audit/*.png audit/mobile-before/
  3. Deploy: bash scripts/deploy.sh → ends with DEPLOY-OK.
  4. Origin check: curl -s http://192.168.0.123:8103/ | grep -c 'retire-planner v1' → 1.
  5. 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). Then mkdir -p audit/mobile-after && cp /home/ubuntu/.hermes/cache/scratch/mobile-audit/*.png audit/mobile-after/
  6. 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 svg min-width 560px 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 vs window.innerHeight).
  7. Off-LAN spot check from the Midhurst troublebox (paramiko; password candidates live in ~/projects/*.py lines containing b1tvvt, host 100.96.20.61, try trouble port 2222 first): curl -s -o /dev/null -w '%{http_code}' https://retire.krisforbes.ca/ → 200, and one vision image → 200.
  8. Commit + push everything to gitea.krisforbes.ca/krisf/retire-planner (main): site changes + audit/mobile-before/ + audit/mobile-after/ + this spec as MOBILE-FIX.md + a short MOBILE-FIX-REPORT.md mapping 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.md committed + pushed.
  • Your FINAL output line must be exactly: MOBILE_PASS_DONE