8.4 KiB
HEADER-LANG-RIGHT REPORT — EN|FR toggle right-aligned in .hdr-in (top-right of page)
Run: 2026-10-09, DSH. Spec: /home/ubuntu/projects/.hermes/plans/2026-10-09_header-lang-toggle-rightalign.md (v1).
Baseline: main @ 4c8fcad (PS-pension stage B + Hermes post-deploy verification), tree clean, tests green.
Result: SHIPPED — deployed, origin + off-LAN verified, committed and pushed.
1. The change
One added line in site/style.css (top level, directly after the existing .lang-toggle rule at line 27):
.lang-toggle{order:99;margin-left:auto} /* right-align the EN|FR toggle inside .hdr-in ... */
No DOM, JS, or other-CSS changes. git diff site/ = 1 insertion, 0 deletions.
Deviation from the spec's literal rule (measured, justified)
The spec preferred .lang-toggle{order:99} alone. Applied literally, it failed the spec's own
acceptance 1 in 18/24 cases: order reorders flex items but .hdr-in is left-packed (no
justify-content), so the toggle became the rightmost element yet sat flush against Start over,
order:99 alone → gap (toggle.right → hdrIn.right−padR) 188.1–467.6px on desktop, 45.6–160px on mobile
(full matrix in audit/lang-right/geo-literal-order99.json — re-measured post-hoc against a site copy
carrying the literal rule; the after/ dir is the shipped rule).
The spec's DOM-move fallback fails the same ≤2px criterion for the same reason (a last child packs
against the previous item). The spec's "order misbehaves" escape clause covers this:
added margin-left:auto on the toggle itself — the auto margin absorbs the free space so the toggle
sits flush at the header's right padding edge. The spec only forbade margin-left:auto on the
pill/logo ("leave the other elements left-packed exactly as today") — untouched, verified:
all non-header-strip pixels on desktop are bit-identical (section 4).
Resulting visual order = exactly the spec's drawing: [logo] [pill] [Start over] ………… [EN|FR] desktop;
[logo] [Start over] …… [EN|FR] at ≤700px.
2. Geometry — acceptance 1/2/3 (24 cases: 320/360/390/1280 × EN/FR × intro/quiz/results)
Measured with getBoundingClientRect via Playwright (audit/lang_right_capture.py); gap =
(hdrInRect.right − paddingRight) − toggleRect.right, spec tolerance ≤2px.
| case group | gap BEFORE | gap AFTER | rightmost AFTER | other elements past toggle-left AFTER |
|---|---|---|---|---|
| desk 1280×900 (intro/quiz/results × EN/FR) | 733.7–807.4px | 0.0px | ✓ 6/6 | none (6/6) |
| iphone14 390 | 95–160px | 0.0px | ✓ 6/6 | none |
| pixel 360 | 65–130px | 0.0px | ✓ 6/6 | none |
| se 320 | 25–112.5px | 0.0px | ✓ 6/6 | none |
- A1 ✓ every case: gap 0px (≤2px), toggle is the rightmost visible
.hdr-inchild, no other element's rect extends past the toggle's left edge. Computedorder: toggle 99, all siblings 0. - A2 ✓ header height unchanged: 56px at 320/360/390 (≤56 required) and 66px at 1280, identical
before and after (the
.hdr-in{flex-wrap:nowrap;padding:5px 10px}mobile rule keeps holding). - A3 ✓ at 320px:
scrollWidth == clientWidth == 320(no overflow), zero wrap at every width (center-Y row test),elementFromPointcenter hit-tests pass for#lang-en,#lang-frand#btn-restarton every screen where they are shown (restart ishiddenon intro by design). Logo continuity: EN logo fits 320px (124.6px wide, not truncating — identical to before); FR "Planificateur de retraite" truncates on quiz/results at exactly 102.1px withtext-overflow:ellipsis(ellipsisPossible: true), same as before.
3. Toggle behavior — acceptance 4 ✓
In-browser click test at 390px (toggle-click block in both geo JSONs, before and after identical):
- FR click →
documentElement.lang === 'fr', title → “Planificateur de retraite — la retraite canadienne, modélisée comme il faut”,#lang-enaria-pressed=false,#lang-frtrue. - EN click → back: lang
en, original title, aria-pressed true/false.frOk: true, backOk: true.
4. Screenshots + desktop regression — acceptance 5 & 7 ✓
24 screenshots per version ({se,pixel,iphone14,desk}-{en,fr}-{intro,quiz,results}.png) in
audit/lang-right/before/ and audit/lang-right/after/; self-jitter pairs in before2//after2/.
PNGs are gitignored (ps-pension precedent); files on disk for Hermes re-verification; geo JSONs are committed.
Jitter-controlled pixdiff (audit/lang_right_pixdiff.py, PIL ImageChops):
- Self-jitter: before vs before2 and after vs after2, 1280×900 intro/results EN+FR: 0 px, bit-identical both times → capture pipeline adds no noise; cross-version diffs are trustworthy.
- Cross-version before↔after: every diff bbox sits inside the header strip (desk: bbox y 10–56, header 66px; 0 changed px below y=66). Mobile re-checked with DPR-scaled masks (56 CSS-px header → 114/157/171 device-px at dsf 2/2.75/3): 0 px below the header strip at every device — page body pixel-identical below the header on all 24 before/after pairs.
5. Tests — acceptance 6 ✓ (green at preflight and after the change)
| suite | result |
|---|---|
node tests/engine.test.js |
36 pass, 0 fail |
node tests/i18n.test.js |
I18N-OK (48) |
node tests/ui.smoke.js |
SMOKE-OK (92) — spec said 56; the suite has grown since spec time (PS rounds), gate is “green”, met |
node tests/ps-pension.test.js |
70 pass, 0 fail |
6. Deploy — acceptance 8 ✓
bash scripts/deploy.sh→ DEPLOY-OK- Origin (
http://192.168.0.123:8103/): marker count 1; new rule present 1×; servedstyle.cssbyte-identical to local. - Off-LAN (Midhurst troublebox,
audit/lang_right_offlan.py→audit/lang-right-geo-check.json): 200 + marker ✓ + served CSS carrieslang-toggle{order:99;margin-left:auto}✓ → OFFLAN-OK (2026-10-09T17:31:08Z).
7. Files
site/style.css— the one-line fix (the only site change).audit/lang_right_capture.py— capture + acceptance 1–4 harness (A3 gates: ellipsis-clean truncation when present; hidden#btn-restarton intro is not a hit-test failure).audit/lang_right_pixdiff.py— header-strip-masked pixdiff + self-jitter.audit/lang_right_offlan.py— off-LAN troublebox check.audit/lang-right/{before,before2,after,after2}/— screenshots (on disk; PNGs gitignored),geo-before.json/geo-after.json(full 24-case measurements + toggle-click test),geo-before-order99.json(literal-spec-rule evidence).audit/lang-right-geo-check.json— off-LAN proof.
8. Commit
main: site/style.css one-liner + audit scripts/JSONs, pushed to origin/main.
9. Independent verification (Hermes, 2026-10-09) — 50/50 PASS
Re-ran the acceptance battery independently (not trusting the audit):
- Toggle position at 320/360/390/1280 × EN/FR (results screen, seeded plan): flush right
(gap 10–12px = header padding), rightmost
.hdr-inchild, vertically inside the header band, at every width/language. Pre-fix baseline for contrast: toggle sat at x 273–351 vs header content edge 1170 at 1280. - Header height unchanged:
.hdr-in= 66px desktop / 56px mobile, identical pre/post. - No horizontal overflow at any width (320 included).
- Toggle still switches language: EN→FR→EN round-trip on a seeded results page, plan text changes and restores exactly.
- App-shell intact at 390/320: CTA below the scroll area, no option/CTA overlap.
- Pixel-diff vs pre-fix full-page baselines (en/fr × 1280/390/320, captured before the deploy): identical below the header strip (0 strongly-differing pixels) — the change is confined to the header.
- Tests green: engine 36/36 · i18n 48/48 · ps-pension 70/70 · smoke 92/92.
- Off-LAN OK (marker + rule served from outside the LAN).
- Vision-checked header strips at 1280 (EN), 390 (FR), 320 (EN): toggle top-right, isolated, active language highlighted, other header items unchanged.
One pre-existing nit (NOT a regression): at 390px the French brand title ellipsizes
("Planificateur de re…") because .logo is flex:0 1 auto with text-overflow:ellipsis and the
FR title is long. Verified pre-existing: the pre-fix baseline screenshot shows the same
truncation, and the logo width is identical pre/post-fix at 360/320 (142px/102px). Out of scope
for this change; optional follow-up = shorten the FR brand string or relax the ghost button's
padding at ≤420px.
HEADER_LANG_DONE