fix: header reflows under enlarged text; reopen the step-1 nowrap decision

The step-1 header decision is formally reopened (AGENTS.md (ak)): its record
attributed the residual to the wrong cause and read a measured 944px functional
failure as a robustness margin.

Mechanism — wrapping, plus a gated sticky offset:
- `flex-wrap: nowrap` removed from `.header-inner` (measured necessary AND
  sufficient) and from `.nav-list` (measured inert; removed so the prohibition
  is not contradicted by a `nowrap` in the same file), with a dead `flex: none`.
- `inset-block-start` becomes a two-band, two-term saturating clamp() so the
  header is sticky only while the masthead is one row. A media query cannot
  express this: its `rem` resolves against the browser DEFAULT font size, a
  property's against the root element. The second term catches a root BELOW the
  default, where the 80rem content cap shrinks faster than the header's px
  minimums; without it 65px of `#main` sat behind the header at 9px.
- Wrapping is the only mechanism that reflows under all THREE enlargement paths,
  because Chrome's minimum-font-size setting is invisible to @media, to
  @container and to every length unit.

Measured, 22 pages x 16 widths = 352 page-widths per method:
  root-style 32px        175/352 -> 0/352   (1280px: 944px overflow -> 0)
  minimum-font-size 32   219/352 -> 88/352  (residual is the footer email)
  default-font-size 32     0/352 -> 0/352
  default (root 16)        0/352 -> 0/352
Nav items and CTA on-screen in 1408/1408. A further 762 points across roots
9-32, both thresholds, the band seam and all five Chrome presets: 0 failing.

Normal-settings identity: 0 differences on 352 page-widths across six metrics,
with six nav items and with a seventh injected. Header 81.00px at all eight
widths >= 1056, CTA gap 0.00px. Lighthouse: 22 pages, no category below 95,
CLS 0.000 on every page.

--header-h is reworded as a FLOOR, not a constant; value unchanged.

Also in this step, per ruling:
- /bio/ print `font-weight` frozen at 400 — the circulated PDF's typography
  changes only when its content is deliberately revised, never as a side effect
  of a screen refactor. Declaration byte-identical; the constraint is recorded.
- CLAUDE.md: the two-simulation rule for enlarged text, the zsh
  no-word-splitting rule, the third (minimum-font-size) mechanism, and
  "state the grid with the count".

