131 lines
8.0 KiB
Markdown
131 lines
8.0 KiB
Markdown
# QUIZ APP-SHELL FIX — Round 4 Report
|
||
|
||
**Spec:** `QUIZ-APPSHELL-FIX.md` · **Fix commit:** this round (follows `f4b5698`) · **Status: DEPLOYED & VERIFIED**
|
||
|
||
The mobile quiz (≤700px) had a pinned/sticky `#btn-next` that sat on top of the last
|
||
option row (up to 100% of the last `.opt` covered) and the page itself scrolled
|
||
(document 100–306px past the viewport, so the CTA floated over content at every
|
||
document scroll position). Fix: app-shell the quiz screen — `#step-body` becomes the
|
||
only scroll container; `#btn-next` + `#panel` become static flex children of a fixed
|
||
bottom cluster. CSS-only, zero `app.js` changes.
|
||
|
||
## Root cause
|
||
|
||
Round 3 kept `#btn-next` `position:sticky` (anchored above `--sheet-h`) and `#panel`
|
||
`position:fixed` at ≤700px. Sticky-in-scrolling-document means the CTA hovers over
|
||
whatever content scrolls beneath it — on the province step (13 two-column options) the
|
||
last row sat exactly under the CTA band. Document-level scroll (`html`,`body` height
|
||
100vh with `overflow:hidden` only on `body`) plus `.opt` min-height growth in FR made
|
||
it worse. No geometry change made "pinned above content" and "never overlaps options"
|
||
coexist — the quiz needed a real app-shell.
|
||
|
||
## The change — `site/style.css`
|
||
|
||
1. **Reverted** round-3 FR rule `html[lang="fr"] .opt{font-size:13px;padding:12px 4px}`
|
||
(+ its comment). FR `.opt` rows are back to the base 15px / 14px 10px, identical
|
||
metrics to EN. Fix `f4b5698` (FR end-anchor in app.js) untouched.
|
||
2. **New `@media(max-width:700px)` block appended at end of file** (all of it inside
|
||
the media query; desktop untouched):
|
||
|
||
```css
|
||
#scr-quiz.scr{display:flex;flex-direction:column;height:calc(100vh - 69px);height:calc(100dvh - 69px);overflow:hidden;padding:0}
|
||
#scr-quiz.scr[hidden]{display:none}
|
||
.quiz-wrap{display:flex;flex-direction:column;flex:1;min-height:0}
|
||
.quiz-main{display:flex;flex-direction:column;flex:1;min-height:0}
|
||
#step-body{flex:1;overflow-y:auto;min-height:0;padding:10px 12px 16px}
|
||
#btn-next{position:static;margin:10px 12px 12px}
|
||
#panel{position:static;margin:0 8px calc(8px + env(safe-area-inset-bottom,0px))}
|
||
main:has(#scr-quiz:not([hidden])){padding-bottom:0}
|
||
```
|
||
|
||
The six spec rules are verbatim. Three deliberate additions, all disclosed in
|
||
"Deviations" below: placement at EOF, the `[hidden]` guard, and the scoped
|
||
`main:has(...)` padding rule.
|
||
|
||
## Geometry — province step, scroll 0 (CSS px)
|
||
|
||
`covLast%` = % of the LAST option row covered by the CTA (visible-true, clipped to
|
||
the scroll container); `docOver` = document scroll overflow px.
|
||
|
||
| device/lang | BEFORE covLast% | BEFORE docOver | AFTER covLast% | AFTER docOver | next t–b | panel t–b |
|
||
|---|---|---|---|---|---|---|
|
||
| iphone14 390 en | 58.4 | 119 | **0.0** | **0** | 536.3–586.3 | 618.3–836 |
|
||
| iphone14 390 fr | 100 | 100 | **0.0** | **0** | 519.3–569.3 | 601.3–836 |
|
||
| pixel 360 en | 81.8 | 163 | **0.0** | **0** | 492.3–542.3 | 574.3–792 |
|
||
| pixel 360 fr | 58.8 | 176 | **0.0** | **0** | 475.3–525.3 | 557.3–792 |
|
||
| se 320 en | 83.3 | 297 | **0.0** | **0** | 375.3–425.3 | 457.3–692 |
|
||
| se 320 fr | 62.2 | 306 | **0.0** | **0** | 375.3–425.3 | 457.3–692 |
|
||
|
||
Pixel proof, same region (old CTA band, 390 FR province, scroll 0): 95.2% CTA-blue
|
||
px before → 6.9% after (option grid visible).
|
||
|
||
## Acceptance (spec §acceptance) — all PASS, local + live
|
||
|
||
Battery: `audit/appshell_verify.py` — 254 checks per environment, at 320/360/390 ×
|
||
EN+FR, province (13 `.opt`) + vision (12 chips) at scroll 0/mid/max, plus full walks.
|
||
|
||
- **Local: 254/254 APPSHELL-VERIFY-OK** · **Live: 254/254 APPSHELL-VERIFY-OK**
|
||
- A1/A2 CTA fully visible (top ≥ header bottom, bottom ≤ panel top, inside viewport)
|
||
and zero CTA↔`.opt` visual overlap (rects clipped to `#step-body`): PASS at every
|
||
scroll position, all devices/langs, both steps.
|
||
- A2 CTA↔panel: `next.bottom ≤ panel.top` with a 32px air gap everywhere; PASS.
|
||
- A3 last `.opt` fully visible at max scroll (`lastOpt.bottom ≤ stepBody.rect.bottom−4`):
|
||
PASS on 12/12 device/lang/step combos, local and live.
|
||
- A4 no document scroll: `scrollHeight ≤ innerHeight + 1` — 12/12 walks cover **all
|
||
14 single / all 17 couple steps** at 320/360/390 EN+FR, max docOver **0**.
|
||
- A5 panel fully visible (panel.bottom ≤ viewport), 4 chips + insight readable,
|
||
sheet expand/scrim/scrim-tap-collapse flow verified at all 3 widths.
|
||
- A6 Back + "i / n" counter + progress bar visible on every check.
|
||
- A7 tests: engine **36/36**, i18n **48**, smoke **56** (run after CSS change).
|
||
- A8 desktop 1280×900 EN+FR province + vision/tall + results: baseline
|
||
(`audit/appshell-baseline/`) vs post-change (`audit/appshell-desktop-after/`):
|
||
**6/6 identical, 0 px diff.**
|
||
- A9 intro/results 390 full-page EN+FR before vs after: **0 px diff** (4/4).
|
||
- A10 deploy `DEPLOY-OK`; served `style.css` byte-identical to local; origin marker
|
||
`retire-planner v1` count = 1; off-LAN (Midhurst troublebox) `OFFLAN-OK`:
|
||
200 + marker + app-shell rules present + FR hack gone (`audit/appshell-geo-check.json`).
|
||
|
||
### Desktop "pixel-identical" footnote
|
||
|
||
One live re-run of `desktop-en-results` differed from baseline by 25 px at
|
||
channel-delta ≤ 1/255 (0.0022%, bbox on the vision-image rounded-corner AA edge).
|
||
Determinism probe: two consecutive screenshots of the *same live page, same CSS*
|
||
jittered by the same 25 px between themselves, while each of those matched the
|
||
baseline 0 px (`audit/appshell-desktop-live/desktop-en-results-rerun.png` = baseline
|
||
exactly). Run-to-run rasterizer jitter, not a CSS effect — every changed rule is
|
||
inside `@media(max-width:700px)` and cannot evaluate at 1280px.
|
||
|
||
## Deviations from spec (disclosed)
|
||
|
||
1. **Block placement — appended at EOF, not inside the existing 700px block.**
|
||
The base rule `#step-body{…min-height:320px;padding:18px}` sits *later in the
|
||
file* than the existing `@media(max-width:700px)` block, and media queries add no
|
||
specificity — placed there, `min-height:320px` beat `min-height:0`, the shell
|
||
overflowed, and the CTA overlapped the panel at 320px. Spec process step 1 says
|
||
"append **one new media block**" and note 3 requires "later in file wins", so the
|
||
block goes after all base rules (just before `@media print`). The six rules are
|
||
verbatim.
|
||
2. **Added `#scr-quiz.scr[hidden]{display:none}`.** `display:flex` at id+class
|
||
specificity beats the UA `[hidden]{display:none}`, so the hidden quiz shell was
|
||
rendering on intro/results pages (+775px of dead shell, caught by full-page pixel
|
||
diff). This one-line guard restores spec intent ("intro/results must not change");
|
||
intro/results are now pixel-identical.
|
||
3. **Added `main:has(#scr-quiz:not([hidden])){padding-bottom:0}`.** `main`'s 12px
|
||
bottom padding sits below the exactly-viewport-tall quiz section and added 12px of
|
||
document scroll on every quiz step (acceptance A4). Scoped with `:has()` to the
|
||
quiz screen (spec note 2 explicitly sanctions this pattern); intro/results
|
||
unaffected (proven by A9, 0 px diff, 4/4 full-page).
|
||
4. **No `app.js` changes** — spec allows ("if any JS change is needed, stop and
|
||
report; prefer CSS-only"). The old `--sheet-h`/`syncSheetH` JS keeps running; the
|
||
sticky `#btn-next` rule at ≤860px and fixed `.panel` rule are overridden by the
|
||
later static rules (spec note 5: layering leftovers harmless).
|
||
|
||
## Evidence files
|
||
|
||
- `audit/appshell-baseline/` — 6 desktop PNGs (pre-change, == deployed round-3, byte-check `live == HEAD` done pre-deploy).
|
||
- `audit/appshell-before/` — mobile PNGs + `geo-before.json` (defect: FR@390 last row 100% covered, docOver 100–306px).
|
||
- `audit/appshell-after/` — post-change mobile PNGs (320/360/390 × EN+FR, province/vision/maxscroll/sheet-open/intro/results), `geo-after.json`, `geo-live.json`, `verify-after.json`, `verify-live.json` (254/254 both).
|
||
- `audit/appshell-desktop-after/`, `audit/appshell-desktop-live/` (+`-rerun.png` determinism shot) — desktop PNGs, 0 px vs baseline.
|
||
- `audit/appshell-geo-check.json` — off-LAN check (200 + markers, `OFFLAN-OK`).
|
||
- Scripts: `audit/appshell_capture.py`, `audit/appshell_verify.py`, `audit/appshell_pixdiff.py`, `audit/appshell_offlan.py`.
|