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:
co-authored by
Claude Opus 5
parent
0f7595b602
commit
fce89d46eb
@@ -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 1056–1091 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.59–350.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 10–69 px, across roots 18–30 — 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 63–74**, 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 9–15 (70), roots 16–32 × viewports 1056–1920 × three pages (300), the band seam at viewports 1180–1260 (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 1180–1260 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 224–243 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 1024–1100 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.58–270.56 px** there, so the skip link lands with **68–174 px of `#main` behind it** on 22 of 22 pages from 1056 px up. **It was 10–26 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 1086–1090 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 4–6 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 10–26 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 18–30 — **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, 10–69 px of `#main` behind the header, roots 18–30. 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 (328–350 → **244.59–350.86**), the minimum-font-size header range (228–271 → **164.58–270.56**) and its coverage range (132–174 → **68–174**), 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.58–2.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.
|
||||||
|
|||||||
@@ -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
@@ -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.59–350.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, 10–69 px, across
|
||||||
|
roots 18–30 — 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
|
||||||
|
16–32 × viewports 1056–1920 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
|
||||||
|
1180–1260 × roots 16–32). ⚠️ **`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.58–270.56 px | 97 px | **68–174 px** | live, unfixable in CSS |
|
||||||
|
| fallback font metrics + 7th nav item, **default** text size, 1056–1091 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.59–350.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 10–69 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 10–26 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 224–243 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
|
||||||
|
1024–1100 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.58–270.56 px** and the skip link lands with **68–174 px of `#main` behind
|
||||||
|
it**, on 22 of 22 pages from 1056 px up. It was **10–26 px before**, so this pass
|
||||||
|
widened it. *(These read "228–271 px" and "132–174 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.59–86.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 1086–1090 were checked and clear; they are all 141 px. It
|
||||||
|
is a contiguous **36 px** band, 1056–1091. 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
@@ -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
|
||||||
|
18–30. **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 9–15 (70), roots 16–32 (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
|
||||||
|
224–243 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.58–270.56 px** under that setting, so "Skip to content" lands with
|
||||||
|
**68–174 px of `#main` behind the header** on 22 of 22 pages from 1056 px
|
||||||
|
up — it was 10–26 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 < 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
|
||||||
|
|||||||
@@ -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 244–351px); `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 10–69px covered across roots
|
||||||
|
18–30; `-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 1056–1091, 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
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user