Files
adr-sml/docs/02-design-system.md
T
Pouya LajevardiandClaude Opus 5 0f7595b602 feat: eyebrow 14px and one small-text floor; fix a site-wide reflow defect; stamp TTL; §4 bars the struck universal
Pouya's five rulings of 2026-08-31, after 64bce10. One commit, because AGENTS.md
entry (aj) covers all five and the review fixes interleave across them —
recorded here rather than left to be inferred.

1. THE EYEBROW, 13 -> 14px, at the one token definition. `--text-eyebrow` is
deliberately NOT an alias of `--text-sm` despite sharing its value: the two move
for different reasons, and aliasing would mean a change to body-meta type
silently moving every eyebrow. Sweep by rendering, 22 pages x 2 widths, 801 mono
instances: 441 carry `.eyebrow`, 420 now at 14px, 21 held at 11px. Zero of 801
below 4.5:1.

Header check, which Pouya asked for explicitly: nothing header-side consumes the
token, `--header-h` is 81px, and the sticky header measures exactly 81.00px at
every width from 66rem up with the CTA on `.header-inner`'s content edge. Nothing
overflowed, so nothing was improvised.

2. /contact/'s LABELS RAISED, AND THE FLOOR CLAIM REWRITTEN BECAUSE THE RULING'S
PREMISE WAS FALSE. The ruling asked docs/02 to record the tagline as "the ONLY
sub-14px text on the site". It is not: the `--text-xs` 12px rung has ten
declaration sites and 130 elements rendering at 12px. So docs/02 records the
claim that IS verified — `--text-2xs` has one consumer in the repository — and
enumerates the 12px rung as a separate treatment. Writing a false claim into the
spec because the conclusion was handed down is the move the rules forbid.

A third block moved that the ruling did not name: /bio/'s ten <h2>s and its strap
were copies of five of `.eyebrow`'s six declarations at 11px — the same escape the
footer headings were. docs/02 justified holding them by the one-page PDF
constraint; measured, `@media print` sets both to 7pt, so the screen size never
reached the PDF and that was never the reason.

Consolidating them DID change the printed sheet, because the missing sixth
declaration was `font-weight`: 500 grew the PDF 89,496 -> 91,151 bytes. Print
therefore freezes 400, the committed PDF stays byte-identical (10 differing
bytes, all /CreationDate and /ModDate), and unifying is Pouya's call.

3. THE REFLOW DEFECT WAS SITE-WIDE AND /bio/ WAS NOT THE BINDING CAUSE.
`white-space: nowrap` on `SiteHeader .brand-name` held all 22 pages 63px over at
320px/root 32. Eight cause-specific fixes, no `overflow-x` anywhere; all eight
produce byte-identical geometry at root 16. `Pill` is marked in docs/02 as the
backstop it is — the real cause was `PracticeCard`'s rem-based padding, and with
it clamped "Construction" goes from 94x220 in six two-character lines to 158x85 in
two.

Three instrument findings now in docs/02, each of which hid a real defect:
`break-word` does not reduce min-content and `anywhere` does;
`getBoundingClientRect()` reports border boxes, so an element sweep cannot see
text spilling outside its own box; and `mobile: true` emulation expands the
layout viewport, so `scrollWidth - innerWidth` reads 0 while the reader still
scrolls sideways.

4. TTL STAMPED `ENABLED` in §7, with `DISABLED` at first verification recorded
rather than overwritten. Q60 narrowed to its second half and OPEN: no record has
been watched to vanish, and `ENABLED` proves the setting, not the behaviour.
R19's sweep found three stale copies outside §7; all now defer to it. R19 itself
was not edited — it points at §7 rather than carrying state, which is the
property that made it work.

5. §4 GAINS THE STRUCK-UNIVERSAL ROW, citing the committed ontario.ca extract and
cross-referenced to `check:claims`'s `struck-universal-q39`. It bars the claim in
BOTH directions: the commercial half is Pouya's attributed position, not a
verified fact. The row immediately caught two places asserting it flatly —
`SiteHeader.astro` and §9 Q33 — both now attributed. `check:claims` unmodified;
still frozen.

REVIEW: adversarial-reviewer, two rounds, 16 findings, ALL ACCEPTED, NONE
DECLINED. claims-auditor did not run (D20). Eight of round 2's ten were defects
in round 1's own fixes.

