# 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 `` to `site/index.html` (inside `
`, 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=` 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 `` in ``. - `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 `` as the first line inside `` (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 `` 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`