- 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.
144 lines
9.5 KiB
Markdown
144 lines
9.5 KiB
Markdown
# MOBILE-FIX-2 — Report
|
||
|
||
Round 2 of the mobile pass on https://retire.krisforbes.ca, per `MOBILE-FIX-2.md`.
|
||
All three issues fixed, deployed, and verified on the live site at 320/360/390px (iPhone SE,
|
||
Pixel, iPhone 14) + 1280px desktop. Evidence: `audit/mobile-before2/` (pre-fix) and
|
||
`audit/mobile-after2/` (post-fix) — geometry logs, verification transcripts, viewport and
|
||
full-page screenshots (`full-*.png`).
|
||
|
||
**Result: 90/90 live verification assertions PASS (`VERIFY2-OK`), tests 36/36 + SMOKE-OK,
|
||
`DEPLOY-OK`, marker count 1, off-LAN (Midhurst) HTTP 200 + marker 1 (`OFFLAN-OK`).**
|
||
|
||
---
|
||
|
||
## Issue 1 — Primary CTA hidden behind the bottom sheet (FIXED)
|
||
|
||
**Root cause.** `.scr` clearance was a fixed `185px` while the sheet is 218px tall, and the
|
||
Back/Next rows (`position:static`) scrolled behind the sheet. The live sheet height also changes
|
||
when the panel is expanded, so no constant works.
|
||
|
||
**Fix (spec as written, + one necessary addition).**
|
||
- `:root{--sheet-h:218px}`; `syncSheetH()` in `app.js` writes
|
||
`document.documentElement.style.setProperty('--sheet-h', panel.offsetHeight+'px')` — shim-guarded
|
||
(`var h = panel && panel.offsetHeight; if (h)`), called on init (final statement), on `.panel-h`
|
||
toggle, on scrim toggle, on `resize` (`if (window.addEventListener)` guarded), and at the end of
|
||
`updatePanel()` (the panel's content lands ~120ms after load; without this the first paint
|
||
measured the empty 60px header).
|
||
- Mobile block: `.scr{padding-bottom:calc(var(--sheet-h) + 14px + env(safe-area-inset-bottom))}`,
|
||
`.step-nav{position:sticky;bottom:calc(var(--sheet-h) + 14px + env(safe-area-inset-bottom));
|
||
background:var(--bg);z-index:20;padding:6px 0}` (spec-verbatim, + see deviation D5), and
|
||
**`#btn-next{position:sticky;bottom:calc(var(--sheet-h) + 12px + env(safe-area-inset-bottom));z-index:21}`**
|
||
— the CTA itself carries the pin (see deviation D4: in this codebase `.step-nav` is the Back row
|
||
*above* `#step-body`, so a bottom-sticky `.step-nav` alone cannot lift the CTA that lives after
|
||
the step content).
|
||
|
||
**Measured (live, `verify2.py`; sheet 218px, `--sheet-h` live at `218px`/`274px` expanded):**
|
||
|
||
| Check | Before | After |
|
||
|---|---|---|
|
||
| step 0, @0: `#btn-next` bottom vs panel top (390) | 717 vs 618 — behind, untappable | **614 ≤ 616** ✓ tappable ✓ |
|
||
| step 0, @0 (360) | 717 vs 574 — behind | **570 ≤ 572** ✓ tappable ✓ |
|
||
| step 0, @0 (320) | 751 vs 474 — behind | **470 ≤ 472** ✓ tappable ✓ |
|
||
| step 13 (interests), @0: next bottom / tappable | 390: 606 ✓; **360: 665, ✗; 320: 693, ✗** | 390: 606 ✓; **360: 570 ✓; 320: 470 ✓** (all tappable) |
|
||
| step 0, @mid: nav bottom ≤ panelTop−2 / next pinned | 141 (static, mid-scroll unreachable) | nav 88/66/−1, next **614/570/470** ✓ |
|
||
| step 0, @max: last `.opt` (Nunavut) bottom ≤ panelTop−2 | 630/630/664 (mid-scroll unreachable) | **513/469/369** ✓, next tappable ✓ |
|
||
| `.scr` padding-bottom | 185px | **232px** = 218+14 |
|
||
| short step (income): nav top (doc) / CTA top | 97 / 485 | **97 / 485** (390 & 360 — not stuck, pixel-identical to round 1) |
|
||
| income at 320 (step taller than viewport) | CTA 485–535, behind sheet top 474 | **CTA pinned, bottom 470 ≤ 472** ✓ (the fix working) |
|
||
| desktop 1280: `.step-nav` position / `.scr` padding | static / 80px | **static / 80px** ✓ (navTop 107 unchanged) |
|
||
|
||
`sheetVar` was `""` before; now `218px` collapsed, `274px` while the panel is expanded —
|
||
clearance tracks the sheet live.
|
||
|
||
## Issue 2 — "swipe →" overlay painted over chart labels (FIXED)
|
||
|
||
**Root cause.** `.chartbox::before{content:"swipe →";position:absolute;top:…;left:…}` was an
|
||
overlay on top of the SVG — it sat over the tick labels.
|
||
|
||
**Fix (spec as written).** Removed the `::before` rule entirely; the chart cards' h3+p blocks moved
|
||
from `index.html` into `app.js` so every render writes the card via one `chart(sel, inner, legend)`
|
||
helper (title + caption + `<span class="swipe">swipe →</span>` in `.chart-h`, SVG, legend — all as
|
||
one innerHTML string, shim-safe). Base: `.chart-h{display:flex;align-items:flex-start;
|
||
justify-content:space-between;gap:10px}`, `.swipe{display:none}`; ≤860px: `.swipe` shown with the
|
||
spec chip style (`11.5px`, `var(--muted)` on `var(--chip)`, radius 99, `padding:3px 9px`,
|
||
nowrap, `flex:none`) + `.chart-h{position:sticky;left:0}` so the hint stays pinned while the SVG
|
||
scrolls sideways.
|
||
|
||
**Measured.** At 320/360/390, on all 4 charts: `.swipe` present, inside `.chart-h`,
|
||
`offsetParent ≠ null` (visible), inside card bounds, and **intersects 0 of 11–13 `<text>` rects**.
|
||
Desktop 1280: present, `display:none` (hidden) ✓.
|
||
|
||
## Issue 3 — Anchor jumps land under the sticky header (FIXED)
|
||
|
||
**Fix (spec as written).** Global `[id]{scroll-margin-top:70px}` (right after `html{scroll-behavior}`).
|
||
|
||
**Measured (live, 390 results → `#whatif`, 2000 ms):** `scrollY 2662` (>0 ✓), target top **70**
|
||
vs header bottom 55 → clears by 15px ✓. 360: `2771 / 70 / 55` ✓. 320: `2896 / 70 / 55` ✓.
|
||
(Before: 390 top 56 — passed by 1px; 360/320 top **0.0** — covered, FAIL.)
|
||
|
||
---
|
||
|
||
## Documented NON-issues (no changes made, per spec)
|
||
|
||
1. **G2 "$18k" y-tick looked clipped.** Artifact of the round-1 screenshot: the chart card is
|
||
taller than it is wide and the screenshot cropped at the card edge. Measured live before *and*
|
||
after: `svgFitsVertically: true`, `svgBottomMinusBoxBottom: −35` (SVG bottom sits 35px **inside**
|
||
the card), axis tick set renders clean (`["80","85","90","age"]` bottom row, 11 text nodes).
|
||
Nothing is clipped.
|
||
2. **"Four futures" bar cut at the card edge.** Intentional: `.chartbox{overflow-x:auto}` +
|
||
`.chartbox svg{min-width:560px}` (round-1 Fix 2) give charts horizontal scroll on phones —
|
||
desktop scale is preserved and the new in-header "swipe →" hint (Issue 2) now explains it.
|
||
|
||
## Tests / deploy / off-LAN
|
||
|
||
- `node tests/engine.test.js` → **36 pass, 0 fail**; `node tests/ui.smoke.js` → **SMOKE-OK**
|
||
(all round-2 JS is shim-safe: `chart()` writes one innerHTML string directly to `#g1–#g4`;
|
||
`syncSheetH` null/guard-checked; `window.addEventListener` guarded; `#lifeexp-lbl` assignment
|
||
guarded since the label is now created by the g4 header).
|
||
- `bash scripts/deploy.sh` → **DEPLOY-OK**; `curl … | grep -c 'retire-planner v1'` → **1**.
|
||
- Off-LAN via Midhurst troublebox (paramiko, password harvested by regex, never printed):
|
||
login OK (`trouble@100.96.20.61:2222`), `curl https://retire.krisforbes.ca/` → **200** (attempt 1),
|
||
marker count from box **1** → `OFFLAN-OK`.
|
||
|
||
## Deviations & notes (all forced by the codebase, all inside the hard constraints)
|
||
|
||
- **D1** `--muted:#4c5b74; --chip:#eef2f8` added to `:root` (the spec's `.swipe` style references
|
||
them; they existed only as literal colors in round 1 — same values, so nothing re-colored).
|
||
- **D2** `.chart-h>div{min-width:0}` wrapper: keeps the h3+p stack rendering pixel-equivalent to
|
||
the old block on desktop (flex `min-width:auto` would stretch the caption; desktop is unchanged
|
||
per the constraint).
|
||
- **D3** Chart h3/captions moved from `index.html` to `CHART_HDR` in `app.js` verbatim — required
|
||
by the spec's own design ("render the span … via app.js chart() helper"; every re-render must
|
||
keep the header). `#lifeexp-lbl` is now regenerated by the g4 header from `state.a.lifeExp`
|
||
(verified live: renders the answer value).
|
||
- **D4** `#btn-next` sticky pin added alongside the spec's `.step-nav` pin. The spec's premise
|
||
("the nav row is the last row on screen") doesn't match this codebase: `.step-nav` (Back + step
|
||
count) sits *above* `#step-body` and the CTA is the last element of the flow — `.step-nav`-only
|
||
sticky leaves the CTA behind the sheet (staging-measured: `nextBottom 729 > panelTop 618`,
|
||
untappable). The added rule is mobile-scoped, z-ordered under scrim(35)/sheet(40), and is what
|
||
makes the spec's Issue-1 assertions pass.
|
||
- **D5** `.quiz-main .step-nav{margin-bottom:-6px}` (mobile): the spec's `padding:6px 0` grows the
|
||
nav box by 12px and pushes short-step content down; netting the base 6px margin makes the
|
||
non-stuck short steps **pixel-identical to round 1** (income CTA top 485 = the spec's "~485",
|
||
nav box top 97 = true round-1 value measured from the git HEAD copy at all 3 widths).
|
||
Specificity raised (`.quiz-main .step-nav`) because the base `.step-nav` rule sits later in the
|
||
file (cascade order).
|
||
- **D6** Spec's "income nav top ~485": the round-1 **nav box** top in doc coords is 97 (measured on
|
||
the HEAD copy; 485 is the round-1 CTA-row top). Both are asserted unchanged after the fix.
|
||
- **Verification note:** `scrollTo` + rect reads must settle (`scroll-behavior:smooth`) —
|
||
`verify2.py` scrolls with `behavior:'instant'` + 350 ms settle before measuring; the pre-fix
|
||
transcript's `@mid/@max` rows (scrollY 0) are the artifact this fixed.
|
||
- Hard constraints honored: `site/engine/*.js` and constants untouched; `<!-- retire-planner v1 -->`
|
||
first line in `<head>`; privacy copy untouched; no frameworks/build/CDN; all new CSS in the
|
||
≤860px block (plus the two global rules the spec prescribes: `[id]{scroll-margin-top}` and
|
||
`.chart-h`/`.swipe` base, which are layout-neutral on desktop and measured unchanged
|
||
— desktop nav static, padding 80px, swipe hidden).
|
||
|
||
## Files changed
|
||
|
||
`site/style.css` (+27/−2) · `site/app.js` (+50/−8) · `site/index.html` (+1/−12).
|
||
Evidence/verifiers added: `audit/verify2.py`, `audit/measure2_run.py`, `audit/mobile_audit_run2.py`,
|
||
`audit/mobile-before2/` (6 shots + `geo-round2.txt` + pre-fix `verify-round2.txt`),
|
||
`audit/mobile-after2/` (6 viewport + 12 `full-*.png` shots, `geo-round2.txt`, `verify-round2.txt`
|
||
90 PASS / 0 FAIL, `audit-run.txt`).
|