Round 1's blocking finding was a defect in my own record: I wrote that every
reflow residual was zero at "root 16 and root 32, 286 measurements". Two ways of
simulating 200% text are NOT equivalent, because media-query `rem` resolves
against the DEFAULT font size, not the root element's. Under the method docs/02
itself prescribes the site is 944px over on 21 pages and 508px on / at 1280, and
304px at 1920 — nav clipped mid-word, Practice/Fees/Contact and the CTA
off-screen, WCAG 1.4.4 with loss of functionality. My 286 excluded exactly the
widths where the defect lives.

NOT FIXED, DELIBERATELY: the cause is `flex-wrap: nowrap` on `.nav-list` above
66rem, a locked step-1 decision, and the standing instruction is to stop and
report rather than improvise a header change. It is now a blocking item on
docs/06's cutover checklist, ticked only by ruling on it — "not by re-measuring
it with the method that reports zero".

Round 2 also caught: a 63,743-byte figure that was the `cmp -l` differing-byte
count rather than the 1,655-byte size delta; a lost-navigation list naming
Med-Arb, which is not in the masthead at any width, while omitting Contact; the
tagline's justification left recorded at 13px, where one clause of it is false at
14px (20px of document overflow at 1216 with a seventh nav item); my own label
raise reintroducing a 38px element overflow at the width just ruled on; two
copies of the eyebrow treatment left at weight 400; three stale residual tables;
and the min-content explanation duplicated six times in src/.

VERIFIED, exit statuses read directly, never through a pipe: build 0 (22 pages),
check 0 (0 errors/0 warnings/0 hints), check:claims 0, og:proof 0, check:intake 0,
lint 0, minifier tripwire clean, TODO in dist 0 with a source sanity check.
Overflow: 374 measurements over 22 pages, every one zero, row counts asserted
before reading, with positive controls (119px at width 200, 319px at root 64).
Lighthouse 0, run three times with identical category scores: perf 99 on / and
100 elsewhere, a11y 100, best practices 100, SEO 100 on all indexable pages,
CLS 0.000. / sits at LCP 2.03s against a 2.0s budget, unchanged by this work.

The zsh `$VAR` word-split trap fired twice more and both times read as a clean
pass; caught only by asserting row counts first. Two of my own instruments were
wrong before they were right: a `grep -F '0.875rem'` state check that could never
match because Lightning CSS writes `.875rem`, and a probe whose inline
`!important` was outranked by a running CSS transition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
2026-08-31 15:44:13 -04:00

33 KiB
Raw Blame History

02 — Design system

Authority: AGENTS.md §3 D7 — keep the palette and the infinity mark; modernize the execution. The look is not up for redesign. What follows is the system that preserves it while fixing what the old build got wrong.


What carries over unchanged

  • The palette: cream, ink, maroon, gold.
  • The infinity mark — SML Company Ltd.'s actual logo, and the metaphor holds: a dispute is a loop, and the work is redrawing the loop into a line. It is a shaded ribbon, not a stroked curve: a band of variable width that twists in three dimensions, maroon flowing into champagne, passing over itself at the crossing. Ink bounding box 2668 × 1704 = 1.5657:1 [verified 2026-08-26 — measured against the master]. Source of truth: src/assets/brand/sml-infinity-mark.png; provenance in docs/reference/brand-assets.md.
  • The type pairing: Instrument Serif for display, Geist for text, Geist Mono for eyebrows and labels.
  • The editorial register — generous whitespace, restrained colour, serif display type at large sizes.

What changes

Was Is Why
Google Fonts at runtime Self-hosted, subset, preloaded Removes a render-blocking third-party round trip from a page that collects legal inquiries
Fixed px type sizes Fluid clamp() scale One scale from 360 px to 1600 px with no breakpoint jumps
Ad hoc spacing values 8 px base scale Consistent vertical rhythm; no magic numbers
Gold used as a text colour on cream Gold restricted to decorative and on-dark It fails WCAG AA at 2.10:1. Measured, not assumed
Scroll-reveal on every element, always on Reveal on major sections only, gated behind prefers-reduced-motion Motion that reads as confident rather than decorative; accessible by default
2.2 MB single file, ~1 MB of base64 logos (both figures under review — AGENTS.md Q34; and "470 KB PNG", which this row used to assert, has no source anywhere in the repo and has been removed) Optimized SVG mark, AVIF/WebP photography The mark is geometry, so it should be vector. That holds whatever the old file weighed
React 18 dev build + Babel Standalone in the browser Static HTML, near-zero JS The reason the site is invisible to crawlers

