8.0 KiB
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
- Reverted round-3 FR rule
html[lang="fr"] .opt{font-size:13px;padding:12px 4px}(+ its comment). FR.optrows are back to the base 15px / 14px 10px, identical metrics to EN. Fixf4b5698(FR end-anchor in app.js) untouched. - New
@media(max-width:700px)block appended at end of file (all of it inside the media query; desktop untouched):
#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↔
.optvisual overlap (rects clipped to#step-body): PASS at every scroll position, all devices/langs, both steps. - A2 CTA↔panel:
next.bottom ≤ panel.topwith a 32px air gap everywhere; PASS. - A3 last
.optfully 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; servedstyle.cssbyte-identical to local; origin markerretire-planner v1count = 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)
- 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:320pxbeatmin-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. - Added
#scr-quiz.scr[hidden]{display:none}.display:flexat 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. - 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). - No
app.jschanges — spec allows ("if any JS change is needed, stop and report; prefer CSS-only"). The old--sheet-h/syncSheetHJS keeps running; the sticky#btn-nextrule at ≤860px and fixed.panelrule 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-checklive == HEADdone 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.pngdeterminism 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.