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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-09-01 12:34:50 -04:00
co-authored by Claude Opus 5
parent 0f7595b602
commit fce89d46eb
8 changed files with 731 additions and 125 deletions
+60 -25
View File
@@ -181,9 +181,11 @@ const isHome = path === '/';
align-items: center;
gap: var(--space-3);
/* 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
across every width where it is sticky — which is what --header-h and
scroll-padding-top are keyed to. One number instead of two bands. */
two-line brand takes at >=76rem so the sticky header is 81px at every width
where it is sticky — AT THE DEFAULT TEXT SIZE, which is what `--header-h`
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;
color: var(--accent);
text-decoration: none;
@@ -204,18 +206,13 @@ const isHome = path === '/';
same words as the hero eyebrow (docs/01). */
.brand-tagline {
display: none;
/* HELD AT 11px, BELOW THE `.eyebrow` THIS ELEMENT CARRIES. Measured at the
LIVE token value, 14px, on 2026-08-31 — earlier figures here were taken at
13px and understated all three costs. (1) `--header-h` is 81px and the
header goes to 84.98px; `global.css` drives `scroll-padding-top` from that
token. (2) The CTA lands past `.header-inner`'s content edge — 1.3px at
1216 with the SIX nav items shipping today, and 67.8 / 43.8 / 51.8 / 51.8px
at 1216 / 1240 / 1280 / 1440 with a seventh. It does not resolve as the
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. */
/* HELD AT 11px, BELOW THE `.eyebrow` THIS ELEMENT CARRIES. At 14px the header
WRAPS and stands at 144.98px instead of 81.00px — at 1216 with six nav items,
and at EVERY width from 1216 up with a seventh. Nothing overflows and the CTA
stays on the content edge, so the cost is now 64px of header height on every
page: larger than the pre-wrap cost it replaces, and visible rather than
invisible. `docs/02` §Reflow carries the superseded figures. Insights is that
seventh item; `showInsights` turns it on at two published articles. */
font-size: var(--text-2xs);
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
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) {
.site-header {
position: sticky;
inset-block-start: 0;
}
.header-inner {
flex-wrap: nowrap;
/* ⚠️ 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
against the root element. TWO terms, both load-bearing —
`100vw - 66rem` catches a root ABOVE the default (the row wraps and the
header stands 244351px); `1rem - 16px` catches a root BELOW it, where the
80rem content cap shrinks faster than the header's px minimums and the row
wraps at EVERY viewport width — 65px of `#main` sat behind the header at
Chrome's "Very small" (9px) with only the first term. `* 100000` saturates
because wrapping is a step and a ramp left 1069px covered across roots
1830; `-100vh` bounds the result. Both terms are >= 0 at the default size,
so this is exactly `0px`. `docs/02` §Reflow carries the measurements and
the cases where the offset can still be short. */
inset-block-start: clamp(
-100vh,
min(calc((100vw - 66rem) * 100000), calc((1rem - 16px) * 100000)),
0px
);
}
.brand {
margin-inline-end: var(--space-5);
@@ -402,12 +415,20 @@ const isHome = path === '/';
.nav {
flex-basis: auto;
}
/* nowrap, and flex:none so the nav is never squeezed below its content
width. Measured before this: at 960-1250px the seven-item nav broke to
two rows and the header stood at 141px instead of 81px. */
/* ⚠️ NOTHING HERE MAY SAY `flex-wrap: nowrap`, ON `.header-inner` OR ON
`.nav-list`. `.header-inner`'s was the binding one, measured necessary AND
sufficient: a `nowrap` line cannot break, so at a 200% text size the row ran
944px past a 1280px viewport with Practice, Fees, Contact and the CTA
off-screen. `.nav-list`'s is INERT — identical at every width and text
setting, six items and seven — and stays removed only so this prohibition is
not contradicted by a `nowrap` in the same file. Wrapping is the only
mechanism that reflows under all THREE ways a reader enlarges text, because
it is driven by used sizes rather than by a query.
⚠️ "It never wraps above this breakpoint" holds only with the WEBFONTS
LOADED: under fallback metrics with a seventh nav item the header is 141px
across 10561091, which is both a 60px swap-in shift and 44px of `#main`
behind the sticky header. Latent — six items never wrap. `docs/02` §Reflow. */
.nav-list {
flex-wrap: nowrap;
flex: none;
/* 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
width from 1024px up. */
@@ -464,6 +485,20 @@ const isHome = path === '/';
.brand-tagline {
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) {