Colour

Tokens live in src/styles/tokens.css. Never write a raw hex value in a component.

Token Value Use
--cream #faf7f2 Page background
--cream-2 #f3ede0 Alternating section background
--cream-3 #ebe3d1 Cards and insets on cream
--ink #1a1614 Body text; dark section backgrounds
--ink-soft #3a322c Secondary text
--muted #6e6359 Metadata, captions — on cream only
--maroon #5a1a1c Primary action, accents, dark panels
--maroon-d #3d1112 Hover on maroon
--maroon-l #7a2a2c Links on cream
--gold #c9a876 Rules, dividers, on-dark accent — never text on cream
--gold-d #a88858 Large decorative text on cream only
--gold-l #e2c89a Text on ink or maroon

Contrast — measured, 2026-08-25

Against --cream #faf7f2:

Colour Ratio AA body (4.5) AA large (3.0)
--ink #1a1614 16.81 pass pass
--ink-soft #3a322c 11.75 pass pass
--maroon #5a1a1c 12.29 pass pass
--maroon-l #7a2a2c 8.95 pass pass
--muted #6e6359 5.47 pass pass
--gold-d #a88858 3.11 FAIL pass
--gold #c9a876 2.10 FAIL FAIL

Against --maroon #5a1a1c: cream 12.29 · gold-l 8.11 · gold 5.84 — all pass. Against --ink #1a1614: cream 16.81 · gold-l 11.09 · gold 8.00 — all pass. --muted on ink measures 3.07 and fails; use --gold-l or cream at reduced opacity for secondary text on dark.

Hard rules.

  1. --gold is never a text colour on cream. Rules, borders, dividers, icon strokes, and on-dark text only.
  2. --gold-d on cream only at 24 px+ / 19 px bold, and only for decorative display text — never for anything a reader must parse.
  3. Body text on cream is --ink or --ink-soft. Metadata may use --muted.
  4. Secondary text on dark is --gold-l or --cream at ≥ 70% opacity.
  5. No dark mode. A single committed light identity is the right call for a legal practice, and halving the surface area halves the ways contrast breaks.

Typography

Faces. Instrument Serif (display) · Geist 300/400/500/600 (text) · Geist Mono 400/500 (eyebrows, labels, data).

Self-host all three. Subset to Latin + Latin Extended-A. font-display: swap. Preload only the two faces used above the fold — Instrument Serif regular and Geist 400.

Scale. Fluid, clamp(), 1.25 ratio at the small end widening to 1.333 at the display end. Tokens --text-2xs through --text-6xl in tokens.css, plus --text-eyebrow at 14 px — the same value as --text-sm and deliberately not an alias of it; see the eyebrow rule below.

