120 lines
7.4 KiB
Markdown
120 lines
7.4 KiB
Markdown
# 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`**
|