Two of Pouya's rulings of 2026-08-31, committed as one tree at his instruction
("as-is") because AGENTS.md Change Log entry (ai) covers both and splitting it
would mean rewriting the record rather than moving it. This is a deliberate
departure from one-logical-change-per-commit, recorded here rather than left to
be inferred.
THE EYEBROW. `--text-eyebrow: 0.8125rem` added to tokens.css; `.eyebrow` in
global.css retargeted to it. One edit site, which is what the design system
claimed. 13px is not a rung on the modular ladder — it sits between xs and sm
deliberately, because uppercase mono at 0.18em tracking reads smaller than it
measures. `--text-2xs`'s comment stopped calling itself the eyebrow floor.
The rendered sweep (22 pages × 2 widths, 838 mono elements measured over CDP,
not grepped) found exactly one escape and one deliberate override:
- h2.footer-heading, 176 instances — an ESCAPED EYEBROW. Five declarations
byte-identical to `.eyebrow`, differing only in colour. Consolidated to
`class="eyebrow footer-heading"`; the scoped rule is now colour + margin.
The colour is load-bearing, not decorative: `.eyebrow`'s own `--text-meta`
on ink is 3.07:1 and fails.
- span.eyebrow.brand-tagline, 21 instances — HELD at `--text-2xs`. Measured:
at 13px the header grows 81 → 83.4px while `--header-h` is pinned at 81 and
drives `scroll-padding-top`; and with a seventh nav item the CTA lands past
`.header-inner`'s content edge by 42px at 1216, 18px at 1240, 26px at 1280
and 1440. Document overflow is 0 in all of those, so no page-level check
can see it. Insights is that seventh item.
Everything else mono-uppercase is a genuinely different component and was left:
the 0.06em `--tracking-wide` family, the 14px mixed-case designation strip, the
/bio/ print sheet, /contact/'s form labels.
Measured after: contrast unchanged on all 817 instances (11.09 / 8.11 / 5.47 /
5.01:1, all pass at 13px, which is still normal text and needs 4.5:1). Zero
document overflow and identical header geometry at 15 widths. Two eyebrows gain
a line below 414px — /'s hero, already wrapping at 320px before this, and
/insights/'s empty state at 320px only. Accepted, not re-tuned.
docs/02's type spec moved 11–12px → 13px and now enumerates the three 11px
carve-outs instead of implying there are none. The /type-scale/ proof sheet
(d) asked for no longer exists — deleted at build step 2 — so the spec prose is
the proof sheet now.
THE TTL. backend/intake/handler.mjs CONFIRMED to match `AttributeName=ttl`: it
writes `ttl` as a Number, in epoch seconds, at RETENTION_MONTHS = 24. Nothing
needed changing for the enable command.
Removed `|| 0` from the TTL computation. DynamoDB does not expire an item whose
TTL is more than five years past, so `ttl: 0` meant RETAINED FOREVER while
/legal/privacy/ promises deletion — a fallback whose failure mode was the exact
inverse of the claim it was protecting. Unreachable in practice, which is why
it would never have been noticed. A bad value now fails the write.
§7 records TTL as DISABLED at first verification, so the privacy policy's
automatic-deletion promise was unbacked from the moment it was written. §7 is
the only place that status lives; docs/05 and docs/06 carry the constraint and
cite §7, because round 2 of review caught this change set reproducing the SES
DKIM defect — five copies of a status that is about to be re-stamped.
Added, and these are the gate: `TODO(pouya)` on /legal/privacy/'s retention
section, §9 Q60, §12 R19. The page does not publish a period, it asserts a
MECHANISM — deleted by the database rather than by someone remembering — and
nothing in the toolchain can see that. check:claims is frozen with no pattern
for it and deploy does not read docs/06. The copy was NOT softened: it is about
to be true, and weakening a privacy commitment to make it defensible is the
move the rules forbid. What was missing was the gate, not the caveat.
Reviewed by adversarial-reviewer, two rounds (D19 cap), 15 findings, all
accepted, none declined. claims-auditor did not run — D20. Round 1's findings
were almost entirely in prose written that session, and round 2's blocking
finding plus its sharpest should-fix were both defects in round 1's own fixes.
public/pouya-lajevardi-bio.pdf is deliberately NOT in this commit. It was
regenerated and reverted: /bio/'s eyebrow sits inside `.no-print`, so the sheet
has no eyebrow at all, and `cmp -l` showed exactly 10 differing bytes, all in
/CreationDate and /ModDate.
Gates, every one read as an exit status and none through a pipe: check 0,
build 0 (22 pages), check:claims 0, og:proof 0, check:intake 0, lint 0,
minifier tripwire clean, TODO in dist 0. Lighthouse run twice with identical
output: perf 99 on / and 100 on the other 21, a11y 100, best practices 100,
SEO 100 on every indexable page, CLS 0.000, LCP 1.50–2.03s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
338 lines
20 KiB
Markdown
338 lines
20 KiB
Markdown
# 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 13 px — see the eyebrow rule below for why it is not a rung.
|
||
|
||
**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.95`–`1.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, **13 px** (`--text-eyebrow`), `0.18em` tracking, uppercase,
|
||
always paired with a real heading. **13 px is not a rung on the scale** — it
|
||
sits between `--text-xs` and `--text-sm` because uppercase mono at this
|
||
tracking reads smaller than it measures, and at 12 px it was being lost
|
||
against the display type (Pouya, 2026-08-31; it read 11–12 px until then).
|
||
There is **one definition**, `.eyebrow` in `global.css`; the footer's column
|
||
headings carry that class rather than repeating it. An eyebrow is not a
|
||
heading and never carries the `<h*>` — a real heading may carry the class.
|
||
**"One definition" means one definition of the 13 px eyebrow.** The four 11 px
|
||
blocks in the next bullet repeat the mono/`0.18em`/uppercase treatment
|
||
deliberately and are not covered by it.
|
||
- `--text-2xs` (11 px) is the compressed-label floor, not the eyebrow floor.
|
||
**Three carve-outs, and they are deliberate rather than incidental** — do not
|
||
read this line as licence for an 11 px label anywhere else. (a) The header
|
||
tagline, and **it is load-bearing today, not at some future step:** 13 px takes
|
||
the header to 83.4 px while `--header-h` is 81 px, which `global.css` feeds
|
||
into `scroll-padding-top`. A seventh nav item is the second reason and that one
|
||
does arrive with Insights. Both measurements are in `SiteHeader.astro`. (b) The `/bio/` print sheet, which
|
||
`npm run bio:pdf` holds to one page. (c) On `/contact/`, both the
|
||
`<label>`/`<legend>` set and the direct-contact `<dl>` terms.
|
||
⚠️ **(c) is the weak one and is flagged as such.** 11 px uppercase mono
|
||
measures 5.47 : 1 and breaches no floor in this document or in WCAG, but it
|
||
now sits two steps below the eyebrow on the page that collects inquiries, for
|
||
no reason beyond it having been written that way. Raised with Pouya
|
||
2026-08-31; it is an accepted size, not a ratified one.
|
||
- 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`.
|
||
|
||
```css
|
||
@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 320–375 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 counts** — `AGENTS.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:
|
||
|
||
| 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 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. *(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, and its 320/360 residual is the same header decision.
|
||
|
||
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.
|
||
|
||
Two things worth keeping. **`overflow-wrap: break-word` permits a break at
|
||
layout time but does not reduce min-content size** — `anywhere` does, and that
|
||
distinction was the whole of one of those fixes. And 1280 px stays over, from the header's
|
||
deliberate `flex-wrap: nowrap` above 66 rem plus `white-space: nowrap` on the
|
||
brand name — **602 px on `/` and 944 px on every other page**, because
|
||
`SiteHeader` gates the tagline on `!isHome`, so the masthead is wider
|
||
everywhere except home. *(This read a flat "602 px" until 2026-08-28. That
|
||
figure was measured on `/` and generalised; `/about/` was already 944 when it
|
||
was added to this table, and step 4's three pages are 944 too. Found by
|
||
`adversarial-reviewer`.)* The 320 px residual is the same header
|
||
plus the display headline's 104 px floor. Undoing either re-opens the measured
|
||
step-1 header decision, so they stand. All of this is beyond what this floor
|
||
requires — page zoom is clean — so it is a robustness margin rather than a
|
||
failure. Revisit if a real reader hits it.
|
||
|
||
- **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.5654–1.5657 against the master's
|
||
1.5657.
|
||
- **A touch-target measurement of the wrong box is not a finding.** The eight
|
||
cards on `/` report 26–39 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.
|