Rules.

  • Display type (--font-serif) at --text-4xl and above only. It has almost no hinting at small sizes and looks weak below 32 px.
  • Display line-height 0.951.05; letter-spacing -0.02em.
  • Body line-height 1.6. Measure capped at 68ch — the old site ran full-bleed paragraphs at 1400 px, which is unreadable.
  • Eyebrows: mono, 14 px (--text-eyebrow), 0.18em tracking, uppercase, weight 500, always paired with a real heading. Pouya raised it 12 → 13 → 14 px on 2026-08-31, in two passes, because uppercase mono at this tracking reads smaller than it measures and 13 px was still getting lost against the display type. There is one definition, .eyebrow in global.css. An eyebrow is not a heading and never carries the <h*> — but a real heading may carry the class, and fourteen do, in two groups: the footer's four column headings (SiteFooter.astro) and /bio/'s ten block headings. ⚠️ "ONE DEFINITION" HAS ONE CARVE-OUT AND IT IS NAMED RATHER THAN IMPLIED: /contact/'s <label>/<legend> set and its direct-contact <dt>s repeat the treatment instead of taking the class, because they need --text-secondary (11.75 : 1) rather than .eyebrow's --text-meta (5.47 : 1) — a form label is operative text. Everything else about them matches, font-weight included; without that they rendered at 400 beneath a p.eyebrow of the same size and colour, which is the divergence a carve-out is meant to bound rather than hide. Do not widen this to any other element. 14 px is the same value as --text-sm, and --text-eyebrow is deliberately not an alias of it. The two move for different reasons; aliasing would mean a future change to body-meta type silently moved every eyebrow on the site. (This bullet said "13 px is not a rung on the scale — it sits between --text-xs and --text-sm" for one revision. At 14 px it is that rung.)
  • THE 11 px FLOOR HAS EXACTLY ONE CONSUMER, AND THAT IS THE CLAIM THAT IS TRUE. Pouya's ruling, 2026-08-31: "form labels are operative text, and the site should have exactly one small-text floor." --text-2xs (11 px) now has one consumer in src/ — the header tagline (git grep 'var(--text-2xs)' -- src returns one hit, SiteHeader.astro; 21 rendered instances, every one span.eyebrow.brand-tagline) — and nothing else may use it without a measurement recorded beside it. ⚠️ THIS BULLET SAID "THE SMALL-TEXT FLOOR IS 14 px, AND THERE IS EXACTLY ONE EXCEPTION" FOR ONE REVISION, AND THAT WAS FALSE — falsified by this document thirty lines below, which concedes a 12 px family. The ruling asked for the record that the tagline is the only sub-14 px text on the site; it is not, so that is not what is recorded here. What is true is the sentence above: one consumer of the 11 px token. --text-xs (12 px) is a separate rung and is enumerated below. 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 --header-h is 81 px, which global.css feeds into scroll-padding-top; and with a seventh nav item the CTA lands past .header-inner's content edge at 1216, 1240, 1280 and 1440 — it does not resolve as the viewport widens. Insights is that seventh item. Both measurements are in SiteHeader.astro, and document overflow is 0 in every one of those cases, so no page-level check can see it. 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 ratify, superseding the "accepted, not ratified" note this bullet carried for one revision. (b) /contact/'s direct-contact <dl> terms, which are label text on the same page and would otherwise have been left a step behind the labels beside them. (c) /bio/'s ten block headings and its strap, which were copies of five of .eyebrow's six declarations, at 11 px — the same escape the footer's column headings turned out to be — and now carry the class. Not byte-for-byte: the font-size differed, and the missing sixth was font-weight, which is precisely the one the print block now has to freeze. ⚠️ (c)'s carve-out was justified by a reason that did not apply. This bullet said the /bio/ sheet was held at 11 px because npm run bio:pdf holds it to one page. Measured 2026-08-31: the @media print block sets both elements to 7 pt, so the screen size never reached the PDF and the one-page constraint was never what kept them small. Print does now freeze font-weight: 400, because taking .eyebrow's 500 changed the printed sheet — the PDF grows from 89,496 to 91,151 bytes, +1,655 — and the ruling that moved them was about 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, 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 design position — see bio.astro. --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, DefinitionGrid, ArticleCard, ProcessStep (two), insights/[...slug].astro, and /bio/'s two — .fine and .sheet-contact p. The first eight are mono and uppercase or tracked at 0.06em, a third of the eyebrow's tracking, which is what makes them a different treatment. /bio/'s two are neither uppercase nor tracked, so they are not in that family at all — they are simply 12 px text, and an earlier draft of this bullet defined the exception in a way that excluded them and therefore missed them. Rendered count, and say what it is a number of: at 1280 px / root 16 across all 22 pages, 130 elements carrying their own text compute to 12 px (149 if elements that merely inherit the size are counted). ArticleCard and the article meta contribute zero today — no article is published, so neither renders. Every instance measures ≥ 5.01 : 1.
  • Italic display (.it) is the one flourish the design allows. One italic phrase per headline, at most.
  • Never skip a heading level. <h1> once per page.

Spacing and layout

8 px base: --space-1 4px · -2 8 · -3 12 · -4 16 · -5 24 · -6 32 · -7 48 · -8 64 · -9 96 · -10 128 · -11 160.

Content width 1280px; prose measure 68ch; wide media 1440px. Gutters: 24px mobile, 48px desktop. Section rhythm: --space-9 (96 px) mobile, --space-11 (160 px) desktop — --section-y in tokens.css. Corrected 2026-08-26: the curve was 6vw + 2rem, which reaches 160 px only at a 2133 px viewport, so the desktop half of this line was never delivered (measured 108.8 px at 1280 px, 128 px at 1600 px). It is now 9vw + 1rem, which reaches 160 px at 1600 px. If you change the curve, re-measure — a clamp() whose upper bound is unreachable reads exactly like one that works.

