# ROUND 4 FIX: quiz-screen app-shell (mobile) — pinned CTA must never cover content **Spec v1 — written 2026-10-09 by Hermes. Execution: DSH, all stages, zero questions.** Supersedes the round-3 FR compact-button hack (revert it). The retire-label anchor fix (commit `f4b5698`) stays. ## Defect (measured by Hermes on the live site, 2026-10-09) The bilingual refactor made step headers taller. On the province step the pinned sticky CTA (`#btn-next`, `position:sticky; bottom: calc(var(--sheet-h) + 14px)`) now floats over the last row of the option grid at initial scroll: | viewport | lang | #btn-next rect | last row (Nunavut) rect | coverage | |---|---|---|---|---| | 390×844 | fr | [564, 614] | [570, 613] | **~92% covered** | | 390×844 | en | [564, 614] | [589, 632] | ~25px of top covered | | 360×800 / 320×700 | both | worse | worse | see verify_round3.py output | Root cause: the CTA is sticky-pinned above the fixed panel and floats over whatever content sits at that viewport position when the option grid is taller than the space above the fixed cluster. The grid's position is fixed by the content above it, so padding/font tweaks cannot fix all breakpoints. The correct fix is an **app-shell layout**: the quiz screen becomes a flex column whose only scroll container is `#step-body`; the CTA and the panel become flex children of a fixed bottom cluster and can never overlap content at any scroll position, width, or language. ## Current DOM (measured — do not restructure, it is already perfect for this) ``` body header.hdr (h=57, sticky top) main section#scr-quiz.scr (top=69, class "scr" carries the mobile padding rule) div.quiz-wrap div.quiz-main div.prog (h=6) div.step-nav (sticky top, h=56 — Back + "1 / 14") div#step-body (step content: title, sub, .opts) button#btn-next.cta (sticky bottom ≤700px, h=50) aside#panel.panel (position:fixed; bottom:8px, h=218 — sibling of .quiz-main) ``` **The fix is CSS-only.** `#step-body` already exists as the content wrapper; `#btn-next` and `#panel` are already siblings in the right places. No app.js DOM changes. (If you find you need a one-line app.js tweak, keep it minimal and say so in the report.) ## Required changes (all inside `@media(max-width:700px)`, appended after the existing block) ```css /* Round 4: app-shell the quiz screen — #step-body is the only scroll container; #btn-next + #panel form the fixed bottom cluster. Never overlaps content. */ #scr-quiz.scr{display:flex;flex-direction:column;height:calc(100vh - 69px);height:calc(100dvh - 69px);overflow:hidden;padding:0} .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))} ``` Notes / requirements: 1. `height:calc(100vh - 69px)` then `height:calc(100dvh - 69px)` (dvh override for mobile browser chrome). 69px = 57px header + 12px offset above `#scr-quiz` (measured). If the 12px comes from `main` padding, that padding still applies — fine, the calc already accounts for it. 2. **No document scroll on the quiz screen**: after the change, at 320/360/390, `document.documentElement.scrollHeight <= window.innerHeight + 1` on EVERY quiz step (all 14 single / 17 couple steps — walk them). If `main`'s padding-bottom or anything else leaves a few px of document scroll, fix it (e.g. `main{padding-bottom:0}` at ≤700px) WITHOUT changing the intro or results screens visually (verify with screenshots). 3. `#btn-next{position:static}` — the sticky rule at line ~414 (`#btn-next{position:sticky;bottom:calc(var(--sheet-h) + 14px + …)}`) must be overridden (your later rule wins; same specificity, later in file). The CTA must be fully visible at ALL times: rect within [header bottom, panel top]. 4. `#panel{position:static}` — the fixed rule (`#panel{position:fixed;bottom:8px;…}`) must be overridden at ≤700px. The panel becomes the last flex child of `.quiz-wrap`. Keep its visual inset identical (8px sides/bottom + safe-area). The panel's internal `.panel-h` sticky rule becomes inert — harmless, leave it. 5. `#step-body` gets the horizontal padding the `.scr` rule used to provide (`padding:10px 12px 16px`). `#scr-quiz.scr`'s own padding goes to 0 (it is `padding:10px 12px calc(var(--sheet-h) + 14px)` today — your `#scr-quiz.scr` id+class selector beats it). 6. `--sheet-h` / `syncSheetH` become unused at mobile — LEAVE the JS running (harmless), do not rip it out. 7. **Revert** the round-3 rule `html[lang="fr"] .opt{font-size:13px;padding:12px 4px}` (and its comment) from the ≤700px block — superseded; EN and FR option buttons return to identical sizing. 8. Keep everything else: sticky header, `.prog`, `.step-nav` (Back + counter, still pinned by flex now — its sticky rule is inert inside the non-scrolling `.quiz-main`), scrim, sheet expand, swipe hints, chartbox scroll on results (results screen is NOT app-shelled — only `#scr-quiz`). 9. Desktop (>700px) must be byte-identical: all changes are inside `@media(max-width:700px)`. Prove it: screenshot the desktop quiz (province step + a tall step + results) at 1280×900 in EN and FR BEFORE and AFTER — pixel-diff must be 0 (or explain any diff). ## Acceptance (DSH verifies; Hermes re-verifies independently after) At 320/360/390, EN and FR, on the province step AND the interests step (tall): 1. `#btn-next` fully visible (rect.top ≥ header bottom, rect.bottom ≤ panel top) at scroll positions 0 / mid / max of `#step-body`. 2. `#btn-next` never overlaps `#panel` (next.bottom ≤ panel.top + 1) or any `.opt`. 3. At max scroll of `#step-body`, the last `.opt` is fully visible inside the scroll area (lastOpt.bottom ≤ stepBody.rect.bottom − 4). 4. No document scroll on any quiz step (see 2 above). 5. Panel fully visible; 4 chips + insight readable; sheet expand/scrim still works (tap panel-h, scrim appears, tap scrim, collapses). 6. Back button + "1 / 14" counter visible at top; progress bar visible. 7. Tests green: engine 36/36, i18n 48, smoke 56. 8. Desktop 1280×900 EN+FR quiz + results screenshots pixel-identical to the pre-change baseline (baseline taken by Hermes 2026-10-09 into `audit/appshell-baseline/`). 9. Deploy `DEPLOY-OK`, origin marker = 1; off-LAN (Midhurst troublebox) 200 + marker. 10. Save before/after screenshots + measurements to `audit/appshell-{before,after}/`. ## Process 0. Preflight: clean tree on top of `f4b5698`; tests green; take the desktop baselines yourself too (1280×900 EN+FR quiz province + interests + results) into `audit/appshell-baseline/` (Hermes also keeps copies in scratch — compare against yours). 1. Apply the CSS (one new media block + revert the FR rule). CSS-only. 2. Tests + local verification battery (acceptance 1-7 at 320/360/390 EN+FR). 3. Deploy + origin/off-LAN checks + screenshots → `audit/appshell-after/`. 4. Commit + push; write `QUIZ-APPSHELL-REPORT.md` (root cause, geometry before/after, acceptance evidence, desktop pixel-diff result, any deviation). 5. If anything fails twice, stop, keep the last good deploy live, report the blocker. **Your FINAL output line must be exactly: `QUIZ_APPSHELL_DONE`**