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 | | 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 | | 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 | | 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 | — | | ~~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 | — | | ~~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 # 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 ## 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. **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.
+35
View File
@@ -273,6 +273,15 @@ argument and the comparison never ran. Prefer `git grep`, quote or array-expand
anything you pass as flags, and re-check any result whose shape is "uniformly anything you pass as flags, and re-check any result whose shape is "uniformly
bad". bad".
**zsh does not word-split parameter expansions; a loop over `$VAR` runs ONCE —
use command substitution or arrays, and assert the iteration count.** *Pouya's
convention, 2026-09-01.* `for p in $PAGES` iterates one item, not twenty-two,
and `node probe.mjs 320,360 $P` measures one page — both of which then report
"max=0, nonzero=0" and read as a clean sweep. `$(cat file)` and `${=VAR}` do
split; `"${(@f)VAR}"` splits on newlines. **The fix is not remembering which:
assert the count before reading the result** — a probe that says how many rows it
measured cannot silently measure one.
**And re-check "uniformly GOOD" too — that is the dangerous half.** *Added **And re-check "uniformly GOOD" too — that is the dangerous half.** *Added
2026-08-30; sharpened on Pouya's instruction 2026-08-31, as "the sharpest 2026-08-30; sharpened on Pouya's instruction 2026-08-31, as "the sharpest
instrument finding yet".* The same `set -- $pair` loop recurred while confirming instrument finding yet".* The same `set -- $pair` loop recurred while confirming
@@ -387,6 +396,32 @@ So before acting on a number: say what it is a number *of*; confirm the command
actually ran and read its exit status; and check it against a second method that actually ran and read its exit status; and check it against a second method that
cannot fail the same way — the bytes on disk, a screenshot, a hit test. cannot fail the same way — the bytes on disk, a screenshot, a hit test.
**Two simulations of 200% text are not equivalent: media-query `rem` resolves
against the browser DEFAULT font size, not the root element — measure under both
methods before declaring a reflow result.** *Pouya's convention, 2026-09-01.*
Raising the default moves the breakpoints along with the type, so the desktop
layout is never reached and nothing overflows; setting `documentElement.style
.fontSize` doubles the type and leaves the breakpoints where they were, which is
the layout the desktop nav was measured in. One of those reported **0** while the
other reported **944 px** on the same 22 pages, and the prose generalised the
zero.
⚠️ **AND THERE ARE THREE MECHANISMS, NOT TWO — the third is the one that defeats
every CSS conditional.** Measured 2026-09-01: Chrome's **"Minimum font size"**
*floors* computed font sizes instead of scaling them, so text enlarges while
`rem` keeps resolving at 16 px. A media query does not see it, and neither does a
container query — `@container` `rem`/`em` DO track the root element (that is the
one real difference from `@media`, and it is measured), but under a minimum-font-
size setting they still resolve at 16 px while `getComputedStyle` reports 32 px.
**So no query fires, and only used-value layout — wrapping — reflows.** That path
was the worst of the three: on one grid of 22 pages × 16 widths, **219 of 352
page-widths overflowed** against root-style's **175**, and it was the only one
failing at 320 px and 1024 px. **Always state the grid with the count** — two
sweeps in that session quoted totals of 220, 330 and 352 for the same claim, and
side by side they read as contradictions rather than as different width lists.
Measure all three mechanisms; treat a clean result from one as evidence about
that one.
**And a grep that matches is not a finding until you read what it matched.** **And a grep that matches is not a finding until you read what it matched.**
A case-insensitive sweep for `LSO` hit `I aLSO practise`; a superlative sweep for A case-insensitive sweep for `LSO` hit `I aLSO practise`; a superlative sweep for
`leading` hit `the pLEADINGs`. Both on the same page on the same day. Print the `leading` hit `the pLEADINGs`. Both on the same page on the same day. Print the
+371 -66
View File
@@ -145,12 +145,21 @@ alias of it; see the eyebrow rule below.
consumer of the 11 px token. `--text-xs` (12 px) is a separate rung and is consumer of the 11 px token. `--text-xs` (12 px) is a separate rung and is
enumerated below. enumerated below.
**The exception, and why it is deliberate rather than residual:** the tagline is **The exception, and why it is deliberate rather than residual:** the tagline is
ornamental and layout-constrained. 13 px already took the header to 83.4 px while ornamental and layout-constrained. ⚠️ **THE COST WAS RE-MEASURED ON 2026-09-01
`--header-h` is 81 px, which `global.css` feeds into `scroll-padding-top`; and AND IT IS A DIFFERENT COST NOW — the masthead may wrap, so what used to overflow
with a seventh nav item the CTA lands past `.header-inner`'s content edge at invisibly is visible instead.** At `--text-eyebrow` (14 px) the header stands at
1216, 1240, 1280 and 1440 — it does not resolve as the viewport widens. Insights **144.98 px** rather than 81 px — at 1216 px with six nav items, and at **every
is that seventh item. Both measurements are in `SiteHeader.astro`, and document width from 1216 px up** with a seventh (measured at 1216 / 1240 / 1280 / 1360 /
overflow is 0 in every one of those cases, so no page-level check can see it. 1440 / 1600 / 1760 / 1920) — because the row wraps. *(This listed four widths
ending at 1440 for one revision; that list was carried over from the superseded
CTA-past-the-edge measurement, which really did stop at 1440.)* **Document overflow
is 0 and the CTA sits exactly on `.header-inner`'s content edge in every one of
those cases**, where before the fix they were 20 px of overflow at 1216 px, 4 px
at 1280 px, and the CTA 67.8 / 43.8 / 51.8 / 51.8 px past the content edge. So
the two costs this bullet used to cite are **gone**, replaced by one that is
larger and easier to see: 64 px of header height on every page. *(13 px took the
header to 83.4 px; that figure was taken before the masthead could wrap.)*
Insights is that seventh item. The measurements are in `SiteHeader.astro`.
**Three blocks moved to `--text-eyebrow` on 2026-08-31 and their carve-outs are **Three blocks moved to `--text-eyebrow` on 2026-08-31 and their carve-outs are
superseded.** (a) `/contact/`'s `<label>`/`<legend>` set — *raise, do not superseded.** (a) `/contact/`'s `<label>`/`<legend>` set — *raise, do not
ratify*, superseding the "accepted, not ratified" note this bullet carried for ratify*, superseding the "accepted, not ratified" note this bullet carried for
@@ -171,8 +180,17 @@ alias of it; see the eyebrow rule below.
size. *(This read "63,743 bytes" for one revision. That is the `cmp -l` size. *(This read "63,743 bytes" for one revision. That is the `cmp -l`
differing-byte count, not the size delta: the content stream is Flate-compressed, differing-byte count, not the size delta: the content stream is Flate-compressed,
so a one-property change scrambles most of the file. Two figures, and the wrong so a one-property change scrambles most of the file. Two figures, and the wrong
one answered a question nobody asked.)* That freeze is a held decision, not a one answered a question nobody asked.)* ⚠️ **THAT FREEZE IS NOW A RULED
design position — see `bio.astro`. CONSTRAINT, 2026-09-01, NOT A DECISION AWAITING POUYA: the circulated PDF's
typography changes only when its CONTENT is deliberately revised, never as a
side effect of a screen refactor.** So the print block keeps 400 whatever the
screen does, and `.eyebrow`'s 500 stops at the `@media print` boundary. **The
reason it is a rule and not a preference** is that the PDF is the one artefact
this project's apparatus sees only when someone re-renders it (`AGENTS.md`
R16): a refactor that silently re-typesets it changes a document already in a
reader's hands, and the diff that would have shown it is a 1,655-byte
compressed blob nobody reads. A screen change that reaches print is therefore
a defect by construction, not a judgement call — see `bio.astro`.
**`--text-xs` (12 px) IS A SEPARATE RUNG AND IT WAS NOT IN SCOPE ON 2026-08-31. **`--text-xs` (12 px) IS A SEPARATE RUNG AND IT WAS NOT IN SCOPE ON 2026-08-31.
Ten declaration sites, all of them:** `Pill`, `Breadcrumbs`, `CredentialRow`, Ten declaration sites, all of them:** `Pill`, `Breadcrumbs`, `CredentialRow`,
`DefinitionGrid`, `ArticleCard`, `ProcessStep` (two), `insights/[...slug].astro`, `DefinitionGrid`, `ArticleCard`, `ProcessStep` (two), `insights/[...slug].astro`,
@@ -371,70 +389,247 @@ Not a polish pass. A build requirement.
`documentElement.scrollWidth - documentElement.clientWidth`, plus an `documentElement.scrollWidth - documentElement.clientWidth`, plus an
enumeration of every element wider than `clientWidth` to name the offender. enumeration of every element wider than `clientWidth` to name the offender.
⚠️ **THE NARROW-WIDTH RESIDUALS ARE 0. THE 1280 px AND 1920 px ONES ARE NOT, AND **THE LARGE-TEXT NAV OVERFLOW IS FIXED, 2026-09-01.** Pouya reopened the
THIS PARAGRAPH CLAIMED THEY WERE.** Pouya ruled the narrow case a defect on step-1 header decision for the purpose — *"its record attributed the residual to
2026-08-31 — *"the 152/112/82 px overflows at 320/360/390 with 32 px root text the wrong cause and characterized a measured 944 px functional failure as a
violate WCAG 1.4.10 and are not accepted"* — and that case is fixed. The wide robustness margin"* — and ruled *fix, do not accept*. What follows is the
case is a **different cause**, is **not** fixed, and is now stated correctly current state. The narrow case was ruled a defect on 2026-08-31 (*"the
rather than reported as zero. 152/112/82 px overflows at 320/360/390 with 32 px root text violate WCAG 1.4.10
and are not accepted"*) and fixed then.
**TWO METHODS OF SIMULATING 200% TEXT, AND THEY ARE NOT EQUIVALENT. This is the **THREE MECHANISMS ENLARGE TEXT, AND THEY ARE NOT EQUIVALENT — this is the
whole of the error.** Every earlier number in this section came from the command measurement the whole record turned on.** Every figure in this section before
below — `documentElement.style.fontSize = '32px'`. A later sweep used Chrome's 2026-09-01 came from `documentElement.style.fontSize = '32px'`. A later sweep
`Page.setFontSizes({standard: 32})` instead and reported 0 everywhere, and the used Chrome's `Page.setFontSizes({standard: 32})`, reported 0 everywhere, and
prose generalised that to *"at root 16 px and root 32 px"*. the prose generalised that zero. All three are re-measured below.
| | media query `66rem` resolves to | header above 1056 px | 1280 px, root 32 | | mechanism | `rem` in a media query | `rem`/`em` in a PROPERTY | `getComputedStyle` root | sees the enlargement? |
|---|---|---|---| |---|---|---|---|---|
| `documentElement.style.fontSize='32px'` | **1056 px** — matches | `sticky`, `flex-wrap: nowrap` | **944 px over** | | `documentElement.style.fontSize='32px'` (root style) | **16 px → `66rem` = 1056 px** | 32 px | 32 px | media query **no**, property **yes** |
| `Page.setFontSizes({standard:32})` | **2112 px** — never matches | `relative`, `flex-wrap: wrap` | **0** | | `Page.setFontSizes({standard:32})` (the reader's *default font size*) | 32 px → `66rem` = **2112 px** | 32 px | 32 px | **yes, both** — the breakpoint moves with the type |
| `--blink-settings=minimumFontSize=32` (the reader's *minimum font size*) | 16 px → `66rem` = **1056 px** | **16 px** | **32 px** | **NOTHING IN CSS SEES IT** |
In a media query `rem` resolves against the **default** font size, not the root *(Row 1's media-query cell read "32 px → 2112 px" for one revision, which
element's. So raising the *default* moves the breakpoints along with the type and contradicted its own verdict column, the paragraph below it, and measurement —
the desktop nav is never reached; setting the *root* doubles the type while and it deleted the one number that explains why the failure is reachable at all.
leaving the breakpoints at 16 px, which is the layout the desktop nav was If `66rem` really resolved to 2112 px under root scaling, the 66 rem desktop
measured in. **Both are real settings, and there is a THIRD mechanism which is masthead would never be reached at 1280 px and the 944 px overflow could not
the one a reader actually reaches.** Chrome exposes "Font size" — the default, happen. Found by `adversarial-reviewer`.)*
the second row, which moves the breakpoints along with the type and overflows
nothing — and, separately, **"Minimum font size", which FLOORS computed sizes
rather than scaling them and leaves media queries alone.** Measured at 1280 px
with `--blink-settings=minimumFontSize=32`: the `66rem` query matches, the nav is
`sticky` / `nowrap`, `.brand-name` computes to **32 px (floored, not scaled to
42.6 px)**, and the document is over by **830 px**. So the failure is reachable,
and **830 px is the reachable figure while 944 px is the `documentElement`-root
one — they are not the same measurement and must not be quoted as one.** The two
rows below are the two simulations, not the three mechanisms.
**The corrected figures, `documentElement.style.fontSize='32px'`, 22 pages:** Two consequences, both measured 2026-09-01 and both load-bearing:
| width | pages over | overflow | 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
along with the type and the desktop masthead is never reached; setting the
*root* doubles the type and leaves the breakpoints where they were, which is
the layout the desktop nav was measured in. **A container query is the one
construct that differs** — `@container` `rem`/`em` DO resolve against the
root element, measured — which is why the sticky gate below is a property and
not a query.
2. ⚠️ **UNDER A MINIMUM-FONT-SIZE SETTING, `getComputedStyle(el).fontSize` AND
THE `em` UNIT DISAGREE.** Blink floors the *reported computed* font size to
32 px while `em` and `rem` keep resolving at 16 px — so `scroll-padding-top:
6.0625em` computes to **97 px, not 194 px**, a `66em`-wide box measures
**1056 px, not 2112 px**, and `min(0px, calc(100vw - 66em))` is **0px**.
Media queries, container queries and length units are all blind to it.
**Only used-value layout — wrapping — responds**, which is why wrapping had
to be the mechanism and why one residual below cannot be closed in CSS.
**Before and after, 22 pages × 16 widths (320 → 1920 px) = 352 page-widths per
setting, document overflow `documentElement.scrollWidth
documentElement.clientWidth`. Both columns measured on the SAME grid**, from a
build of the previous commit and a build of the working tree, by the same probe:
| text setting | before | after |
|---|---|---| |---|---|---|
| 320 / 360 / 390 / 414 / 640 / 1024 | **0 of 22** | **0** — the ruled targets, fixed | | default (root 16) | 0 of 352 | **0 of 352** |
| 1280 | 22 of 22 | **944 px** on 21 pages, **508 px** on `/` | | root style 32 px | **175 of 352** | **0 of 352** |
| 1920 | 21 of 22 | **304 px** (all but `/`) | | 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, see the residual below |
Root 16 is 0 at every width. The old **944 px** figure is **unchanged**; 602 px **The wide-width figures that were the defect, root style 32 px, and what they
became 508 px on `/` only because `.brand-name`'s `nowrap` went. are now.** Every one is 0 of 22 pages after the fix, and the nav items and the
CTA are on-screen at every width:
⚠️ **AND AT 1280 px IT IS NOT A MARGIN — IT IS UNREACHABLE NAVIGATION.** The | width | before | after |
furthest element is `div.header-cta` at **2224 px** against a 1280 px viewport; |---|---|---|
the nav clips mid-word after "Arbitration", and **Practice (left 1378), Fees | 1056 | 649 px, 22 of 22 | **0** |
(1556), Contact (1666) and the header CTA are entirely off-screen** behind a | 1100 | 606 px, 22 of 22 | **0** |
horizontal scrollbar — including the route to the inquiry form. *(This named | 1216 | 928 px / 491 px on `/`, 22 of 22 | **0** |
"Med-Arb" for one revision. Med-Arb is in `SECONDARY_NAV` and is not in the | 1240 | 904 px / 468 px on `/` | **0** |
masthead at any width; the item actually lost, and the worst of them, is | 1280 | **944 px** / 508 px on `/` | **0** |
Contact.)* That is | 1440 | 784 px / 351 px on `/` | **0** |
WCAG 1.4.4 with loss of functionality. The paragraph struck here called it *"a | 1600 | 624 px / 193 px on `/` | **0** |
robustness margin rather than a failure. Revisit if a real reader hits it."* | 1920 | 304 px, 21 of 22 | **0** |
**THE CAUSE AT 1280 px IS `flex-wrap: nowrap` ON `.nav-list` ABOVE 66 rem, AND *(Only 1280 and 1920 were in the earlier record, so it never showed that the
THAT IS A LOCKED STEP-1 DECISION — SO IT IS AN OPEN DECISION FOR POUYA, NOT A failure ran the whole width of the sticky range. The same run under minimum font
FIX MADE HERE.** His instruction with the eyebrow ruling was explicit: *"if size was 414 / 370 / 854 / 830 / 830 / 750 / 670 / 510 px, of which the record
anything header-side does overflow, stop and report rather than improvising a carried only the 830.)*
layout change."* The options are an `em`-based breakpoint so the header drops to
its stacked layout when the type grows, or letting the nav wrap above 66 rem and ⚠️ **AND IT WAS NOT A MARGIN — IT WAS UNREACHABLE NAVIGATION.** The furthest
giving up the constant 81 px `--header-h`. Both change the desktop masthead. element was `div.header-cta` at **2224 px** against a 1280 px viewport; the nav
Carried on `docs/06`'s cutover checklist so it cannot ship unresolved. clipped mid-word after "Arbitration", and **Practice, Fees, Contact and the
header CTA sat entirely off-screen** behind a horizontal scrollbar — the route
to the inquiry form among them. WCAG 1.4.4 with loss of functionality. The
paragraph struck here called it *"a robustness margin rather than a failure.
Revisit if a real reader hits it."*
**THE FIX IS TWO DECLARATIONS, AND THE SECOND ONE IS THERE BECAUSE THE FIRST
ONE HAS A CONSEQUENCE.**
**(1) The masthead may wrap.** `flex-wrap: nowrap` is gone from `.header-inner`
and from `.nav-list` above 66 rem, along with the dead `flex: none` beside it.
⚠️ **WHICH OF THE TWO WAS THE CAUSE IS NOW MEASURED, AND THIS PARAGRAPH FIRST
GOT IT WRONG.** It said *"that pair, not any one declaration, was the cause"*.
Restoring each declaration alone on the fixed build, root 32, `/about/`
document overflow / number of nav links and 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 state before) | 649/3 | 928/5 | 944/5 | 784/4 | 304/1 |
**`.header-inner`'s `nowrap` was necessary and sufficient.** And the second row
is the one that matters for honesty: **`.nav-list`'s removal is inert** — byte-
identical to shipped at every width, under a 200 % root size *and* under minimum
font size, **with six nav items and with seven**. It is kept removed for two
reasons that are not "it fixed the overflow": the `nowrap` prohibition in
`SiteHeader.astro` would otherwise contradict a `nowrap` still sitting in the
file, and the override only ever re-stated `nowrap` over a base rule that already
wraps. Found by `adversarial-reviewer`; the mechanism sentence about min-content
was true and was not the binding constraint. *(`flex` is a flex-ITEM property and `.nav`
computes `display: block`, measured at 1056 / 1280 / 1920 px — so `flex: none`
set `flex-shrink: 0` on something that is not a flex item and had **no effect**.
Removing it leaves `.nav`'s and `.nav-list`'s geometry byte-identical at all
three widths; 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.)*
**It cost nothing at any normal size: 0 geometry
differences across 22 pages × 16 widths (352 page-widths)**, header exactly **81.00 px** at every
width from 1056 px up, brand on one line, nav on one line, CTA exactly on
`.header-inner`'s content edge (gap 0.00 px) — **and the same with a seventh nav
item injected**, at 1024 / 1047 / 1056 / 1071 / 1100 / 1150 / 1200 / 1216 / 1240
/ 1280 / 1440 / 1920 px, where all seven share one line from 1056 px up. 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 already accounts for seven items.
**(2) The masthead is sticky only while it is one row**, and this half took two
attempts — the first one shipped a regression, which is recorded because the
reason it failed is the useful part.
Wrapping makes the header **taller** at enlarged text: **244.59350.86 px** at a
200 % root size against 81 px normally. A sticky box that size covers `#main`
after "Skip to content", so stickiness has to go wherever the header can exceed
`scroll-padding-top`. 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 WAS WRONG IN TWO WAYS
THAT ONLY AN INTERMEDIATE ROOT SIZE EXPOSES.** Both were found by
`adversarial-reviewer` and independently reproduced: **20 of 300**
(page × viewport × root) points left `#main` behind the header, 1069 px, across
roots 1830 — while root 16 and root 32, the only two sizes the first sweep
measured, were both clean. **WCAG 1.4.4 is a requirement up TO 200 %, not AT
200 %**, and a two-point sweep cannot see a diagonal band.
1. **It RAMPED where it needed to STEP.** One pixel inside the threshold the term
lifts the header by one pixel — but the header has already gained a whole row,
so it still covered `#main`. Wrapping is a step function; the response has to
be one too. Hence the `* 100000` saturation, bounded by `-100vh`.
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**
— the step-1 binary search already recorded that number. Gating the wide band
on 66 rem therefore 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 THAT THE 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 the
sweeps had used 16 and 32 — 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 offset on all 22 pages. So the wrap fix widened a
pre-existing 12 px defect to 65 px, and the second term closes both.
**The shipped form is two bands, each gating on the width ITS layout requires,
and two terms, each catching one direction:**
```css
@media (min-width: 66rem) { .site-header {
position: sticky;
inset-block-start: clamp(-100vh,
min(calc((100vw - 66rem) * 100000), calc((1rem - 16px) * 100000)), 0px); } }
@media (min-width: 76rem) { .site-header {
inset-block-start: clamp(-100vh,
min(calc((100vw - 76rem) * 100000), calc((1rem - 16px) * 100000)), 0px); } }
```
`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 catches 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 whose default text size is "Small" (12 px) loses the sticky header, where
it currently works — but only just: the header is 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 rather than after
it. **At every normal size both evaluate to exactly `0px`** — a matched
`min-width: 66rem` guarantees `100vw ≥ 1056 px` and a matched `min-width: 76rem`
guarantees `100vw ≥ 1216 px`, because `100vw` counts a classic scrollbar and the
query width does not, so the term can only be more positive.
**Measured after: 0.00 px of `#main` covered — 0 of 300 grid points across roots
1632 × viewports 10561920 on three pages, 0 of 176 rows at the default size and
at a 200 % root size over all 22 pages, 0 of 144 cases sitting directly on the
two thresholds, and 0 of 198 at the SEAM where the two bands meet** (viewports
11801260 × roots 1632). ⚠️ **`covered: 0` is also what a non-sticky header
gives, so the seam was checked the other way round as well:** at the default size
across 1180 / 1200 / 1210 / 1215 / 1216 / 1217 / 1220 / 1240 / 1260 / 1440 /
1920 px the computed `inset-block-start` is `0px` and the header is still pinned
at top 0.00 when scrolled — stickiness is preserved, not quietly lost. ⚠️ **Saturation leaves a transition band, and it is
arithmetic rather than measured:** a finite factor means the response 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 that band
was not driven directly — it is bounded by the arithmetic, not by a probe.
*(Lightning CSS folds the factor into the units and emits
`clamp(-100vh,10000000vw - 6600000rem,0px)`. Verified equivalent by measurement,
not by reading: the computed `inset-block-start` is `0px` at every sticky width at
the default size, and the header pins at top 0.00.)*
⚠️ **"AN OFFSET TOO SHORT CANNOT ARISE" WAS WRITTEN HERE AND IN TWO SHIPPED
COMMENTS, AND IT IS FALSE. THE EXCEPTIONS ARE ENUMERATED RATHER THAN WAVED AT**,
because a comment that says "cannot" is the kind that stops the next reader
looking. The gate handles **root-relative** enlargement in both directions.
What it cannot see, measured, header height against computed
`scroll-padding-top`, header `top` = `0px` in every row:
| condition | header | offset | short by | status |
|---|---|---|---|---|
| `minimumFontSize=32` | 164.58270.56 px | 97 px | **68174 px** | live, unfixable in CSS |
| fallback font metrics + 7th nav item, **default** text size, 10561091 px | 141 px | 97 px | **44 px** | latent on `showInsights` |
| `defaultFontSize=9` | 120 px | 54.56 px | ~~65 px~~ | **fixed** by the `1rem - 16px` term |
| root-style 10 px | 69 px | 60.63 px | ~~8 px~~ | **fixed** by the same term |
**`--header-h` IS A FLOOR, NOT A CONSTANT**, and that is the sentence that
had to change. It is the header's height at the **default** text size; above the
default the masthead is deliberately taller, and declaration (2) is what makes
that safe — where the gate can see the header exceed the token it is not sticky,
so a `scroll-padding-top` that is too *generous* is harmless. ⚠️ **It does NOT
make a short offset impossible: the two cases the gate cannot see are tabulated
above.** The token previously 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.
**What the narrow-width fix DID establish, and it is worth keeping separate.** **What the narrow-width fix DID establish, and it is worth keeping separate.**
The narrow residual had been charged to that same `flex-wrap: nowrap` — and for The narrow residual had been charged to that same `flex-wrap: nowrap` — and for
@@ -447,11 +642,15 @@ Not a polish pass. A build requirement.
defended forever**, because the reason given is one nobody wants to re-open — defended forever**, because the reason given is one nobody wants to re-open —
which is exactly what happened for four days. which is exactly what happened for four days.
**The eight narrow-width fixes — no `overflow-x` was added anywhere.** Seven are **TEN FIXES ACROSS THE TWO PASSES — no `overflow-x` was added anywhere.** Eight
cause-specific; the eighth is marked as the backstop it is: are narrow-width (2026-08-31); the last two are the large-text nav (2026-09-01).
All but one are cause-specific; the exception is marked as the backstop it is:
| Element | Was | 320 px | Cause | | Element | Was | 320 px | Cause |
|---|---|---|---| |---|---|---|---|
| `SiteHeader .header-inner` | `flex-wrap: nowrap` removed above 66 rem | **944 px** at 1280 px, root 32 | **Necessary and sufficient, measured.** A `nowrap` flex line cannot break, so the brand / nav / CTA row could not reflow at all |
| `SiteHeader .nav-list` | `flex-wrap: nowrap` and a dead `flex: none` removed above 66 rem | **0 px — inert** | Kept removed so the `nowrap` prohibition is not contradicted by a `nowrap` in the same file. Byte-identical to shipped at every width and every text setting, six items and seven |
| `SiteHeader .site-header` | `inset-block-start: 0` → a saturating `clamp()` gate, in **two** bands (66 rem, and 76 rem where the tagline shows) | *(consequence of the row above)* | Wrapping makes the header 244.59350.86 px at root 32; sticky at that height covered `#main` by up to 157 px after the skip link. **Round 1's single-band `min()` ramped instead of stepping and left 20 of 300 grid points covered by 1069 px** |
| `SiteHeader .brand-name` | `white-space: nowrap` removed | **63 px, all 22 pages** | Two words glued into one unbreakable box | | `SiteHeader .brand-name` | `white-space: nowrap` removed | **63 px, all 22 pages** | Two words glued into one unbreakable box |
| `/bio/ .rates-list` | `overflow-wrap: anywhere` | **152 px** | `https://adr.smlcompany.ca/fees/` has no break opportunity, and the single grid track it sized stretched all five rows to 424 px | | `/bio/ .rates-list` | `overflow-wrap: anywhere` | **152 px** | `https://adr.smlcompany.ca/fees/` has no break opportunity, and the single grid track it sized stretched all five rows to 424 px |
| `/ .hero-h` | `overflow-wrap: anywhere` | 36 px | "contract," at 104 px held `.hero-copy` open | | `/ .hero-h` | `overflow-wrap: anywhere` | 36 px | "contract," at 104 px held `.hero-copy` open |
@@ -475,7 +674,37 @@ Not a polish pass. A build requirement.
card, escaping its own rounded border. Two lines is the better of those. The card, escaping its own rounded border. Two lines is the better of those. The
clamp does not change any normal size: 10vw holds 32 px from 320 px up. clamp does not change any normal size: 10vw holds 32 px from 320 px up.
**Three instrument findings, and each one hid a real defect:** **Six instrument findings, and each one hid a real defect. The last three are
from the 2026-09-01 pass and all three produced a CLEAN-LOOKING result:**
4. ⚠️ **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 mechanism. `global.css` sets `scroll-behavior: smooth`, which
makes `scrollIntoView()` **asynchronous**, so a synchronous read afterwards
measures `scrollY = 0`; and at `scrollY = 0` the target sits exactly at the
header's bottom edge, so `covered` is 0 **by construction**. Pass
`behavior: 'instant'` and assert that `scrollY` equals its predicted value —
once fixed the same probe reported 1026 px covered on 22 of 22 pages, a
pre-existing defect the tautology had hidden.
5. ⚠️ **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`. This codebase's reduced-motion block sets
`transition-duration: 0.01ms !important` on `*`, which starts a transition on
**every** property change, and a synchronous `getComputedStyle` then returns
the transition's *start* value. The instrument now injects
`transition:none;animation:none` instead. Same family as *a running
transition outranks an `!important` author declaration*.
6. **A DOM-INJECTED ELEMENT RECEIVES NO SCOPED STYLES.** Astro scopes component
CSS with a `data-astro-cid-*` attribute, so the hand-built
`<a class="nav-link">` used to test a seventh nav item 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, which is
the direction that makes a width test pass. **Clone a real node** and assert
its computed styles match a sibling's.
The original three:
1. **`overflow-wrap: break-word` permits a break at layout time but does not 1. **`overflow-wrap: break-word` permits a break at layout time but does not
reduce min-content size** — `anywhere` does. Five of the fixes are this reduce min-content size** — `anywhere` does. Five of the fixes are this
@@ -498,6 +727,82 @@ Not a polish pass. A build requirement.
`documentElement.scrollWidth - documentElement.clientWidth`, which is correct `documentElement.scrollWidth - documentElement.clientWidth`, which is correct
in both modes. in both modes.
⚠️ **THE ONE RESIDUAL THAT REMAINS, AND IT IS THE FOOTER RATHER THAN THE
HEADER.** Under Chrome's **minimum font size** only, document overflow remains at
four widths — **14 px at 320, 38 px at 1024, 30 px at 1056, 19 px at 1100**, on
all 22 pages, falling to 0 from 1200 px up. Every instance is
`SiteFooter`: `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 measures 310 px inside a 272 px content area. **The FOOTER'S OWN overflow is
byte-identical before and after** — `div.footer-grid` 78 px at 1056 and 67 px at
1100, `footer.site-footer` 30 px and 19 px, unchanged — and the 320 px and
1024 px *document* figures are unchanged too, at 14 and 38. ⚠️ **The 1056 px and
1100 px document figures DID change, 414 → 30 and 370 → 19, and this sentence
claimed all four were identical.** They changed because the header stopped
contributing its own 414 px and 370 px, not because the footer moved; a document
figure and a footer contribution are different measurements and only the second
is unchanged. Found by `adversarial-reviewer`. It is **out of the scope Pouya set** for this step ("the header and
its consumers"), so it was measured and left rather than improvised: the
10241100 px half looks like one `overflow-wrap: anywhere` on the footer contact
links, the 320 px half is a column-sizing question and is not the same fix.
Not header-side: **no header element extends the document at any width under any
mechanism**, and the nav items and CTA are on-screen in **1408 of 1408**
measurements (four text settings × 22 pages × 16 widths). *(This said "946 of
946" for one revision. 946 is a different sweep — the four runs made with the
tautological skip probe in instrument finding 4 — and reusing it here for the
final build was the two-similar-totals mistake this section keeps warning
about.)* *(The Practice dropdown panel's contents do overflow their own box
by 84 px under this setting, but the panel sits inside a **closed** `<details>`
and never extends the document — checked by taking the furthest-right box rather
than the largest content overflow.)*
⚠️ **AND ONE THAT CANNOT BE CLOSED IN CSS AT ALL.** Under minimum font size the
sticky gate above cannot fire — **both** of its terms evaluate to `0` because
`rem` still resolves at 16 px there, so `100vw - 66rem` is positive and
`1rem - 16px` is exactly zero — and the header stays sticky at
**164.58270.56 px** and 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. *(These read "228271 px" and "132174 px" for one revision; both
omitted the single-row band — 68 px at 1200 px and on `/` from 1216 px up.
`adversarial-reviewer` measured the widths the first sweep had not.)* The trade is deliberate and is the right way round: what it bought is the
removal of **830 px of horizontal overflow with four nav items and the CTA
unreachable** under that same setting. Vertical offset leaves every target
reachable by scrolling; the horizontal case did not. **No media query, container
query or length unit can detect this mechanism** (see the table above), the only
candidates left are a fitted magic number tied to the current nav item count or
a normal-settings behaviour change, and Pouya's constraint was to stop and report
rather than ship either. Carried on `docs/06`'s cutover checklist.
⚠️ **AND ONE MORE LATENT CASE, AT THE DEFAULT TEXT SIZE — NO READER SETTING
INVOLVED: FALLBACK FONT METRICS PLUS THE SEVENTH NAV ITEM.** 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 nav item cloned
from a real node, the header measures **141 px at every width from 1056 to
1091 px**, 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:**
1. **A 60 px collapse** when Geist swaps in — a CLS contribution on all 22 pages,
against the CLS < 0.05 budget. **Larger, not new:** the previous build gives
86.5986.97 px in the same band, so a ~5.6 px shift already existed.
2. ⚠️ **AND 44 px OF `#main` BEHIND THE STICKY HEADER** after the skip link,
because 141 px exceeds the 97 px `scroll-padding-top`. **This one IS new** —
the previous build's 86.97 px stayed under 97 px, so it covered **0**. Framing
this case as "a CLS contribution" understated it, and a reader ruling on a
layout shift would weigh it differently from one ruling on the skip link.
*(The band was recorded as "1056 / 1064 / 1072 / 1084 … 81 px from 1092 up",
which reads as though 10861090 were checked and clear; they are all 141 px. It
is a contiguous **36 px** band, 10561091. Both errors found by
`adversarial-reviewer`.)*
**It is latent, not shipped:** with six items there is no wrap at any width, and
`showInsights` turns the seventh on only at two published articles. **Not fixed
here, and deliberately:** the two candidates are raising the desktop breakpoint
past 1091 px, which changes the layout at normal settings in that band, or giving
Geist a metric-matched `size-adjust` fallback, which is a font-stack change well
outside "the header and its consumers". Both are Pouya's call; carried on
`docs/06`.
**What is still latent, measured and not a defect today:** `.hero-h` has **16 **What is still latent, measured and not a defect today:** `.hero-h` has **16
scoped rules, one per page, and only 2 carry `overflow-wrap: anywhere`** (`/` and scoped rules, one per page, and only 2 carry `overflow-wrap: anywhere`** (`/` and
`/about/`). The other 14 measure 0 because their headlines' longest words are `/about/`). The other 14 measure 0 because their headlines' longest words are
+69 -15
View File
@@ -534,21 +534,75 @@ Then invalidate `/*`.
- [ ] Favicon set complete - [ ] Favicon set complete
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox - [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
- [ ] Tested at 320 px and at 200% zoom - [ ] Tested at 320 px and at 200% zoom
- [ ] ⚠️ **THE 200%-DEFAULT-TEXT RESIDUAL AT 1280 px AND 1920 px IS RULED ON, - [x] **THE 200%-TEXT NAV OVERFLOW IS FIXED, 2026-09-01 — THIS ITEM IS
EITHER WAY. BLOCKING, AND IT IS A DECISION RATHER THAN A TASK.** With DISCHARGED.** Pouya reopened the step-1 header decision and ruled *fix, do
`documentElement.style.fontSize = '32px'` the site is over by **944 px on 21 not accept*. `flex-wrap: nowrap` is gone from `.header-inner` (the necessary
pages and 508 px on `/` at 1280 px**, and **304 px on 21 pages at 1920 px** and sufficient one, measured) and from `.nav-list`, and the masthead is
— and it is not a scroll margin: the nav clips mid-word and **Practice, Fees, sticky only while it is one row — a saturating `clamp()` gate in two bands,
Contact and the header CTA are entirely off-screen — including the route to 66 rem and 76 rem. Measured across 22 pages × 16 widths, before and after on
the inquiry form**, WCAG 1.4.4 with loss of functionality. The cause is `flex-wrap: nowrap` on `.nav-list` above the same grid: **root-style 32 px went from 175 of 352 page-widths over to
66 rem, a **locked step-1 header decision**, so no fix was improvised — see 0**; minimum font size from 219 to 88, all of the remainder being the footer;
`docs/02` §Reflow for the measurement, the two non-equivalent ways of default font size stays 0. Nav items and CTA on-screen in **1408 of 1408**
simulating 200% text, and the two candidate fixes. **This item is ticked by measurements. The shipped rendering at the default size is
Pouya either fixing it or recording it as knowingly accepted with the **geometry-identical — 0 differences on 352 page-widths**, header 81.00 px,
measured consequence — not by re-measuring it with the method that reports CTA on the content edge, with six nav items and with a seventh.
zero.** The narrow widths (320/360/390/414/640/1024) are fixed and are 0. ⚠️ **AND IT WAS VERIFIED ACROSS THE INTERMEDIATE ROOT SIZES, WHICH IS WHAT
Raised by `adversarial-reviewer`, 2026-08-31, against a record that had ROUND 1 DID NOT DO.** WCAG 1.4.4 is a requirement up TO 200 %, not AT 200 %:
declared the whole thing fixed round 1 sampled root 16 and root 32 only, both clean, and shipped a gate that
left `#main` behind the header at **20 of 300** grid points across roots
1830. **And round 2 found the gate was one-sided**: Chrome's "Font size" has
five presets (9 / 12 / 16 / 20 / 24 px) and only 16 had been swept, so at
**9 px the masthead wrapped for the opposite reason** — the `80rem` content
cap shrinks while the header's px minimums do not — leaving **65 px of `#main`
behind the header** on 21 of 22 pages at every width. A second gate term
(`1rem - 16px`) closes it, and closes a **pre-existing 12 px** shortfall on
all 22 pages at that setting as well. Now **0 failing across 762 points**:
roots 915 (70), roots 1632 (300), the band seam (198), both thresholds
(144) and all five default-font-size presets (50). Both findings by
`adversarial-reviewer`; `docs/02` §Reflow carries all of it
- [ ] ⚠️ **TWO REFLOW RESIDUALS SURVIVE, BOTH UNDER CHROME'S MINIMUM-FONT-SIZE
SETTING ONLY, AND BOTH NEED A RULING RATHER THAN A RE-MEASUREMENT.**
*(a) The footer, and it is OUT OF the scope Pouya set for the header step.*
Document overflow of **14 px at 320, 38 px at 1024, 30 px at 1056 and 19 px
at 1100 CSS px**, on all 22 pages, 0 from 1200 px up. Cause:
`info@smlcompany.ca` has no break opportunity and needs 310 px inside a
224243 px footer-contact column; at 320 px the `.footer-col` box itself is
310 px in a 272 px content area. **Pre-existing: the footer's OWN overflow is
byte-identical before and after** (`div.footer-grid` 78 px at 1056, 67 px at
1100). The 1056/1100 *document* figures did change, 414 → 30 and 370 → 19,
because the header stopped contributing its own — a document figure and a
footer contribution are different measurements.
*(b) The skip link, and it CANNOT be fixed in CSS.* The header stays sticky
at **164.58270.56 px** under that setting, so "Skip to content" lands with
**68174 px of `#main` behind the header** on 22 of 22 pages from 1056 px
up — it was 1026 px before, so the header fix widened it, deliberately, to
buy the removal of 830 px of horizontal overflow with four nav items and the
CTA unreachable. **Nothing in CSS can detect that mechanism**: media queries,
container queries AND length units all keep resolving `rem`/`em` at 16 px
while `getComputedStyle` reports 32 px (measured; `docs/02` §Reflow). The
remaining candidates are a fitted constant tied to the nav item count, or a
behaviour change at normal settings. **Ticked by Pouya fixing (a) and ruling
on (b) — not by re-measuring with a method that reports zero**
- [ ] ⚠️ **THE SEVENTH NAV ITEM PLUS FALLBACK FONT METRICS — A LAYOUT SHIFT *AND*
A SKIP-LINK FAILURE, AT THE DEFAULT TEXT SIZE, ARMING ITSELF WHEN THE SECOND
INSIGHTS ARTICLE PUBLISHES.** No reader setting is 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
nav item present, 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 throughout. Two consequences: **(i)** a **60 px
collapse** when Geist swaps in, on all 22 pages, against the CLS &lt; 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 "Skip
to content", because 141 px exceeds the 97 px `scroll-padding-top` — **this
half IS new**, the previous build's 86.97 px stayed under 97 px and covered 0.
**Latent, not shipped:** six items never wrap, and `showInsights` turns the
seventh on at two published articles. Two candidate fixes, both Pouya's call
because both are outside "the header and its consumers": raise the desktop
breakpoint past 1091 px, which changes the normal-settings layout in that
band, or give Geist a metric-matched `size-adjust` fallback. Raised by
`adversarial-reviewer`, 2026-09-01; `docs/02` §Reflow carries the measurements
**Infrastructure** **Infrastructure**
- [ ] S3 versioning enabled - [ ] S3 versioning enabled
+60 -25
View File
@@ -181,9 +181,11 @@ const isHome = path === '/';
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
/* 48px, not 44. Still clears the touch floor, and it reserves the height the /* 48px, not 44. Still clears the touch floor, and it reserves the height the
two-line brand takes at >=76rem so the sticky header is one constant 81px two-line brand takes at >=76rem so the sticky header is 81px at every width
across every width where it is sticky — which is what --header-h and where it is sticky — AT THE DEFAULT TEXT SIZE, which is what `--header-h`
scroll-padding-top are keyed to. One number instead of two bands. */ and `scroll-padding-top` are keyed to. One number instead of two bands.
Above the default the masthead wraps and is taller on purpose, and the
sticky gate at the 66rem block is what keeps that safe. */
min-block-size: 48px; min-block-size: 48px;
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
@@ -204,18 +206,13 @@ const isHome = path === '/';
same words as the hero eyebrow (docs/01). */ same words as the hero eyebrow (docs/01). */
.brand-tagline { .brand-tagline {
display: none; display: none;
/* HELD AT 11px, BELOW THE `.eyebrow` THIS ELEMENT CARRIES. Measured at the /* HELD AT 11px, BELOW THE `.eyebrow` THIS ELEMENT CARRIES. At 14px the header
LIVE token value, 14px, on 2026-08-31 — earlier figures here were taken at WRAPS and stands at 144.98px instead of 81.00px — at 1216 with six nav items,
13px and understated all three costs. (1) `--header-h` is 81px and the and at EVERY width from 1216 up with a seventh. Nothing overflows and the CTA
header goes to 84.98px; `global.css` drives `scroll-padding-top` from that stays on the content edge, so the cost is now 64px of header height on every
token. (2) The CTA lands past `.header-inner`'s content edge — 1.3px at page: larger than the pre-wrap cost it replaces, and visible rather than
1216 with the SIX nav items shipping today, and 67.8 / 43.8 / 51.8 / 51.8px invisible. `docs/02` §Reflow carries the superseded figures. Insights is that
at 1216 / 1240 / 1280 / 1440 with a seventh. It does not resolve as the seventh item; `showInsights` turns it on at two published articles. */
viewport widens. Insights is that item; `showInsights` turns it on at two
published articles. (3) ⚠️ AND AT 14px IT DOES GROW THE DOCUMENT: 20px of
overflow at 1216 and 4px at 1280 with seven items. At 13px it did not,
which is why this comment used to say a page-level check could not see it.
At 11px all three are zero, with six items and with seven. */
font-size: var(--text-2xs); font-size: var(--text-2xs);
white-space: nowrap; white-space: nowrap;
} }
@@ -387,14 +384,30 @@ const isHome = path === '/';
Sticky only from here up, too. Below this the nav takes a second row and Sticky only from here up, too. Below this the nav takes a second row and
the header stands at 137px, which is more of a small viewport than a the header stands at 137px, which is more of a small viewport than a
sticky header is worth. Deviation from docs/02 "Sticky"; recorded there. */ sticky header is worth. Deviation from docs/02 "Sticky"; recorded there.
That same reasoning is what the sticky gate below extends to text size: a
tall header is not worth sticking whether the height comes from a narrow
viewport or from large type. */
@media (min-width: 66rem) { @media (min-width: 66rem) {
.site-header { .site-header {
position: sticky; position: sticky;
inset-block-start: 0; /* ⚠️ STICKY ONLY WHILE THE MASTHEAD IS ONE ROW. A media query cannot say so:
} its `rem` resolves against the browser's DEFAULT font size, a property's
.header-inner { against the root element. TWO terms, both load-bearing —
flex-wrap: nowrap; `100vw - 66rem` catches a root ABOVE the default (the row wraps and the
header stands 244351px); `1rem - 16px` catches a root BELOW it, where the
80rem content cap shrinks faster than the header's px minimums and the row
wraps at EVERY viewport width — 65px of `#main` sat behind the header at
Chrome's "Very small" (9px) with only the first term. `* 100000` saturates
because wrapping is a step and a ramp left 1069px covered across roots
1830; `-100vh` bounds the result. Both terms are >= 0 at the default size,
so this is exactly `0px`. `docs/02` §Reflow carries the measurements and
the cases where the offset can still be short. */
inset-block-start: clamp(
-100vh,
min(calc((100vw - 66rem) * 100000), calc((1rem - 16px) * 100000)),
0px
);
} }
.brand { .brand {
margin-inline-end: var(--space-5); margin-inline-end: var(--space-5);
@@ -402,12 +415,20 @@ const isHome = path === '/';
.nav { .nav {
flex-basis: auto; flex-basis: auto;
} }
/* nowrap, and flex:none so the nav is never squeezed below its content /* ⚠️ NOTHING HERE MAY SAY `flex-wrap: nowrap`, ON `.header-inner` OR ON
width. Measured before this: at 960-1250px the seven-item nav broke to `.nav-list`. `.header-inner`'s was the binding one, measured necessary AND
two rows and the header stood at 141px instead of 81px. */ sufficient: a `nowrap` line cannot break, so at a 200% text size the row ran
944px past a 1280px viewport with Practice, Fees, Contact and the CTA
off-screen. `.nav-list`'s is INERT — identical at every width and text
setting, six items and seven — and stays removed only so this prohibition is
not contradicted by a `nowrap` in the same file. Wrapping is the only
mechanism that reflows under all THREE ways a reader enlarges text, because
it is driven by used sizes rather than by a query.
⚠️ "It never wraps above this breakpoint" holds only with the WEBFONTS
LOADED: under fallback metrics with a seventh nav item the header is 141px
across 10561091, which is both a 60px swap-in shift and 44px of `#main`
behind the sticky header. Latent — six items never wrap. `docs/02` §Reflow. */
.nav-list { .nav-list {
flex-wrap: nowrap;
flex: none;
/* 16px from 66rem, widening to 24px at 80rem — see that block; the 76rem /* 16px from 66rem, widening to 24px at 80rem — see that block; the 76rem
block deliberately does NOT widen it. Measured with seven items at every block deliberately does NOT widen it. Measured with seven items at every
width from 1024px up. */ width from 1024px up. */
@@ -464,6 +485,20 @@ const isHome = path === '/';
.brand-tagline { .brand-tagline {
display: block; display: block;
} }
/* THE STICKY GATE MOVES WITH THE TAGLINE, and this is the same threshold the
binary search above produced: with the tagline the one-row masthead fits
from 1207px = 75.4rem, so 66rem is no longer the width it needs. Gating the
wider band on 66rem left the header sticky and wrapped from root 18 up —
46px of `#main` behind it at 1216/1280, 51px at 1440, 69px at 1920 — while
`/` measured clean throughout, because `/` is the one page that suppresses
the tagline. Each band gates on the width ITS layout requires. */
.site-header {
inset-block-start: clamp(
-100vh,
min(calc((100vw - 76rem) * 100000), calc((1rem - 16px) * 100000)),
0px
);
}
} }
@media (min-width: 80rem) { @media (min-width: 80rem) {
+5 -9
View File
@@ -368,15 +368,11 @@ const PROCESSES = [
.sheet-head { .sheet-head {
padding-block-end: 8pt; padding-block-end: 8pt;
} }
/* ⚠️ `font-weight` IS FROZEN AT 400, AND FROZEN RATHER THAN CHOSEN. These /* ⚠️ `font-weight` IS FROZEN AT 400 BY RULING, 2026-09-01. The circulated
elements carried no weight of their own before 2026-08-31 and rendered at PDF's typography changes only when its CONTENT is deliberately revised,
400; taking `.eyebrow` gave them its 500, which changed the PRINTED sheet — never as a side effect of a screen refactor — so print keeps 400 while
a visual change to the artefact circulated with an appointment proposal, screen takes the 500 every other eyebrow has. `docs/02` §Accessibility
from a ruling that was about SIZE. `docs/02` §Accessibility floor carries floor carries the reasoning and the byte figures. */
the byte figures. So print keeps 400
and the committed PDF stays byte-identical; screen takes the 500 every other
eyebrow has. ⚠️ POUYA'S CALL WHETHER TO UNIFY: if he does, this declaration
goes and `npm run bio:pdf` must be re-run and re-committed. */
.block h2, .block h2,
.sheet-strap { .sheet-strap {
font-weight: var(--weight-normal); font-weight: var(--weight-normal);
+5 -1
View File
@@ -121,7 +121,11 @@ html {
/* The header is sticky from 66rem up, and `scroll-padding-top` has to clear it /* The header is sticky from 66rem up, and `scroll-padding-top` has to clear it
or "Skip to content" drops the reader behind it — the one control that exists or "Skip to content" drops the reader behind it — the one control that exists
specifically for keyboard users, landing them on content they cannot see. specifically for keyboard users, landing them on content they cannot see.
--header-h is defined in tokens.css beside the value it has to match. */ `--header-h` is defined in tokens.css beside the value it has to match, and it
is a FLOOR — above the default text size the masthead reflows and is taller, and
`SiteHeader` gates stickiness so the offset is generous rather than short.
⚠️ TWO SETTINGS DEFEAT THAT GATE and leave this offset short; `docs/02` §Reflow
names them and shows why no CSS can detect either. */
@media (min-width: 66rem) { @media (min-width: 66rem) {
html { html {
scroll-padding-top: calc(var(--header-h) + var(--space-4)); scroll-padding-top: calc(var(--header-h) + var(--space-4));
+14 -9
View File
@@ -137,15 +137,20 @@
/* --- Layout ------------------------------------------------------------ */ /* --- Layout ------------------------------------------------------------ */
/* Sticky-header height at >= 66rem, where the header IS sticky. global.css /* ⚠️ A FLOOR, NOT A CONSTANT: the sticky header's height AT THE DEFAULT TEXT
drives `scroll-padding-top` off this, so the skip link does not drop the SIZE. Above the default the masthead reflows and is deliberately taller, which
reader behind the header. If SiteHeader's padding or nav sizing changes, is why the nav no longer runs off-screen. `global.css` drives
re-measure and change this with it — one fact living in two files. `scroll-padding-top` off it and `SiteHeader` gates stickiness so the offset is
[measured 2026-08-26 — headless Chrome at 1024/1100/1280/1440px, with six generous rather than short — **but not in every case**: `docs/02` §Reflow lists
nav items and with a seventh injected. 81px at every one: 32 padding + 48 the two settings where it is still short and why no CSS can see them. If
reserved brand block + the 1px bottom border, which is easy to forget and SiteHeader's padding or nav sizing changes, re-measure and change this with
is why this is measured rather than added up. The brand reserves 48px so it — one fact living in two files.
the height does not change when the tagline appears at 76rem] */ [measured 2026-08-26, re-measured 2026-09-01 — headless Chrome, six nav items
and a seventh injected. 81.00px at all EIGHT widths from 1056 to 1920px
(1056/1100/1216/1240/1280/1440/1600/1920): 32 padding + 48 reserved brand
block + the 1px bottom border, which is easy to forget and is why this is
measured rather than added up. The brand reserves 48px so the height does not
change when the tagline appears at 76rem] */
--header-h: 5.0625rem; /* 81 — measured, not chosen */ --header-h: 5.0625rem; /* 81 — measured, not chosen */
--width-content: 80rem; /* 1280 */ --width-content: 80rem; /* 1280 */