Grid: 12 columns desktop, 6 tablet, 4 mobile, --space-5 gutter.


Motion

The old build animated nearly everything on scroll. The replacement is deliberate and quiet.

  • Reveal on section entry only — not on every child element. Sub-element stagger is limited to card grids, and capped at six children.
  • Duration 600ms, easing cubic-bezier(.2,.7,.2,1). Transform and opacity only — never layout properties.
  • Implement with animation-timeline: view(), behind @supports. Amended 2026-08-26: the IntersectionObserver alternative this line used to offer first is now ruled out, not merely second choice. It has to run inline in <head> to avoid a flash, and 05-backend-spec.md specifies script-src 'self' with no unsafe-inline — so the only script on the site would have been the one thing the site's own CSP refuses to execute, and a per-build hash drifts from the policy pinning it. The CSS route ships zero JavaScript. Not a framework, not a library, not a script.
  • Content is visible without the feature. The @supports gate is load-bearing, not defensive: without it, a browser that ignores animation-timeline runs the animation once against the document timeline at load; with it, that browser gets no animation and fully visible content. The old build had this exactly backwards and shipped a blank page.
  • And without a print timeline. A scroll-driven animation has no timeline when printing, so a revealed element renders at its from state — opacity: 0. Measured 2026-08-26: before the print override existed, printing a page to PDF dropped four card headings from the output entirely. /about/ is written to be printed by people evaluating an appointment.
  • Hover transitions 250ms.
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Nothing animates infinitely. Nothing autoplays. Nothing moves on page load except the reveal of the hero.


Components