Two rounds of adversarial-reviewer, eleven findings, all resolved; round 2's
blocking finding was a defect in round 1's own fix. Two suggested fixes declined
with reasons in (ak). claims-auditor deliberately not run — D20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-09-01 12:34:50 -04:00
co-authored by Claude Opus 5
parent 0f7595b602
commit fce89d46eb
8 changed files with 731 additions and 125 deletions
+172
View File
@@ -931,6 +931,7 @@ never being raised again.
| R17 | **THE DEPLOY KEY'S FIRST ROTATION IS DUE 2026-11-26, AND THAT DATE IS THE WHOLE ROW.** Access key for `adr-sml-deploy` created **2026-08-26**; D3 commits to **quarterly** rotation. Pouya's instruction when Q22 closed: *"Put the DATE in the R-series reminder, not the intention."* **Rotation procedure is in `docs/06-deployment.md` §Key rotation** — create the second key, update the Gitea secrets, verify a deploy, **then** delete the first; never delete before verifying, or the next push fails with no way to authenticate the fix. **Then set the next date here (2027-02-26) rather than deleting the row.** Also re-run **all nine** `simulate-principal-policy` checks after any policy edit (they are listed and reproducible in `docs/reference/deploy-credential-verification.md` §5) — the narrowness is the control (§10), and a widened policy is invisible from inside the repo. **The key is currently UNUSED — `AccessKeyLastUsed` returns `ServiceName`/`Region` = `N/A` and no `LastUsedDate`** — because deploys still run locally via `npm run deploy`; that does not pause the clock, and an unused long-lived key is exactly the one nobody remembers to rotate | 2026-08-28 | This is the reminder D3 has been waiting for a subject since 2026-08-26. §10 records the reason it matters more here than on a normal project: the secret lives on **jointly-administered** infrastructure, and the account also holds another business's client-database backups. A static key on shared infrastructure is only as safe as its age and its scope, and **nothing in the repo, the build, or AWS will prompt anyone on either.** §7 carries the inventory; this row carries the date |
| R18 | **THE SIX `docs/reference/` EXTRACTS BEHIND `/practice/*` ARE DATED 2026-08-29, AND SIX SHIPPED SENTENCES TURN ON FACTS THAT MOVE.** Build step 5 put statute, regulation, tribunal and bill status onto public pages — sourced, but **sourced as at one day**. The volatile ones, in order of how fast they move: **(a) federal Bill C-36** — `/practice/technology/` says it *"was introduced in June 2026 and was at second reading when this page was written"*; if it receives royal assent the page is wrong about the most load-bearing fact on it. **(b) the Ontario regulation under `Electricity Act` s. 28.1** — `/practice/energy/` says it *"had not been made as of August 2026"*. **(c) the ERO 026-0853 consultation**, comment period to **12 September 2026**. **(d) the Tribunals Ontario annual report** — `/practice/insurance/` publishes FY2024-25 figures and the extract records that a 2025-26 edition was never ruled out. **(e) the SABS**, amended with effect 1 July 2026. **(f) the ADRIC National Mediation Rules**, under review by ADRIC's own committee. **(g) ADRIC's Code of Ethics** — added 2026-08-30, build step 6. `/process/` §Confidentiality quotes it verbatim from `docs/reference/adr-institution-names.md` (retrieved 2026-08-29) **with a live link to ADRIC's page**, which is what makes it checkable and also what makes a stale quotation visible. It is the slowest-moving item here — a professional code, not a bill — so it does not change the cadence; it is listed because the trigger below says "all six" and there are now seven. **THE TRIGGER: re-check all seven extracts before any cutover, and before any republish that turns on one of them — the same event trigger R10 uses.** Then re-stamp the extract. **A page that was true when it was written and is false when it is read is still a false page**, and this is the first change set on the project to put that class of fact into public copy at volume | 2026-08-29 | Six sentences, six files, one retrieval date. Nothing here fires on its own; a fact with a shelf life and no owner is exactly what §12 exists for |
| R19 | ⚠️ **DYNAMODB TTL BACKS A PUBLISHED PRIVACY PROMISE AND `/legal/privacy/` ASSERTS THE MECHANISM, NOT JUST THE PERIOD.** **§7 records the status and its stamp; this row deliberately does not restate it** — one place for a service status, or the copy that goes stale is the one nobody re-reads. **THE TRIGGER, and its two halves are not interchangeable: re-run `describe-time-to-live` and confirm `ENABLED`, THEN write a record with a near-future `ttl` and confirm it actually disappears.** `ENABLED` proves the setting; only the test record proves the behaviour. Writing the attribute proves neither — the handler's side is verified and is not what this row is about. Both halves are on `docs/06`'s cutover checklist and the question is §9 Q60. Re-stamp §7 the same day, **and when you do, sweep for the copies: this fact reached five files outside §7 in one change set and had to be pulled back.** Close this row only when the test record has been observed to vanish | 2026-08-31 | **This is R9's exact shape at higher stakes.** R9 exists because the SES alarms are configured and notify nobody until one subscription is confirmed — a control that reads as covered and is not. Here the control backs a **statement to the public on a privacy policy**, which is the one class of claim this project treats as unrecoverable, and the failure is silent in both directions: nothing in the repo, the build or AWS reports that records are accumulating forever. A cutover checklist fires once; §12 is read aloud every session |
| R20 | ⚠️ **THE SEVENTH NAV ITEM ARMS TWO MEASURED HEADER DEFECTS, AND ITS TRIGGER IS A CONTENT EVENT RATHER THAN A DATE.** `SiteHeader` computes `showInsights` from the collection — Insights joins the masthead **automatically at two published articles** — so nothing in the build, the specs or a person's memory stands between publishing article #2 and arming both of these. With seven items **and fallback font metrics** (what a reader on `docs/04`'s Slow 4G profile sees during the `font-display: swap` window, at the DEFAULT text size, no reader setting involved) the header measures **141 px across a contiguous 10561091 px band** instead of 81 px: **(a)** it then collapses **60 px** when Geist swaps in, on all 22 pages, against the CLS < 0.05 budget; and **(b)** 141 px exceeds the 97 px `scroll-padding-top`, so "Skip to content" lands with **44 px of `#main` behind the sticky header** — and (b) is **new as of 2026-09-01**, the previous build's 86.97 px stayed under 97 px and covered 0. **THE TRIGGER: before publishing the second Insights article, re-measure the masthead with seven items under blocked webfonts, and rule.** The two candidate fixes are raising the desktop breakpoint past 1091 px — which changes the normal-settings layout in that band — or giving Geist a metric-matched `size-adjust` fallback; both are outside the scope the header step was given, and both close (a) and (b) together. **Why this is a §12 row and not only a `docs/06` line:** a cutover checklist fires once, at cutover, and this arms itself later, on an editorial decision taken by someone who will not be reading the deployment spec. `docs/02` §Reflow carries the measurements | 2026-09-01 | It is latent today and invisible from inside the repo: six nav items never wrap, so every check passes, and the defect appears the day a second article ships. That is R13's shape — a temporary state that becomes permanent because nothing prompts anyone — with the added twist that the prompt would have to fire on a content event. Raised by `adversarial-reviewer`, round 2 |
| ~~R7~~ | **RATIFIED / SUPERSEDED 2026-08-26.** (a) Cache-policy table matching the pipeline — **accepted**; documenting what the pipeline does beats documenting an intention. (b) `s3:AbortMultipartUpload` omitted — **accepted, reasoning corrected**: the lifecycle rule does not exist and is therefore not the cover; the actual cover is that `aws s3 sync` only goes multipart above 8 MB and the largest asset is a 357 KB portrait. Recorded in `docs/06-deployment.md` with a revisit trigger. (c) The `aws s3 ls` pre-flight — **superseded** by the variable guard now running as the workflow's first step | 2026-08-26 | — |
| ~~R8~~ | **PROMOTED TO A RULE 2026-08-26.** A reminder was too weak for a pattern that survived three entries. *A sweep is a command, not a claim* now sits in `CLAUDE.md` under Conventions, in `/build` Phase 6, and in `/wrap` step 3: any claim that a change was applied across files must cite the command and be written only after reading its output | 2026-08-26 | — |
@@ -938,6 +939,177 @@ never being raised again.
# Change Log
## 2026-09-01 (ak) — The large-text nav overflow is FIXED and the step-1 header decision is formally reopened; the masthead may wrap and is sticky only while it is one row; three enlargement mechanisms measured, one of them provably invisible to CSS — and round 1's own fix shipped a regression that only an intermediate text size could show
**Prior commits:** `64bce10` (entry (ai)'s tree, as-is) and `0f7595b` (entry (aj)'s five-item step). Tree was clean at the start of this session.
**Scope, set by Pouya:** *"the header and its consumers, plus the records named."* One code item plus records. Standard D19/D20: `adversarial-reviewer` per step, two-round cap, `claims-auditor` deferred to cutover.
---
### 0. The reopening, recorded as a reopening
Pouya reopened the **step-1 header decision**`flex-wrap: nowrap` on `.nav-list` above 66 rem and a fixed `--header-h` — in terms: *"its record attributed the residual to the wrong cause and characterized a measured 944 px functional failure as a robustness margin."* Ruling: **fix, do not accept.** Per the constitution the reopening is recorded here and **entry (u)'s original decision is not edited retroactively**; entry (aj), where the wrong attribution was written, likewise stands with this entry as the correction.
### 1. The mechanism — two declarations, and the second one took two attempts
**(a) The masthead may wrap.** `flex-wrap: nowrap` removed from `.header-inner` and `.nav-list` in the `@media (min-width: 66rem)` block, with the `flex: none` beside it.
⚠️ **WHICH OF THE TWO WAS THE CAUSE IS MEASURED, AND THE FIRST DRAFT OF THIS ENTRY GOT IT WRONG.** It said *"the pair was the cause, not either one."* Restoring each alone on the fixed build, root 32, `/about/` — document overflow / nav+CTA off-screen:
| restored above 66 rem | 1056 | 1216 | 1280 | 1440 | 1920 |
|---|---|---|---|---|---|
| neither (shipped) | 0/0 | 0/0 | 0/0 | 0/0 | 0/0 |
| `.nav-list { flex-wrap: nowrap }` only | 0/0 | 0/0 | 0/0 | 0/0 | 0/0 |
| `.header-inner { flex-wrap: nowrap }` only | 0/0 | 273/1 | 209/1 | 49/1 | 0/0 |
| both (= the previous state) | 649/3 | 928/5 | 944/5 | 784/4 | 304/1 |
**`.header-inner`'s was necessary and sufficient.** And row two is the honest part: **`.nav-list`'s removal is inert** — identical to shipped at every width, under a 200 % root size *and* under minimum font size, with six nav items and with seven. It stays removed for two reasons that are **not** "it fixed the overflow": the `nowrap` prohibition in the component would otherwise contradict a `nowrap` in the same file, and the override only re-stated `nowrap` over a base rule that already wraps.
**`flex: none` was dead, measured rather than reasoned.** `flex` is a flex-ITEM property and `.nav` computes `display: block` at 1056 / 1280 / 1920 px, so it set `flex-shrink: 0` on something that is not a flex item. Removing it leaves `.nav`'s and `.nav-list`'s geometry byte-identical; the only trace was the computed `flex-shrink`, 0 → 1. Its comment claimed it was what stopped the nav being squeezed, which it never was.
**(b) The masthead is sticky only while it is one row.** Wrapping makes the header **taller** — 244.59350.86 px at a 200 % root size against 81 px normally — and a sticky box that size covers `#main` after the skip link. The gate is a **property**, not a query, because in a property `rem` tracks the root element.
⚠️ **ROUND 1 WROTE `min(0px, calc(100vw - 66rem))`, AND IT SHIPPED A REGRESSION THAT ONLY AN INTERMEDIATE TEXT SIZE COULD SHOW.** `adversarial-reviewer` found it; independently reproduced: **20 of 300** (page × viewport × root) points left `#main` behind the header by 1069 px, across roots 1830 — while **root 16 and root 32, the only two sizes round 1 measured, were both clean.** Two distinct defects:
1. **It RAMPED where it had to STEP.** One pixel inside the threshold the term lifts the header one pixel, but the header has already gained a whole row. Wrapping is a step function; the response has to be one too.
2. **It used the wrong threshold in the wider band.** The tagline appears at 76 rem, and with it the one-row masthead fits only from **1207 px = 75.4 rem** — a number the step-1 binary search had already recorded. Gating that band on 66 rem fired far too late. **`/` measured clean throughout, and that is what identified the driver:** `/` is the one page that suppresses the masthead tagline.
⚠️ **AND ROUND 2 FOUND THE REPLACEMENT GATE WAS ONE-SIDED.** It could only fire when the root was *larger* than the default. **Chrome's "Font size" control has five presets — Very small 9 px, Small 12, Medium 16, Large 20, Very large 24 — and every sweep so far had used 16 and 32, and 32 is not one of them.** At 9 px the masthead wraps for the **opposite** reason: `--width-content` is `80rem`, so the content column shrinks to 720 px while the header's `min-inline-size: 44px` / `min-block-size: 44px` floors do not, and the row wraps at *every* viewport width. The header stood at **120 px against a 54.56 px `scroll-padding-top` — 65 px of `#main` behind it, on the 21 pages that render the tagline, at 1056 through 1920 px.** `/` measured 12 px, and that 12 px is **pre-existing**: the previous build is 67 px against the same 54.56 px on all 22 pages. So the wrap fix widened a pre-existing 12 px defect to 65 px.
**The shipped form is two bands × two terms** — each band gating on the width ITS layout requires, each term catching one direction: `clamp(-100vh, min(calc((100vw - 66rem) * 100000), calc((1rem - 16px) * 100000)), 0px)`, and the same with `76rem`. `1rem - 16px` is negative exactly when the root is below the CSS initial font size — the 16 px the whole rem scale is built on — and it also closes the root-style equivalent, where the previous form left the header sticky at 69 px against a 60.63 px offset at root 10. **The cost, measured and accepted:** a reader on "Small" (12 px) loses the sticky header, where it currently works — but only just, at 73 px against a 72.75 px offset, a **0.25 px** margin. Trading a 0.25 px margin for a provable gate is the right way round. Both thresholds are the existing layout breakpoints and both sit just above the measured one-row fit width (1047 px ≈ 65.4 rem without the tagline, 1207 px ≈ 75.4 rem with it), so the gate fires **at or before** the wrap. **At every normal size both evaluate to exactly `0px`**, because a matched `min-width: 66rem` guarantees `100vw ≥ 1056 px` and a matched `min-width: 76rem` guarantees `100vw ≥ 1216 px``100vw` counts a classic scrollbar and the query width does not, so the term can only be more positive.
⚠️ **The saturation leaves a transition band, and it is arithmetic rather than measured.** A finite factor is only a near-step, so the band is about (header `scroll-padding-top`) / factor ≈ **0.0007 px** of viewport width. CDP takes integer viewport widths, so it was not driven directly — it is bounded by arithmetic, and that is stated rather than dressed up as a measurement.
**No `inset-block-start: 0` fallback declaration, and that is a decision with a reason.** If `clamp()` were unsupported the declaration would drop, `inset-block-start` would compute to `auto`, and a sticky element with `auto` does not stick — graceful, not broken. But the property is the LOGICAL `inset-block-start`, and every engine that understands it also understands `clamp()`: Chrome 87 vs 79, Safari 14.1 vs 13.1. The only gap is **Firefox 6374**, an 18-month window ending April 2020, where the failure mode is "the masthead is not sticky". Dead code by this project's standard, so it is recorded here instead of written.
**Why a media query cannot express (b), and why a container query cannot either.** Measured:
| mechanism | `rem` in `@media` | `rem`/`em` in a property | `@container` `rem`/`em` | `getComputedStyle` root |
|---|---|---|---|---|
| `documentElement.style.fontSize='32px'` | 16 px | **32 px** | **32 px** | 32 px |
| `Page.setFontSizes({standard:32})` | **32 px** | **32 px** | **32 px** | 32 px |
| `--blink-settings=minimumFontSize=32` | 16 px | **16 px** | **16 px** | **32 px** |
1. **In a media query `rem` resolves against the browser's DEFAULT font size, not the root element's** — so raising the *default* moves the breakpoints with the type and the desktop masthead is never reached, while setting the *root* doubles the type and leaves the breakpoints where they were. That is the whole of entry (aj)'s reported disagreement between "944 px" and "0".
2. **A container query IS different: `@container` `rem`/`em` DO resolve against the root element.** Measured directly. That made a container query on `<body>` a real candidate, rejected because a property gets the same answer with no containment on the layout root and no `@container` support cliff.
3. ⚠️ **UNDER A MINIMUM-FONT-SIZE SETTING, `getComputedStyle(el).fontSize` AND THE `em` UNIT DISAGREE.** Blink floors the *reported* computed size to 32 px while `em` and `rem` keep resolving at 16 px: `scroll-padding-top: 6.0625em` computes to **97 px, not 194 px**; a `66em` box measures **1056 px, not 2112 px**; `min(0px, calc(100vw - 66em))` is **0px**. **No query and no length unit can see that mechanism — only used-value layout can.** That is why wrapping had to be the fix rather than a breakpoint of any kind, and why one residual below cannot be closed in CSS.
### 2. What it fixed — 22 pages × 16 widths (320 → 1920 px), four text settings, before and after on the SAME grid
| text setting | before | after |
|---|---|---|
| default (root 16) | 0 of 352 | **0 of 352** |
| root style 32 px | **175 of 352** | **0 of 352** |
| default font size 32 | 0 of 352 | **0 of 352** |
| minimum font size 32 | **219 of 352** | **88 of 352** — all of it the footer |
**Nav items and the CTA are on-screen in 1408 of 1408 measurements.** The wide-width figures that were the defect, root style 32 px, all now 0 of 22 pages: **1056 → 649 px, 1100 → 606 px, 1216 → 928 px, 1240 → 904 px, 1280 → 944 px, 1440 → 784 px, 1600 → 624 px, 1920 → 304 px.** Six of those eight columns are new — entry (aj) measured only 1280 and 1920, so it never showed that the failure ran the entire width of the sticky range. The same run under minimum font size was **414 / 370 / 854 / 830 / 830 / 750 / 670 / 510 px**, of which the record carried only the 830.
**And across the whole root-size space, which is the check round 1 skipped: 0 failing across 762 points** — roots 915 (70), roots 1632 × viewports 10561920 × three pages (300), the band seam at viewports 11801260 (198), both gate thresholds (144), and all five of Chrome's default-font-size presets (50). ⚠️ **`covered: 0` is also what a non-sticky header gives, so the seam was checked in the other direction too:** at the default size across 11801260 px the computed `inset-block-start` is `0px` and the header is still pinned at top 0.00 when scrolled, so stickiness is preserved rather than quietly lost — the failure mode a coverage-only probe cannot distinguish. **WCAG 1.4.4 is a requirement up TO 200 %, not AT 200 %**, and a two-point sweep cannot see a diagonal band.
### 3. The normal-settings identity check, which is what the constraint actually demanded
Same instrument, two artefacts: the previous commit built into a separate directory, the working tree built into `dist/`.
**0 differences across 352 page-widths on all five metrics** — full-page geometry fingerprint (every element's x/y/width/height), header height, CTA box, brand-name box, and skip-link landing. Header **81.00 px** at all eight widths from 1056 px up, brand on one line, nav on one line, CTA gap to `.header-inner`'s content edge **0.00 px**, `scroll-padding-top` 97 px, `#main` covered **0.00 px**.
**And the same with a seventh nav item** — Insights, which `showInsights` turns on at two published articles: **0 fingerprint differences and 0 header-height differences**, with all seven links sharing one line from 1056 px up, measured at 1024 / 1047 / 1056 / 1071 / 1100 / 1150 / 1200 / 1216 / 1240 / 1280 / 1440 / 1920 px. **So the `nowrap` was not load-bearing at any width above the breakpoint**; the 66 rem breakpoint is what keeps the row intact, and it was already measured against seven items at step 1.
**Boundary check:** `inset-block-start` computes to exactly `0px` at 1056 / 1057 / 1071 / 1072 / 1100 / 1280 / 1920 and the header pins at top 0.00 when scrolled; below 1056 it is `relative` and scrolls away. **Condense-on-scroll re-checked** (same media query): at 1280 the border and shadow still change on scroll with header height stable at 81 px, so no CLS; at 900 it correctly does not fire.
### 4. `--header-h` becomes a floor, and its consumer is re-verified
The token asserted *"one constant 81 px across every width where it is sticky"*, and **that sentence is what made a 944 px overflow read as settled.** It now records the header's height **at the default text size** — a floor.
**Its one consumer is `scroll-padding-top` in `global.css`, and `#main` is the only fragment target in the whole site** — `grep -roh 'href="#[^"]*"' dist` returns `href="#main"` 22 times and nothing else, and `/about/` carries exactly one `id`. Declaration (b) is what makes the floor safe where the gate can see the header exceed the token, so an offset that is too *generous* is harmless. ⚠️ **It does NOT make a short offset impossible** — §6's residuals (b) and (c) are the two cases the gate is blind to, and a first draft of this entry asserted the absolute. Measured after: **0.00 px of `#main` covered** at the default size and at a 200 % root size, on 22 pages × 16 widths, and across the 300-point root grid. No `min-block-size: var(--header-h)` was added to the header — it would never bind at any measured setting and would be dead code; the floor is enforced by the gate, not asserted by a declaration.
### 5. The tagline's recorded costs were invalidated by this change, and re-measured
The masthead tagline is held at 11 px on three measured costs. Wrapping changed two, so all three were re-measured at `--text-eyebrow` (14 px), on pages that render it, with six items and with seven:
| | before the wrap fix | after |
|---|---|---|
| document overflow, 7 items | **20 px at 1216, 4 px at 1280** | **0 at every width** |
| CTA past `.header-inner`'s content edge, 7 items | **67.8 / 43.8 / 51.8 / 51.8 px** | **0.00 px** |
| header height, 7 items | 84.98 px | **144.98 px at every width from 1216 up** |
**The two costs the record cited are gone, replaced by one that is larger and visible:** 64 px of header height on every page. Corrected in `SiteHeader.astro` and `docs/02`; the hold at 11 px stands on a better reason than it had. *(The width list said "1216 / 1240 / 1280 / 1440" for one revision — carried over from the superseded CTA measurement, which really did stop at 1440. Measured at 1216 / 1240 / 1280 / 1360 / 1440 / 1600 / 1760 / 1920: 144.98 px at all eight.)*
### 6. Three residuals, all carried to `docs/06`
**(a) The footer, under minimum font size only — out of the scope Pouya set, so measured and left.** 14 px at 320, 38 px at 1024, 30 px at 1056, 19 px at 1100; 0 from 1200 px up; all 22 pages. Cause: **`info@smlcompany.ca` has no break opportunity and needs 310 px inside a 224243 px footer-contact column**, and at 320 px the `.footer-col` box itself is 310 px in a 272 px content area. **The footer's OWN overflow is byte-identical before and after**`div.footer-grid` 78 px at 1056, 67 px at 1100 — though the 1056/1100 *document* figures did change, 414 → 30 and 370 → 19, because the header stopped contributing its own. *(A first draft said "all four figures byte-identical", conflating a document figure with a footer contribution.)* The 10241100 half looks like one `overflow-wrap: anywhere`; the 320 px half is a column-sizing question and is not the same fix, which is the second reason not to improvise it inside a bounded step. **No header element extends the document at any width under any mechanism.** *(The Practice dropdown panel's contents do overflow their own box by 84 px under that setting, but the panel sits inside a **closed** `<details>` and never extends the document — established by taking the furthest-right box rather than the largest content overflow.)*
**(b) The skip link under minimum font size — cannot be fixed in CSS.** The header stays sticky at **164.58270.56 px** there, so the skip link lands with **68174 px of `#main` behind it** on 22 of 22 pages from 1056 px up. **It was 1026 px before, so this pass widened it, and that is a real cost recorded rather than buried.** The trade is deliberate and the right way round: it bought the removal of **830 px of horizontal overflow with four nav items and the CTA unreachable** under that same setting — a vertical offset leaves every target reachable by scrolling; the horizontal case did not. Per §1 finding 3 no query and no length unit can detect the mechanism; the only candidates left are a constant fitted to the current nav item count or a behaviour change at normal settings, and the constraint was to stop and report rather than ship either.
**(c) NEW: fallback font metrics plus the seventh nav item — at the DEFAULT text size, no reader setting involved.** With every `.woff2` blocked — what a reader on the Slow 4G profile `docs/04` budgets against sees first, since the fonts are `font-display: swap` — and a seventh item cloned in, the header measures **141 px at every width from 1056 to 1091 px** — a contiguous **36 px** band — and 81 px from 1092 px up; with the webfonts loaded it is 81 px at all of them. Two consequences, and the record first carried only the smaller one: **(i)** a **60 px collapse** when Geist swaps in, on all 22 pages, against the CLS < 0.05 budget — *larger, not new*, since the previous build shifts ~5.6 px in the same band; and **(ii) 44 px of `#main` behind the sticky header** after the skip link, because 141 px exceeds the 97 px offset — **this half IS new**: the previous build's 86.97 px stayed under 97 px and covered **0**. Framing the case as "a CLS contribution" understated it. *(The band was also recorded as four widths ending at 1084 with "81 px from 1092 up", which reads as though 10861090 were checked and clear; they are all 141 px. Both errors found by `adversarial-reviewer`.)* **Latent, not shipped** — six items never wrap, and `showInsights` arms it at two published articles. Both candidate fixes — raising the desktop breakpoint past 1091 px, or a metric-matched `size-adjust` fallback for Geist — are outside "the header and its consumers".
**None is a new §9 question — D19.** None blocks a page, a claim or a deploy; all three block **cutover**, which is what `docs/06`'s checklist is for.
### 7. `/bio/` print weight — ruled, not awaiting a ruling
Pouya: *"The circulated PDF's typography changes only when its content is deliberately revised, never as a side effect of screen refactors."* The `font-weight: var(--weight-normal)` freeze in `bio.astro`'s `@media print` block is now a **live constraint** rather than a held decision, and `docs/02` carries the reasoning: the PDF is the one artefact the apparatus sees only when someone re-renders it (R16), so a screen change that reaches print is a defect by construction. **No re-render was needed**`public/pouya-lajevardi-bio.pdf` is untouched and the print declaration is byte-identical.
### 8. `CLAUDE.md` — two conventions, one corrected against its own dictation
- **"Two simulations of 200% text are not equivalent: media-query `rem` resolves against the browser default font size, not the root element."** ⚠️ **Pouya's wording says "both methods". There are THREE mechanisms, and the third defeats every CSS conditional** — so the line is written as dictated and then extended, because a rule that says "measure under both" would have missed the path that produced 219 of 352 failing page-widths and was the only one failing at 320 px and 1024 px. The block also now carries **"always state the grid with the count"**, because this session quoted totals of 220, 330 and 352 for the same claim and side by side they read as contradictions.
- **"zsh does not word-split parameter expansions; a loop over `$VAR` runs once — use command substitution or arrays, and assert the iteration count."**
### 9. Instrument findings — five, and every one produced a CLEAN-LOOKING result
Recorded in `docs/02` §Reflow as findings 46 beside entry (aj)'s three.
1. ⚠️ **A UNIFORM PASS FROM A TAUTOLOGY.** The skip-link probe reported `0.0 px of #main covered` on **946 of 946** page-widths, before and after, under every setting. `global.css` sets `scroll-behavior: smooth`, which makes `scrollIntoView()` **asynchronous**, so a synchronous read measures `scrollY = 0` — and at `scrollY = 0` the target sits exactly at the header's bottom edge, so `covered` is 0 **by construction**. Fixed with `behavior: 'instant'` plus an assertion that `scrollY` equals its predicted value; the probe then reported a **pre-existing 1026 px defect on 22 of 22 pages** the tautology had hidden.
2. ⚠️ **A RUNNING TRANSITION MAKES A PROPERTY READ RETURN THE OLD VALUE.** Forcing reduced motion — by CDP emulation *or* by `--force-prefers-reduced-motion` — made `documentElement.style.fontSize = '32px'` read back as **16 px** while the inline `style` attribute plainly said `font-size: 32px`. The reduced-motion block sets `transition-duration: 0.01ms !important` on `*`, which starts a transition on **every** property change. Two runs died on the assertion rather than silently measuring at root 16 and reporting a clean sweep.
3. **A DOM-INJECTED ELEMENT RECEIVES NO SCOPED STYLES.** Astro scopes component CSS with `data-astro-cid-*`, so a hand-built `<a class="nav-link">` got **none** of `.nav-link`'s rules — no `min-inline-size`, no `min-block-size`, no `font-size` — and measured narrower and shorter than a real item, **the direction that makes a width test pass.** Replaced with a clone of a real node plus an assertion that its computed styles match a sibling's. *(And the replacement assertion was itself too strict — it hard-coded 14 px, which legitimately scales under enlargement, so two runs failed for the wrong reason. It now compares against a sibling.)*
4. ⚠️ **AND THE ONE THAT COST TWO ROUNDS: A TWO-POINT SWEEP CANNOT SEE A BAND, AND BOTH POINTS WERE ON THE SAME SIDE.** Round 1 measured root 16 and root 32, got 0 and 0, and shipped a gate that fails at roots 1830 — **the endpoints of a range are the worst possible sample when the failure is a discontinuity inside it**, and here the discontinuity is a *wrap*, which happens between sizes rather than at one. Round 2 then found that both samples were **above or at** the default, so an entire direction had never been tested: **Chrome's actual presets are 9 / 12 / 16 / 20 / 24 px and 32 is not one of them**, so the sweep had used a value no reader can select while missing three that they can. Sweep the interval AND both directions from the default, and prefer the values the product's own UI offers over round numbers.
5. **A `covered: 0` result cannot distinguish "nothing covers the target" from "the header is not sticky at all".** The seam between the two gate bands was checked in both directions for that reason — coverage 0 *and* `inset-block-start: 0px` with the header still pinned at top 0.00 at the default size.
### 10. The review — ELEVEN findings across two rounds, every one accepted
`adversarial-reviewer`, two rounds, D19 cap. **Nothing was declined.** One blocking, six should-fix:
1. **BLOCKING — the sticky gate ramped instead of stepping, and used the wrong threshold in the tagline band.** 20 of 300 points, 1069 px of `#main` behind the header, roots 1830. Fixed as §1(b); reproduced independently before fixing.
2. **The three-mechanism table's first row contradicted its own verdict column and measurement** — it said media-query `66rem` resolves to 2112 px under root scaling, which would mean the 944 px overflow could not happen. Corrected to 1056 px.
3. **The `docs/06` item was ticked on a two-endpoint verification.** Now ticked with the 300-point intermediate grid recorded in the item.
4. **The causation claim was wrong in both directions.** Fixed as §1(a), with the isolation table.
5. **"It never wraps above this breakpoint" holds only with the webfonts loaded.** Recorded as residual (c) and the comment's claim qualified.
6. **Four recorded ranges did not cover the space they claimed** — the header-height range at 200 % root (328350 → **244.59350.86**), the minimum-font-size header range (228271 → **164.58270.56**) and its coverage range (132174 → **68174**), and the tagline width list (four widths → **every width from 1216 up**). All four were correct for the widths sampled and over-claimed beyond them.
7. **D19 / simplicity — 83 lines added to `src/` for four functional lines.** Five comment blocks were longer than the code beneath them. Trimmed: the superseded tagline arithmetic, the token's argument against its own former sentence, `bio.astro`'s revision narrative, and `global.css`'s measurement detail. **A whole stale comment block was found duplicated** — round 1's sticky comment survived beside its replacement, still carrying the superseded figures.
**Round 2 returned four more, and one of them was blocking — in round 1's own fix.** That is the pattern D19's second round exists for, and it landed exactly as the constitution predicts:
1. **BLOCKING — the replacement gate was one-sided**, and Chrome's five font-size presets had never been swept. Fixed as §1(b) above; reproduced independently, including against the previous build, before changing anything.
2. **Three shipped comments and one spec paragraph asserted that a short offset "cannot arise", carving out one exception where there are two.** Replaced with an enumerated table of the four cases and their status — two live, two now fixed. A comment that says "cannot" is the kind that stops the next reader looking.
3. **The fallback-metrics case was recorded as a CLS issue only**, and its band was wrong. Corrected as residual (c).
4. **Simplicity / D19 again — 85 comment lines for 4 declaration lines, and the media-query-`rem` fact asserted in five places.** `tokens.css` and `global.css` now cite `docs/02` §Reflow instead of restating it; the load-bearing sentences stay in the component.
**TWO OF ROUND 2's SUGGESTED FIXES WERE CONSIDERED AND NOT TAKEN, recorded with the reasoning as D19 requires.** Neither is a disagreement about the defect — both were offered as alternatives to the gate term that shipped.
- **`--width-content: max(80rem, 1280px)`** — would stop the content column shrinking below 1280 px and so stop the masthead wrapping at a small root. Declined: it changes **every page's** layout at every below-default text size, which is a design-system change with site-wide reach, and it is the opposite of what a reader who *chose* smaller text is asking for. The gate term fixes the defect inside the header, which is the scope Pouya set.
- **`--header-h: calc(2 * var(--space-4) + 48px + 1px)`** — the header's measured composition rather than a flat `5.0625rem`, exact at 81 px when the root is 16. Declined as a no-op for correctness: `scroll-padding-top` is the token's only consumer, and `6.0625r ≥ 2r + 49` for every `r ≥ 12.06`, so the rem form is already sufficient wherever the header is both sticky and one row — and below the default the gate now makes it non-sticky, so the value there is never used. It is a genuine improvement in *truth* and would be a fine tidy-up in its own change set; taking a token change with site-wide reach as the last act of a capped review loop is how round 3 gets invented.
**Two rounds is the cap — D19 — so what round 2 found is fixed and this ships.** Stating the reasoning as the constitution requires: on 2026-08-28 two rounds produced 48 findings with most of round two's being defects in round one's fixes, and past two the manufacture rate exceeds the value of the marginal finding. This session is the same shape: round 1 found the original defect, round 2 found the defect in round 1's fix, and both were real.
The reviewer also confirmed a large part of the record by re-measuring it, and **three of its own statements were corrected against measurement rather than accepted**: the previous build's fallback-metrics behaviour (it shifts 5.6 px rather than overflowing without shifting), the characterisation of `flex: none` as "never applied" (it applied and had no effect), and — from round 2 — its reading that both removed `nowrap` declarations are needed at `defaultFontSize=9`, which is true there and does not disturb the root-32 isolation table, so both statements stand side by side rather than one replacing the other.
### 11. R11 — dependency currency, re-checked and reported, not acted on
`npm view` against all 19 pins, `[verified 2026-09-01]`. **`npm audit`: 0 vulnerabilities, all severities.** Two pins are a **major** behind and are out of this step's scope: **`@astrojs/mdx` ^7.0.8 → 8.0.0** and **`typescript` ^6.0.3 → 7.0.2**. Four more are within their caret and need nothing: `astro` 7.2.9 → 7.2.10, `@astrojs/sitemap` 3.7.3 → 3.7.4, `globals` 17.11.0 → 17.12.0, `typescript-eslint` 8.68.0 → 8.69.0. Raised for Pouya rather than upgraded — an MDX major on a site whose Insights collection is MDX is not a change to make inside a header fix.
### 12. Verification
All read as exit statuses, none through a pipe. `npm run check` **0** (0 errors / 0 warnings / 0 hints, all three lines read); `npm run build` **0**, 22 pages; `npm run check:claims` **0**; `npm run og:proof` **0**; `npm run check:intake` **0**; `npm run lint` **0**. Minifier tripwire — `grep -rE 'animation:[^;}]*(scroll\(\)|view\(\))' dist`**exit 1, no match**, with `animation-timeline: scroll()` and `view()` both confirmed present in the emitted CSS. No `TODO(pouya)` in `dist`, with a positive control proving the scanner reaches `dist`.
**The emitted gate was verified rather than read.** Lightning CSS folds the factor into the units and writes `clamp(-100vh,10000000vw - 6600000rem,0px)` and `clamp(-100vh,10000000vw - 7600000rem,0px)`; equivalence was confirmed by measuring the computed value (`0px` at every sticky width at the default size, header pinned at top 0.00), not by reading the arithmetic.
**Lighthouse — `npm run lighthouse`, the local gate, exit 0. 22 pages, no category below 95:** performance 99 on `/` and 100 on the other 21, accessibility 100, best practices 100, SEO 100 on the 16 indexable pages and 69n on the six deliberately `noindex`. **CLS 0.000 on all 22 pages**, which is the number this change set most needed: wrapping a sticky header is exactly the shape that produces a scroll-linked shift, and it produced none. LCP 1.582.03 s, with `/` at 2.03 s — over the 2 s budget, **unchanged by this work and pre-existing** (`/` was 2.03 s in entry (aj) too).
⚠️ **THE FIRST LIGHTHOUSE RUN OF THIS SESSION CRASHED AND WAS NEARLY RECORDED AS PASSING, AND THE FAILURE MODE IS WORTH THE LINE.** It died with `ENOENT … dist/practice/energy/index.html` because a rebuild cleared `dist/` underneath it — the script enumerates pages from `dist/`, so it cannot survive a concurrent build. **And the harness reported "exit code 0"**, because the shell command was `npm run lighthouse > log 2>&1; echo "exit=$?"` and the status belonged to the trailing `echo`, not to Lighthouse. Re-run in isolation with Lighthouse's own status captured to a file. Same family as the `tail -3` and the `sed`-in-a-pipeline traps: **the last command in a list is not the command you are asking about.**
**And the measurements were re-anchored to the shipped bytes:** after the final comment-only edits, a recursive digest of `dist/` matched the digest of the tree the measurements were taken against, so the figures above describe the bytes that would ship.
## 2026-08-31 (aj) — The eyebrow goes to 14 px and the small-text floor becomes real; the reflow defect turns out to be site-wide and its record turns out to be wrong twice; TTL is stamped `ENABLED`; §4 gains the struck-universal row
**Prior commit:** `64bce10` — entry (ai)'s twelve-file tree (eyebrow 12 → 13 px, TTL confirmed and gated), committed as-is on Pouya's instruction. It is a deliberate departure from one-logical-change-per-commit: entry (ai) covers both items and splitting it would have meant rewriting the record rather than moving it.