Component Notes
InfinityMark ⚠️ Currently a raster — a documented, temporary exception to this rule (AGENTS.md Q38, R13). The mark is gradient-mesh artwork rather than flat vector paths. An SVG is held and it renders faithfully — and it renders faithfully because it IS the raster: 257,278 bytes wrapping seven embedded base64 PNGs, against 3,063 B for the AVIF a Retina browser takes in the header — 84×. Figures re-stated 2026-08-27 because a second, larger call site now exists and the single number had become misleading: the mark ships at two intrinsic widths, 64 px (header, footer) and 232 px (the home page's approach section, which renders at 225.5 px). At 232 px a DPR-2 device takes 14,555 B and DPR-3 22,639 B, so the ratio against the SVG there is ~11×, not 84×. The full ladders are in docs/reference/brand-assets.md; do not quote one number as if it covered both. Inlining it would breach CLAUDE.md's no-base64-images rule. Pouya settled the characterisation on 2026-08-27: a first draft called the file "a raster in a vector wrapper", a later draft withdrew that as unfair, and the withdrawal went too far. Both halves are true — the fidelity is real, and it is bought with embedded raster, which is exactly why fidelity was never the question. The exception is about payload and composition. It renders AVIF/WebP with a PNG fallback; a Retina device takes 3,063 B of AVIF, alt="" when decorative, aspect ratio pinned to 667 / 426. Restore this rule the moment the commissioned vector master lands. Until then the rule stands unchanged for every other mark or icon: inline SVG, currentColor, aria-hidden when decorative, never a PNG
SiteHeader Sticky from 66 rem (1056 px) up; static below it. Practice dropdown as CSS-only <details>. (a) The one-row header holds the brand, seven nav items and the CTA — Insights is the seventh, arriving on its own at build step 7. Binary search on the built page puts the true fit at 1047 px; 66 rem is the clean token above it. Below 66 rem the nav takes its own row and the header measures 141 px at tablet widths and 189 px at 320375 px. (b) "Condenses on scroll" is a hairline rule and a shadow, not a size change — a position: sticky header stays in normal flow, so shrinking its padding lifts every page below it, against the CLS < 0.05 budget. Longhands only; see the component on what the minifier does to the animation shorthand. (This row has been wrong twice, instructively. It first said 60 rem / "~115 px", a height the header never took. It then said 64 rem "with 32 px of clearance" — that 32 px was .header-inner's own column-gap mistaken for slack; the real figure at 1024 px with seven items was 21.6 px, and nothing overflowed only because flexbox crushed the logo inside the brand block. Measure slack, not gaps.)
SiteFooter Three-column sitemap, contact block, designations, entity line
Eyebrow Mono label with optional maroon dot
SectionHeading Eyebrow + display heading + optional lede, one measure
Button Variants primary (maroon) · ghost (outlined) · gold (ink bg, gold-l text). Renders <a> or <button> correctly
Pill Small bordered label for designations and sector chips
CredentialRow Three or four credential slots. Never matter countsAGENTS.md §4
PracticeCard Sector chip, heading, one paragraph, arrow link
ProcessStep Numbered step, timing, body
ArticleCard Title, description, date, topic pills, reading time
Prose Long-form wrapper. Owns all typographic defaults for MDX
SEO Title, description, canonical, OG, Twitter, JSON-LD — see 04-seo-spec.md

Focus states. Every interactive element gets a visible focus ring: outline: 2px solid var(--maroon); outline-offset: 3px. Use :focus-visible. Never outline: none without a replacement — the old build removed it globally.


Accessibility floor

Not a polish pass. A build requirement.

  • One <h1> per page; heading levels never skipped.

  • Landmarks: <header>, <nav>, <main>, <footer>. Skip-to-content link first in tab order.

  • Every image has alt. Decorative images get alt="".

  • Colour never carries meaning alone.

  • All functionality reachable by keyboard; focus order matches visual order.

  • Forms: real <label> elements, aria-describedby for hints, errors announced with role="alert" and tied to their field.

  • Touch targets ≥ 44 × 44 px.

  • Test at 200% zoom and at 320 px width. Both measured 2026-08-27 on /: document overflow 0 at 320, 360, 390, 414, 640, 768, 900, 1024, 1056, 1200, 1216, 1280, 1440 and 1920 CSS px, with zero elements extending past the viewport. Page zoom at 200% of 1280 is the 640 column and at 400% is the 320 column, so WCAG 1.4.4 and 1.4.10 are both covered by that sweep.

    A stricter case is not fully clean, and it is recorded rather than left to be discovered. With the reader's default font size at 200% — root at 32 px, a real accessibility setting and not page zoom — / measured 234 px of overflow at 390. Brought down in three measured steps:

    ⚠️ THE THREE TABLES BELOW ARE A HISTORY OF HOW THE NUMBER CAME DOWN, NOT THE CURRENT STATE. Every residual in their final rows — the 3 px, the 23 px and the 63 px — went to 0 on 2026-08-31, on all 22 pages at 320 / 360 / 390 / 414 / 640 / 1024 px. The current state is §Reflow below, which also carries what is still NOT zero at 1280 px and 1920 px. Read the rows as dated steps.

    Fix 390 px 320 px
    as first built 234 px 304 px
    minmax(min(Nrem, 100%), 1fr) on three grids 83 px 153 px
    .credentials made explicit repeat(2, minmax(0, 1fr)); .feature padding clamped and overflow-wrap: anywhere on its title; .contact-action flex: 0 1 auto + min-inline-size: 0 3 px 63 px

    65 px corrected to 63 px on 2026-08-28, re-measured independently on the same page and setting. Two pixels, and it is recorded because a table that reads as the site-wide record has to be re-measurable rather than remembered.

    /about/ added 2026-08-28 — step 3, and the first page to be measured against this table rather than establishing it:

    Page and fix 390 px 360 px 320 px
    /about/ as first built 38 px 68 px 108 px
    .designation-part white-space: nowrap removed (the separator is held by an NBSP instead) and overflow-wrap: anywhere on .hero-h 0 px 23 px 63 px

    Step 4's three pages added 2026-08-28, and they needed no fix of their own — the guards this table produced were applied while writing them rather than after measuring:

    Page 390 px 360 px 320 px
    /mediation/ 3 px 23 px 63 px
    /arbitration/ 3 px 23 px 63 px
    /med-arb/ 0 px 23 px 63 px

    At the default root size all three are 0 px at every width measured. ⚠️ THE ELEMENT-LEVEL SWEEP BELOW IS ALSO HISTORICAL. It read: "the element-level sweep at 320/root-32, predicate right > clientWidth, names three PracticeCard chips on /mediation/ — Construction, Technology, Shareholder — plus the header and footer brand elements, which appear on all five pages." Re-run with that exact predicate on 2026-08-31: 0 elements, on all five pages at 320 / 360 / 390 with root 32. The chips were fixed at their cause (PracticeCard's padding) and the brand elements at theirs; see §Reflow. (This read "six" for one pass. Six is the count of article.card with scrollWidth > clientWidth, a different predicate on a different element; adversarial-reviewer re-ran it and no predicate yields six chips. A number in this table has to be re-runnable, which is the whole reason the table exists.) All three are byte-identical to /'s — same widths, same offsets, same text — so step 4 introduced no new element-level overflow.

    Two findings from that page specifically. The nowrap was introduced as a fix for an orphaned · at the end of a wrapped line, and it made the whole designation item unbreakable — a fix that created a reflow regression, caught only because this table existed to regress against. And the type scale is rem-based, so an <h1> at --text-5xl computes to 88 px at root 32 and a single unbreakable 9-character name ("Lajevardi") exceeds the 224 px content box at 320 px; overflow-wrap: anywhere is the only remedy that reduces min-content size. /about/ now measures equal to or better than / at all three widths. (Its 320/360 residual was recorded here as "the same header decision" until 2026-08-31, when that attribution turned out to be wrong and the residual went to 0 — see below.)

    Command, so the numbers are re-runnable rather than quoted: headless Chrome over the built dist, document.documentElement.style.fontSize = '32px', then documentElement.scrollWidth - documentElement.clientWidth, plus an 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 THIS PARAGRAPH CLAIMED THEY WERE. Pouya ruled the narrow case a defect on 2026-08-31 — "the 152/112/82 px overflows at 320/360/390 with 32 px root text violate WCAG 1.4.10 and are not accepted" — and that case is fixed. The wide case is a different cause, is not fixed, and is now stated correctly rather than reported as zero.

    TWO METHODS OF SIMULATING 200% TEXT, AND THEY ARE NOT EQUIVALENT. This is the whole of the error. Every earlier number in this section came from the command below — documentElement.style.fontSize = '32px'. A later sweep used Chrome's Page.setFontSizes({standard: 32}) instead and reported 0 everywhere, and the prose generalised that to "at root 16 px and root 32 px".

    media query 66rem resolves to header above 1056 px 1280 px, root 32
    documentElement.style.fontSize='32px' 1056 px — matches sticky, flex-wrap: nowrap 944 px over
    Page.setFontSizes({standard:32}) 2112 px — never matches relative, flex-wrap: wrap 0

    In a media query rem resolves against the default font size, not the root element's. So raising the default moves the breakpoints along with the type and the desktop nav is never reached; setting the root doubles the type while leaving the breakpoints at 16 px, which is the layout the desktop nav was measured in. Both are real settings, and there is a THIRD mechanism which is the one a reader actually reaches. Chrome exposes "Font size" — the default, 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:

    width pages over overflow
    320 / 360 / 390 / 414 / 640 / 1024 0 of 22 0 — the ruled targets, fixed
    1280 22 of 22 944 px on 21 pages, 508 px on /
    1920 21 of 22 304 px (all but /)

    Root 16 is 0 at every width. The old 944 px figure is unchanged; 602 px became 508 px on / only because .brand-name's nowrap went.

    ⚠️ AND AT 1280 px IT IS NOT A MARGIN — IT IS UNREACHABLE NAVIGATION. The 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 (1556), Contact (1666) and the header CTA are entirely off-screen behind a horizontal scrollbar — including the route to the inquiry form. (This named "Med-Arb" for one revision. Med-Arb is in SECONDARY_NAV and is not in the masthead at any width; the item actually lost, and the worst of them, is Contact.) That is 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 CAUSE AT 1280 px IS flex-wrap: nowrap ON .nav-list ABOVE 66 rem, AND THAT IS A LOCKED STEP-1 DECISION — SO IT IS AN OPEN DECISION FOR POUYA, NOT A FIX MADE HERE. His instruction with the eyebrow ruling was explicit: "if anything header-side does overflow, stop and report rather than improvising a 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 giving up the constant 81 px --header-h. Both change the desktop masthead. Carried on docs/06's cutover checklist so it cannot ship unresolved.

    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 case that attribution was wrong. The cause there was white-space: nowrap on .brand-name, a different declaration in the same component, and removing it changes nothing at any real size: the brand name is one line at all 16 widths at root 16, the sticky header still measures exactly 81 px at every width from 66 rem up, and the CTA still lands exactly on .header-inner's content edge. A residual defended by the wrong cause is defended forever, because the reason given is one nobody wants to re-open — which is exactly what happened for four days.

    The eight narrow-width fixes — no overflow-x was added anywhere. Seven are cause-specific; the eighth is marked as the backstop it is:

    Element Was 320 px Cause
    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
    / .hero-h overflow-wrap: anywhere 36 px "contract," at 104 px held .hero-copy open
    / .feature-body overflow-wrap: anywhere 26 px "party-appointed" at 202 px
    CredentialRow .credential-label overflow-wrap: anywhere 38 px "designation" needs 143 px in an 88 px track
    SiteFooter .footer-brand flex-wrap: wrap 12 px Flex item at min-width: auto cannot shrink below "Lajevardi"
    PracticeCard .card padding clamped (the cause under Pill) The space scale is rem-based, so --space-6 is 64 px a side at root 32 — 128 px of padding in a ~224 px box
    Pill overflow-wrap: anywhere 33 px → 1 px ⚠️ SYMPTOM, NOT CAUSE. A one-word pill cannot wrap at a space that is not there

    flex-wrap: wrap on the footer brand rather than anywhere, because it reflows without hyphenating a person's name.

    ⚠️ Pill's anywhere IS A BACKSTOP AND THE TRADE IS RECORDED RATHER THAN HIDDEN. It removes the min-content floor of a shrink-to-fit inline-flex box, so the pill collapses to whatever its parent gives it. The real cause was PracticeCard's padding: with it clamped, "Construction" at root 32 / 320 px went from 94 × 220 px broken into six two-character lines to 158 × 85 px on two lines, and root 16 is unchanged at 121 × 27 px on one line. anywhere is still load-bearing — removing it leaves 1 px of document overflow on /, /mediation/ and /practice/, and renders the pill 240 px wide inside a 224 px 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.

    Three instrument findings, and each one hid a real defect:

    1. overflow-wrap: break-word permits a break at layout time but does not reduce min-content sizeanywhere does. Five of the fixes are this distinction. global.css:166 sets break-word on p AND h1h6(this line said "h1h6 and nothing else, so body copy inherits no wrapping at all", which was wrong in a way that mattered: .feature-body is a <p> and already had break-word, so the premise given for its fix was false while the fix itself was right). Nothing else gets one, so a <span>, <li>, <dd> or <a> inherits no wrapping.
    2. getBoundingClientRect() reports BORDER BOXES, so an element sweep cannot see text spilling outside its own box. .credential-label's words ran 86 px past an 88 px cell while every element's rect stayed inside the viewport — the document was 38 px over and the offender was invisible to the predicate this table's own command uses. Read scrollWidth - clientWidth per element as well as per document.
    3. Under Emulation.setDeviceMetricsOverride({mobile: true}) the LAYOUT viewport expands to fit the content, so innerWidth becomes 472 at a requested 320 and scrollWidth - innerWidth reads 0 while the reader is still scrolling sideways. The honest formula is documentElement.scrollWidth - documentElement.clientWidth, which is correct in both modes.

    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 /about/). The other 14 measure 0 because their headlines' longest words are shorter. The single-source fix would be to change global.css's h1h6 rule from break-word to anywhere and delete both overrides; that is a design-system change with a site-wide blast radius on intrinsic sizing and it has not been measured, so it is recorded here rather than done.

  • Measure the elements, not only the page. A document-level overflow check passes while a flex child absorbs the deficit by being crushed — that is how step 1 shipped a logo at aspect 0.891 under a green "0 overflow at every width". Assert the rendered geometry of the thing you care about. On / every infinity-mark instance measures 1.56541.5657 against the master's 1.5657.

  • A touch-target measurement of the wrong box is not a finding. The eight cards on / report 2639 px-tall <a> elements and are fine: each card's whole box is the link's hit area via ::after { inset: 0 }, verified by hit-testing nine points per card at three widths (24 cards, 9/9). Hit-test before enlarging anything. WCAG 2.5.8's inline exception also applies to a link sitting mid-sentence, and one such link on / is deliberately left at 164 x 21.

  • Every page must be readable and navigable with JavaScript disabled.