Files
adr-sml/src/pages/index.astro
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

987 lines
43 KiB
Plaintext

---
/**
* `/` — Home. Build step 2 (docs/01 §Build order): "proves the design system
* end to end."
*
* JOB (docs/01 §`/`): "establish the unusual stack in under ten seconds, and
* route each of the four audiences to its surface." Leans in-house counsel.
*
* SECTIONS, against docs/01's outline:
* 1 Hero · 2 Credential row · 3 The approach · 4 Two practices ·
* 5 Practice areas · 6 Process preview · 7 Latest insights · 8 Contact band
*
* SECTION 7 IS BUILT AS OF STEP 7b AND RENDERS NOTHING TODAY. The markup is
* behind `latest.length > 0`, so with no published article no card, no heading
* and no link is emitted. D9 means the flip is Pouya's — the schema refuses
* `draft: false` without `reviewedByPouya: true` — and `SiteHeader` gates the
* Insights NAV item on the same predicate at two pieces. Do not "finish" this by
* hardcoding a placeholder card.
*
* ⚠️ **THE STEP-2 REASONING FOR DEFERRING THIS SECTION WAS THAT AN UNRENDERED
* COMPONENT STILL SHIPS ITS CSS. THAT IS TRUE, AND IT IS NOW MEASURED RATHER
* THAN ARGUED:** importing `ArticleCard` puts **10 rules, 1,496 bytes, 4.4% of
* `dist/index.html`** into this page for a block that renders nothing. Astro
* bundles a component's scoped styles on IMPORT, not on render, and
* `inlineStylesheets: 'auto'` inlines them here.
*
* It is kept anyway, and the reason is also a measurement: `npm run lighthouse -- /`
* returns **performance 99, LCP 2.03 s, CLS 0.000 — identical before and after
* the 1,498-byte growth.** So the cost is real in bytes and absent in the metric,
* on the one page already at `docs/04`'s LCP budget. The dead weight clears
* itself the moment an article publishes, which is the same event that makes the
* section visible.
*
* EVERY FACTUAL CLAIM ON THIS PAGE TRACES TO AGENTS.md §4, and the ones that
* carry risk are constants from src/data/site.ts rather than typed here.
* Notably: arbitration is scoped to COMMERCIAL matters throughout (§4
* Offerings, Q39 2026-08-27), the held designations come from `CREDENTIALS`,
* and nothing claims or implies licensure (D13).
*/
import { Picture, getImage } from 'astro:assets';
import BaseLayout from '../layouts/BaseLayout.astro';
import Button from '../components/Button.astro';
import ContactBand from '../components/ContactBand.astro';
import CredentialRow from '../components/CredentialRow.astro';
import Eyebrow from '../components/Eyebrow.astro';
import InfinityMark from '../components/InfinityMark.astro';
import PracticeCard from '../components/PracticeCard.astro';
import ProcessStep from '../components/ProcessStep.astro';
import ArticleCard from '../components/ArticleCard.astro';
import SectionHeading from '../components/SectionHeading.astro';
import portrait from '../assets/pouya-lajevardi.jpg';
import ogDefault from '../assets/og-portrait.jpg';
import { getCollection } from 'astro:content';
import { homeGraph } from '../data/schema';
import {
ASYMMETRY_LINE,
CREDENTIAL_ROW,
CREDENTIAL_ROW_ARB,
PRACTICE_AREAS,
PORTRAIT,
PROCESS,
PROCESS_FRAMING,
SITE,
} from '../data/site';
/**
* FOUR SLOTS, NOT THREE, and it is now a §4 substitution-principle choice
* rather than a condition.
*
* docs/01 §`/` item 2 says "Three slots"; §4's substitution principle
* authorises `Q.Arb` as a fourth "where one exists", and this layout has one.
* It was MANDATORY here until 2026-08-29 under §4's paired-disclosure
* condition, because the page says *arbitrator* in its second sentence and the
* stage had to appear beside the offering. Q.Arb is held and that condition is
* dissolved; the slot stays because a fourth held credential earns its place,
* not because anything requires it.
*/
const credentials = [...CREDENTIAL_ROW, CREDENTIAL_ROW_ARB];
/**
* docs/04 lists `image` on the Person node. This generates it with EXACTLY the
* transform SEO.astro already applies to the same source — jpeg, 1200 x 630 —
* so Astro's asset cache returns the same hashed file rather than emitting a
* second copy for the crawler's benefit. Verified by asset count: dist holds
* one `og-portrait.*.jpeg` with this line present, not two.
*
* JPEG on purpose, for the same reason SEO.astro uses it: link-preview and
* structured-data consumers are not browsers and several still do not decode
* WebP, let alone AVIF.
*/
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
/* THE THREE MOST RECENT, and the same `!data.draft` predicate the rest of the
site uses — see the `draft` field in `src/content.config.ts`. Sorted here
rather than trusting the loader's order: `glob()` returns files in directory
order, which is alphabetical by filename and has nothing to do with date. */
const latest = (await getCollection('insights', ({ data }) => !data.draft))
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime())
.slice(0, 3);
---
<BaseLayout
title={`${SITE.name} · ${SITE.tagline}`}
description="Mediation and commercial arbitration in Toronto. Construction, technology, energy, insurance and shareholder disputes, read as contracts and as engineering."
imageAlt={PORTRAIT.alt}
jsonLd={graph}
preloadSerifItalic
>
{/* ---- 1. Hero ------------------------------------------------------- */}
<section class="hero">
<div class="wrap hero-inner">
<div class="hero-copy">
{
/* docs/01 specifies this exact string as the hero eyebrow. The
masthead no longer repeats it on this page — see SiteHeader. */
}
<Eyebrow dot>{SITE.tagline}</Eyebrow>
{
/* docs/03 §Approved headline options, option 1 — the recommended one.
Option 3 is struck there: "every side" asserts having acted as
party, as counsel and as neutral, and §4 verifies one of the three.
The italic is the single flourish docs/02 allows in a headline. */
}
<h1 class="display hero-h">
A mediator who reads the contract, the code, and <em class="it"
>the room</em
>.
</h1>
{
/* THREE CONSTRAINTS ON THIS PARAGRAPH. Trimmed to them under D19; the
history is in the `AGENTS.md` Change Log.
1. ARBITRATION STAYS OFFERING-SHAPED. §4 verifies that he ACCEPTS
arbitral appointments and separately verifies completed sole
mediations; there is no completed-arbitration row. A present
indicative beside "mediate" invites a track record §4 does not
hold.
2. NO COMPARATIVE CLAIM ABOUT OTHER NEUTRALS — Q41(b), closed
2026-08-27, and struck from `docs/03`'s positioning statement too.
3. THE TAIL IS POUYA'S WORDING, VERBATIM: *"built for disputes that
turn on the contract, the code, and the engineering documents"*.
Its echo of the `<h1>`'s triad is deliberate. */
}
<p class="hero-lede">
I mediate from Toronto, and I accept commercial arbitration
appointments. I also practise as a machine-learning and infrastructure
engineer, so the matters I take are the ones that turn on the
contract, the code, and the engineering documents: the change order,
the model card, the System Impact Assessment, and the regulatory
overlay around them.
</p>
<div class="hero-cta">
<Button href="/contact/" variant="primary"
>Request a consultation &rarr;</Button
>
<Button href="/process/" variant="ghost">How I work</Button>
</div>
</div>
{
/* `widths` + `sizes` rather than `densities`: the portrait is fluid, and
a density ladder would size it from one assumed CSS width.
⚠️ THE OLD CEILING ARGUMENT WAS WRONG, AND IT WAS WRONG BY IGNORING
THE RANGE WHERE THIS IMAGE IS WIDEST. It read: "The largest real
render is ~476px (content 1280 - 96 gutter - 64 gap, x 0.425), so 960
is the 2x ceiling." That arithmetic describes the TWO-COLUMN layout,
which only engages at 66rem. Below 66rem the hero is a single column
and the portrait is the full content width. Measured, both pages:
viewport slot DPR-2 needs picked result
640 592 1184 760/960 1.23-1.56x upscale
768 672 1344 960 1.40x
900 804 1608 960 1.68x
1024 928 1856 960 1.93x
At 768/DPR-1 a 760w file EXISTS and is not chosen, so part of the loss
was purely the wrong `sizes` (60vw declared against a ~88vw slot).
Reconfirmed with the HTTP cache cleared — an earlier probe reported a
2.81x OVERSIZED fetch at 390/DPR-1 which was a cache artefact, not a
defect.
TWO FIXES, BOTH REQUIRED. (1) `.hero-portrait` is capped at 30rem in
the single-column range, so the widest real slot is 480 CSS px
everywhere — which makes 960 exactly right for DPR 2 rather than
accidentally short. (2) A 1440 rung, because 480 x 3 = 1440 and the
desktop slot at DPR 3 already needed 1287-1428; the ≥66rem range was
upscaling ~1.34-1.49x at DPR 3 before this and nobody had measured it.
The master is 1600, so 1440 exists.
THE CAP CHANGES HOW THIS PAGE LOOKS between 640px and 1055px — the
portrait was 592-928px wide there and is now 480. That is a visible
design change to a reviewed page, made on payload grounds; raising the
cap is a one-line change but the ladder has to grow with it.
`width` AND `height` ARE PASSED ALONGSIDE `widths`, AND THAT IS NOT
REDUNDANT. With `widths` alone, Astro emits the UNTOUCHED 1600px
master as the <img src> fallback — measured 254,626 bytes sitting in
dist as the declared fallback for a 476px slot. Passing width/height
pins it to the 960 variant instead (78,665 B), and the 1600px file
stops being generated at all. Same defect InfinityMark's comment
records for a missing `width`, in the one shape that survives passing
`widths`. Verified by reading the emitted `src` attribute and that
file's real dimensions — not the build log, which reported "before:
349kB" for every variant either way.
What a browser actually takes is the AVIF: 5.6 / 7.3 / 11.1 / 14.8 /
21.5 kB across the first five widths [measured 2026-08-27]; 1080 and
1440 are added 2026-08-28 for DPR 3 (26.6 / 48.8 kB).
AND THE 1440 RUNG OVERSHOT, SO 1080 EXISTS TO CORRECT IT. Adding 1440
for DPR 3 removed a 1.07x upscale at 390/DPR3 and replaced it with a
**48,799 B fetch where the old one was 21,526 B** — +27 KB on a phone,
to fix a 7% softness nobody can see. A browser takes the smallest
candidate at or above what it needs, and with no rung between 960 and
1440 the only choices were "slightly soft" or "+27 KB". 1080 makes
1026 (390 x DPR 3) exact and cheap. This was a defect in the fix for
the defect above, found by measuring the fix rather than the source.
AND 1080 ALONE MISSED THE TWO LARGEST CURRENT PHONES, WHICH
MAKES THIS THE THIRD ITERATION OF THIS LADDER. 1080 was tuned to
390 CSS px x DPR 3 (= 1026), and `sizes` resolves to
`calc(100vw - 3rem)` up to 528px, so every phone wider than 390
overshoots to the next rung: iPhone 14 Plus (428@3, needs 1140)
and 15/16 Pro Max (430@3, needs 1146) both took **1440 —
48,799 B**, against 27,594 for the device the rung was tuned
for. +21,205 B, 13% of page weight. A 1200 rung closes it at
1.05x. Measured after: no rung more than 1.06x oversized on the
phone axis, and still no upscaling anywhere — 0 upscaling across 11
real device profiles on both pages.
TWO RESIDUAL OVER-FETCHES, LEFT DELIBERATELY, so neither reads as an
oversight later. **320@2** needs 544 and takes 640 (1.18x): there is no
rung between 480 and 640, and 480 would be a 1.13x UPSCALE, so the
oversize is the better half of that trade. **1056@2 needs 760.4 and
takes 960 (1.26x, +6.8 KB)** — a knife-edge, and worth stating because
it looks like a `sizes` error and is not: the declared `36vw` is
accurate to the measured 36.0% track, and 380.2 x 2 = 760.4 misses the
760 rung by four tenths of a pixel. Declaring 35vw to duck under it
would make `sizes` less truthful across the whole band in exchange for
a 0.05% upscale at this width. The declaration stays honest and one
viewport over-fetches.
⚠️ `fetchpriority="high"` IS GONE, AND THIS PAGE HAD IT WHILE
`/about/` WITHHELD IT ON THE IDENTICAL MEASUREMENT. The old comment
read "eager + fetchpriority=high because this is the LCP candidate on
the page docs/04 budgets hardest" — true only from 768px up. Measured,
cache cleared per device:
320x568 @2 portrait visible 0px LCP — 11,058 B
360x780 @3 portrait visible 0px LCP — 21,526 B
390x844 @3 portrait visible 0px LCP P.hero-lede 27,594 B
430x932 @3 portrait visible 30px LCP P.hero-lede 48,799 B
1280x900 @1 portrait visible 595px LCP IMG.portrait 7,257 B
So on mobile — the axis the ≥95 budget is actually measured on — it
promoted 27-49 KB of image the reader cannot see above the Geist face
that paints the real LCP element. `/about/` already withheld it for
exactly this reason and this page did the opposite; the inconsistency
is the finding.
`loading="eager"` STAYS: the portrait is the LCP element from 768px up,
and eager costs nothing where it is off-screen. If the desktop LCP ever
needs protecting explicitly, the right instrument is
`<link rel="preload" imagesrcset imagesizes>` in <head>, which honours
`sizes` and therefore self-cancels on phones — not a blanket attribute
that cannot. */
}
<div class="hero-portrait">
<Picture
src={portrait}
width={960}
height={960}
widths={[380, 480, 640, 760, 960, 1080, 1200, 1440]}
sizes="(min-width: 80rem) 476px, (min-width: 66rem) 36vw, (min-width: 33rem) 480px, calc(100vw - 3rem)"
formats={['avif', 'webp']}
fallbackFormat="jpeg"
alt={PORTRAIT.alt}
loading="eager"
decoding="sync"
class="portrait-img"
/>
</div>
</div>
</section>
{/* ---- 2. Credential row --------------------------------------------- */}
<section class="wrap credential-band" aria-label="Credentials">
<CredentialRow slots={credentials} />
</section>
{
/* NO `aria-labelledby` ON THE CONTENT SECTIONS, and that is the fix for an
inconsistency rather than a removal of information.
Four of them carried one and this one did not, which put five named
regions plus header / nav / main / footer / footer-nav in a screen
reader's region list — eleven entries for a marketing page — with the only
unnamed content section being the odd one out. A `<section>` without an
accessible name is not exposed as a region at all, and the visible `<h2>`s
already give heading navigation, which is how a reader moves through a
page like this.
THE ONE EXCEPTION IS THE CREDENTIAL BAND, which has no visible heading, so
`aria-label` is the only thing that can name it. The rule is: name a region
only where it has no heading of its own. */
}
{/* ---- 3. The approach ----------------------------------------------- */}
<section class="section section-inverse approach reveal">
<div class="wrap approach-inner">
<div class="approach-copy">
<SectionHeading eyebrow="The approach" level={2}>
<span slot="heading">Two directions at once.</span>
</SectionHeading>
{
/* THIS PARAGRAPH USED TO OPEN "Law and engineering are not blended
here", and `claims-auditor` flagged it as Q37's struck parallel
relocated from the credential label into prose — a degree and a
practice under one noun, one day after Pouya struck exactly that
construction. It is a fair reading and the fix is the same fix:
make the two halves asymmetric. What a neutral does with a contract
is read it; what an engineer does is engineering. Neither sentence
now sets "Law" beside "engineering" as two instances of one thing.
`docs/01` §`/` item 3 and `docs/03` §Home both specify this section
as "the 'two directions at once' argument — law and engineering
converging on the same dispute", so the ARGUMENT is unchanged and
still delivered; only the construction that carried the implication
is gone.
**Q41(a) CLOSED 2026-08-27: Q37's reasoning DOES extend to prose,
and prose has to do more than avoid the parallel.** Pouya: *"The
implication test applies everywhere, not just to labels. Prose has
more room, so it is easier to satisfy: state the asymmetry
explicitly rather than relying on a parallel construction to carry
it."* Avoiding the pair was therefore only half the fix — a reader
can still supply the missing symmetry from silence. So the second
paragraph now names both halves for what they are: training on one
side, current work on the other. "Training I hold" is the opposite
of a licence claim, which is the point of saying it out loud.
`docs/01`'s and `docs/03`'s own phrase "law and engineering" is the
struck construction; both now carry a note not to lift it into copy.
The argument it names is Pouya's and stands. */
}
<div class="prose approach-prose">
<p>
Any dispute I take gets read twice: once against the documents, and
once as engineering. The two readings are not blended here. They run
at the same time.
</p>
{
/* FROM A CONSTANT, NOT TYPED. It was typed here and then typed
again on `/about/`, and the two copies had ALREADY diverged inside
one session — a comma here, full stops there. Q41(a) makes this
the sentence responsible for foreclosing the licence implication,
so it is the worst string on the site to let drift. See
ASYMMETRY_LINE in src/data/site.ts. */
}
<p>{ASYMMETRY_LINE}</p>
{
/* TWO COMPARATIVES CAME OUT OF THESE PARAGRAPHS ON 2026-08-28, and
both had SURVIVED the sweep that closed Q41(b) the day before:
"The second half of each pair usually arrives as a separate
expert report." — an empirical claim about how disputes are
usually run, i.e. about a population of other matters.
"it is why the technical half is not something a party has to
commission and wait for" — the same claim in counterfactual
form, which is harder to spot and says more.
Pouya's ruling on Q41(b) is the test: *"assert his capability, not
the field's incapability."* Both worked by asserting the field's.
What replaces them says only what he does, which is the stronger
claim anyway — and it is shorter. */
}
<p>
A construction claim is a contract question and a scheduling
question. A software dispute is a licence question and an
architecture question. A grid connection is a regulatory question
and a load question.
</p>
<p>
I read both halves of each pair myself. That is the whole of it.
</p>
<p class="approach-metaphor">
My mark is an infinity loop, and it is the argument in one line:
disputes are loops. The work is redrawing the loop into a line.
</p>
</div>
</div>
{
/* Decorative: the paragraph beside it says what it is, so an
accessible name here would be read twice. Measured on this ground —
the ribbon's champagne half carries the silhouette against ink, the
maroon half against cream; it reads on both. */
}
<div class="approach-mark">
{
/* width=232 and loading=lazy are both measured, not defaults.
`size` caps at 9rem tall, so the mark renders at up to 144 x 225.5
CSS px — five times the header's 50.1px, and the component's default
64px ladder tops out at 192px. Measured upscale before this: 1.17x
at DPR 1, 2.35x at DPR 2, 3.52x at DPR 3. 232 with densities
[1,2,3] gives 232/464/696, and 696 covers the 676 device px a DPR-3
screen asks for. Lazy because this sits roughly a screen and a half
down; the component defaults to eager for the masthead. */
}
<InfinityMark
size="clamp(4rem, 14vw, 9rem)"
width={232}
loading="lazy"
/>
</div>
</div>
</section>
{/* ---- 4. Two practices --------------------------------------------- */}
<section class="section practices reveal">
<div class="wrap">
{
/* The id goes on a span INSIDE the h2 via the `heading` slot, so
`aria-labelledby` names the visible heading. The first version passed
`title=` and additionally rendered a `.visually-hidden` span carrying
the id, which put the same words in the accessibility tree twice —
found in the heading-outline dump, not by reading the source. */
}
{
/* THE WRAPPER IS THE FIX, NOT DECORATION. This was
`<SectionHeading class="section-head">`, and the page's rule compiled
to `.section-head[data-astro-cid-<page>]` while the rendered root
carried SectionHeading's own cid — so it never matched. Measured:
`margin-block-end: 0px` and a 0px gap to the cards on all three call
sites, with `.display`'s 0.98 line-height putting the glyphs over the
card's top edge. `astro check` 0 errors, `eslint` clean, and the source
looked right. Fourth instance of this on the project; SectionHeading no
longer accepts a `class` at all, and passing one is now a build
error. */
}
<div class="section-head">
<SectionHeading eyebrow="What I do" level={2}>
<span slot="heading">Two processes.</span>
</SectionHeading>
</div>
<div class="pair">
<article class="feature">
<h3 class="feature-title">
<a href="/mediation/">Mediation</a>
</h3>
{
/* NO FEE CLAIM ON THIS CARD — a constraint, not an omission.
There are two ways to get it wrong in one line and both have
shipped once: "at one published rate" reads as ONE price where
D14's card sets two, and an unqualified "preparation included"
sells an uncapped allowance where `docs/07` caps it at stated
hours. A card this size cannot state either properly.
`/mediation/` and `/fees/` carry the card. */
}
<p class="feature-body">
Sole mediator, Q.Med through ADRIC and ADRIO, with multiple
completed sole mediations. Half day or full day, in person or by
video.
</p>
<span class="feature-arrow" aria-hidden="true">&rarr;</span>
</article>
<article class="feature">
<h3 class="feature-title">
<a href="/arbitration/">Arbitration</a>
</h3>
{
/* COMMERCIAL MATTERS ONLY (Q39) — family arbitration is not offered,
and that scope is Pouya's own choice, not a conclusion this record
draws about the law. */
}
<p class="feature-body">
I accept sole, party-appointed and co-arbitration appointments in
commercial matters, on a documents-only, expedited or full-hearing
track.
</p>
<span class="feature-arrow" aria-hidden="true">&rarr;</span>
</article>
</div>
{
/* Med-Arb has its own §4 Offerings row (Q35, 2026-08-27), so it is named
here as a present offering. docs/01 item 4 asked for it as "the
long-term arc"; there is no arc, and that item is amended. */
}
<p class="pair-note">
<strong>Med-Arb</strong> combines the two: one neutral mediates, then arbitrates
whatever has not settled. I accept those appointments in commercial matters.
The page on it meets the procedural-fairness objection head on rather than
around it &mdash;
<a href="/med-arb/">how med-arb works &rarr;</a>
</p>
</div>
</section>
{/* ---- 5. Practice areas -------------------------------------------- */}
{
/* docs/01: "the most important block on the page for search, because it
distributes authority to the pages that can actually rank." */
}
<section class="section section-alt areas reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="Practice areas"
level={2}
lede="Six areas, chosen because the disputes in them turn on documents I can read without an intermediary."
>
<span slot="heading">Where the work is.</span>
</SectionHeading>
</div>
{
/* reveal-stagger is capped at six children by design (docs/02) and there
are exactly six. A seventh would land with the sixth, not break. */
}
<div class="area-grid reveal-stagger">
{
PRACTICE_AREAS.map((area) => (
<PracticeCard
href={`/practice/${area.slug}/`}
chip={area.chip}
title={area.name}
level={3}
>
{area.blurb}
</PracticeCard>
))
}
</div>
{
/* THIS SAID "All six areas, and what else is offered", which asserted
offerings beyond the six, none of which had a §4 row.
**Q42 CLOSED 2026-08-27, and one of the four candidates came out.**
Early neutral evaluation, dispute-system design and pre-dispute
technical advisory now have Offerings rows. **Settlement counsel does
not, and never will** — Pouya struck it as his own error in `docs/01`:
*"Settlement counsel acts FOR a party in negotiation. That is a
partisan role, and putting it on a site that (a) sells neutrality and
(b) asserts no licensure under D13 is wrong twice over."*
THIS LINE STILL READS "All six practice areas" and that is unchanged
on purpose. The three rowed processes are `/practice/`'s "also
offered" strip at step 5, not a claim `/` makes in a link label — a
six-card grid followed by "and what else is offered" is the
Med-Arb-in-the-footer shape whether or not the rows exist. */
}
<p class="areas-more">
<a href="/practice/">All six practice areas &rarr;</a>
</p>
</div>
</section>
{/* ---- 6. Process preview ------------------------------------------- */}
<section class="section process reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="How it runs"
level={2}
lede="Five steps, from the first call to the conclusion — including what happens if the matter does not settle."
>
<span slot="heading">From first call to conclusion.</span>
</SectionHeading>
</div>
<ol class="process-strip" role="list">
{
PROCESS.map((step, i) => (
<ProcessStep n={i + 1} title={step.title} timing={step.timing}>
{step.body}
</ProcessStep>
))
}
</ol>
{
/* NOT OPTIONAL, AND NOT A DISCLAIMER — Q43, Pouya 2026-08-27. It is the
condition on which the five timings may be published at all: *"Published
as typical, they are honest and useful; published as commitments, the
first matter that slips makes the page false."* It sits directly under
the numbers rather than in the section lede above them, because a
reader who scans the strip and skips the lede has read a commitment.
`/process/` renders the same constant at step 6. */
}
<p class="process-framing">{PROCESS_FRAMING}</p>
<p class="process-more">
<a href="/process/">What happens if the matter does not settle &rarr;</a
>
</p>
</div>
</section>
{/* ---- 7. Latest insights -------------------------------------------- */}
{
latest.length > 0 && (
<section class="section section-alt reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="Insights"
level={2}
lede="Notes on process, regulatory change, and the technical record underneath commercial disputes."
>
<span slot="heading">Recently written.</span>
</SectionHeading>
</div>
<div class="grid-autofit insights-grid" style="--grid-min: 20rem">
{latest.map((entry) => (
<ArticleCard
href={`/insights/${entry.id}/`}
title={entry.data.title}
description={entry.data.description}
date={entry.data.publishDate}
topics={entry.data.topics}
readingTime={entry.data.readingTime}
level={3}
/>
))}
</div>
<p class="insights-more">
<a href="/insights/">Everything written &rarr;</a>
</p>
</div>
</section>
)
}
{/* ---- 8. Contact band ---------------------------------------------- */}
{
/* A COMPONENT SINCE 2026-08-28. It was ~20 lines of markup plus ~20 of CSS
here and the same again on `/about/`, and the two had already drifted
(`.contact-body` at 52ch here, 46ch there) inside the session that wrote
the second one. Seventeen pages remain. docs/01 item 8's missing booking
link is documented in the component, once. */
}
<ContactBand />
</BaseLayout>
<style>
/* --- 7. Latest insights --------------------------------------------- */
/* `.grid-autofit` (global.css) carries the columns and the `min()` guard. */
.insights-grid {
gap: var(--space-5);
}
.insights-more {
margin-block-start: var(--space-6);
font-size: var(--text-base);
}
/* --- 1. Hero -------------------------------------------------------- */
.hero {
padding-block: var(--space-8) var(--space-9);
}
.hero-inner {
display: grid;
gap: var(--space-7);
align-items: center;
}
.hero-copy {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.hero-h {
/* --text-6xl is the display ceiling. At 360px it is 52px and the headline
runs four lines; `text-wrap: balance` (global.css) keeps them even. */
font-size: var(--text-6xl);
max-inline-size: 22ch;
/* `anywhere`, not `break-word` — one word here held the whole hero column
open. `docs/02` §Reflow, instrument finding 1. */
overflow-wrap: anywhere;
}
.hero-lede {
max-inline-size: var(--width-prose);
font-size: var(--text-lg);
line-height: var(--leading-relaxed);
color: var(--text-secondary);
}
.hero-cta {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin-block-start: var(--space-2);
}
/* The 4:5 crop is CSS, not a build step: Astro's image service does not crop,
and cropping offline would mean committing a second derived binary on top
of the ~3.16 MB of brand and portrait masters already in the repo. The cost
is that a browser downloads the full square and shows 80% of it; the source
is 1600 x 1600 and the largest variant generated is 960 wide. */
.hero-portrait {
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: var(--radius-lg);
background: var(--bg-raised);
/* THE CAP IS THE FIX FOR THE UPSCALE, not a style preference. Uncapped, the
single-column slot ran to 928 CSS px at a 1024px viewport, which is 1856
device px at DPR 2 against a 960w ceiling — 1.93x. 30rem makes 480 the
widest real slot on any page, so the ladder's 960 covers DPR 2 exactly
and the new 1440 rung covers DPR 3. See the <Picture> comment above.
`margin-inline: auto` because a 480px box in a 928px column would
otherwise sit hard against the inline start. */
max-inline-size: 30rem;
margin-inline: auto;
}
/* The <picture> wrapper is the box that gets sized, NOT the <img> — the exact
defect CLAUDE.md records for <Button> and then for <Picture> inside
InfinityMark. `class="portrait-img"` lands on the <img>, so the <img> rule
below is reached via Astro's cid on the emitted element, and the wrapper is
targeted by the bare `picture` selector, which the markup does carry. */
.hero-portrait :global(picture) {
display: block;
block-size: 100%;
}
.portrait-img {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
/* Above centre: the subject's head is in the upper half of a square crop. */
object-position: 50% 22%;
}
@media (min-width: 66rem) {
.hero-portrait {
/* Above 66rem the grid track is already 390-476px, so the cap is inert —
released anyway so the track, not this number, governs the two-column
layout. Keeping it would silently become the constraint if the track
ever widened. */
max-inline-size: none;
}
.hero {
padding-block: var(--space-9);
}
.hero-inner {
/* 1.15 / 0.85 — the copy column carries a 22ch headline and a 68ch lede;
an even split starves the headline and leaves the portrait oversized. */
grid-template-columns: 1.15fr 0.85fr;
gap: var(--space-8);
}
}
/* --- 2. Credential row ---------------------------------------------- */
.credential-band {
/* No .section wrapper: the row owns its own padding-block and border, and
stacking --section-y on top would put 160px of air around a 4-line band. */
padding-block-end: var(--space-4);
}
/* --- 3. The approach ------------------------------------------------ */
.approach-inner {
display: grid;
gap: var(--space-7);
align-items: center;
}
/* NO `display: flex; gap` ANY MORE, AND THAT IS A CORRECTION TO A FIX.
global.css gained `:where(.prose) > p + p { margin-block-start }` on
2026-08-28 because a bare `.prose` block had no paragraph spacing at all.
The new rule's comment asserted "with `:where()` the flex container's gap
governs and this contributes nothing" — **false, and it was measured false
immediately after being written.** `:where()` controls SPECIFICITY, not
whether a declaration applies: nothing here was overriding the margin, so
flex `gap` and the margin both applied and this block's paragraph gaps went
**24px -> 48px**. Spacing now comes from the one global rule, which is the
point of having it. */
.approach-prose {
margin-block-start: var(--space-6);
font-size: var(--text-lg);
line-height: var(--leading-relaxed);
}
.approach-metaphor {
padding-block-start: var(--space-5);
border-block-start: 1px solid var(--rule);
font-family: var(--font-serif);
font-size: var(--text-xl);
line-height: var(--leading-snug);
color: var(--text-inverse-2);
}
.approach-mark {
display: flex;
justify-content: center;
}
@media (min-width: 66rem) {
.approach-inner {
grid-template-columns: 1fr auto;
gap: var(--space-9);
}
/* Mark second in the DOM and second visually. No `order` anywhere on this
page: reordering flex or grid items puts focus order out of step with
visual order, which is what WCAG 2.4.3 and docs/02 both forbid, and it
already cost a header rebuild at step 1. */
}
/* --- 4. Two practices ---------------------------------------------- */
/* NO `.section-head` RULE — it moved to `global.css`, where `.prose` lives.
It was byte-identical in both pages, and every one of the seventeen
remaining pages needs it for the same reason (a parent cannot style a child
component's root, so the wrapper must be page-owned). */
.pair {
display: grid;
gap: var(--space-5);
}
.feature {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
/* CLAMPED, not a flat --space-7. At a 200% default font size (root 32px)
`3rem` is 96px a side — 192px of horizontal padding inside a 342px
container, which was most of the 83px residual overflow
`adversarial-reviewer` measured. The vw term keeps it at 48px on any real
viewport and lets it collapse when the rem is doubled. */
padding: clamp(var(--space-5), 4vw, var(--space-7));
background: var(--bg-raised);
border-radius: var(--radius-lg);
transition: background-color var(--dur-hover) var(--ease);
}
.feature:hover {
background: var(--cream-2);
}
.feature-title {
font-family: var(--font-serif);
font-size: var(--text-3xl);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
/* `overflow-wrap: break-word` (global.css) permits a break at layout time
but does NOT reduce min-content size, so "Arbitration" at a 60px
--text-3xl held the card open. `anywhere` does reduce it. Only reachable
at a large default font size; at every real size the word never breaks. */
overflow-wrap: anywhere;
}
.feature-title a {
color: var(--text);
text-decoration: none;
}
/* One link per card, hit area the whole card, accessible name the heading —
same pattern and same reasoning as PracticeCard. */
.feature-title a::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-lg);
}
.feature:hover .feature-title a {
color: var(--accent);
}
.feature:has(a:focus-visible) {
outline: 2px solid var(--focus-ring);
outline-offset: var(--focus-offset);
}
.feature-title a:focus-visible {
outline: none;
}
.feature-body {
flex: 1 1 auto;
max-inline-size: 46ch;
color: var(--text-secondary);
/* `anywhere`, not the `break-word` `global.css` already gives this `<p>` —
one token held this card's grid track open. `docs/02` §Reflow, instrument
finding 1. */
overflow-wrap: anywhere;
}
.feature-arrow {
font-size: var(--text-xl);
line-height: 1;
color: var(--accent);
}
.pair-note {
max-inline-size: var(--width-prose);
margin-block-start: var(--space-6);
color: var(--text-secondary);
}
@media (min-width: 56rem) {
.pair {
grid-template-columns: 1fr 1fr;
gap: var(--space-6);
}
}
/* --- 5. Practice areas --------------------------------------------- */
.area-grid {
display: grid;
/* auto-fit with an 18rem floor: 1 up on a phone, 2 up on a tablet, 3 up on
a desktop, with no breakpoint of its own. The cards size themselves to
the cell (see PracticeCard) — a parent cannot reach a child's root.
`min(18rem, 100%)`: a bare rem floor is a hard minimum, so at a large
default font size (root 32px) 18rem becomes 576px and the track will not
shrink. Measured at root 200%: this grid overflowed a 390px viewport by
234px. See CredentialRow for the same guard and the full reasoning. */
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
gap: var(--space-5);
}
/* THESE TWO LINKS STAND ALONE ON THEIR OWN LINE, so docs/02's 44px touch
floor applies to them in full. Measured before this rule: 250.8 x 18 and
287.9 x 18 — the paragraph's line box and nothing more.
`inline-flex` + min-block-size rather than padding, so the 44px IS the hit
area rather than visual air, and `inline-size: fit-content` keeps the
target the width of the words instead of the whole measure.
The INLINE link in `.pair-note` ("how med-arb works") is deliberately NOT
given this treatment: it sits mid-sentence at 164 x 21, and WCAG 2.5.8
exempts a target "in a sentence or its size is otherwise constrained by the
line-height of non-target text". Padding it out would break the paragraph's
leading to satisfy a rule that does not apply to it. A deviation from
docs/02's flat wording, taken deliberately and recorded rather than left to
look like an oversight.
The card heading links measure 26-39px tall and are NOT a finding: each
card's whole box is the link's hit area via `::after { inset: 0 }`. Verified
by hit-testing nine points per card at 390 / 768 / 1280px — 24 cards, 9/9
inside the link every time. The width sweep flagged them because it
measured the <a>'s own box, which is not the target. */
.areas-more,
.process-more {
margin-block-start: var(--space-6);
font-size: var(--text-sm);
}
.areas-more a,
.process-more a {
display: inline-flex;
align-items: center;
inline-size: fit-content;
min-block-size: 44px;
}
/* --- 6. Process preview -------------------------------------------- */
.process-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
gap: var(--space-5);
margin: 0;
}
/* --space-5 rather than --space-6: this belongs to the strip above it, not to
the link below.
THE RATIO IN THIS COMMENT WAS WRONG WHEN FIRST WRITTEN. It said
"--text-meta on cream measures 3.07:1". It does not: `#6e6359` on `#faf7f2`
measures **5.47:1** and passes AA. 3.07:1 is `--muted` on **ink**, which is
exactly what `tokens.css` says and what this comment misread. So the
*reason* given was false even though the *choice* is right —
--text-secondary (10.76-11.75:1) is correct here because this sentence is a
CONDITION on the numbers above it, not metadata about them, and a condition
has to read like body copy. Corrected 2026-08-28 on
`adversarial-reviewer`'s measurement. */
.process-framing {
margin-block-start: var(--space-5);
max-inline-size: var(--width-prose);
font-size: var(--text-sm);
color: var(--text-secondary);
}
/* NO CONTACT-BAND RULES HERE. `.contact-inner`, `.contact-h`, `.contact-body`
and `.contact-action` moved to `ContactBand.astro` with the markup they
style. They were left behind after the extraction — dead CSS shipping to
every visitor, and worse, the **46ch / 52ch divergence the extraction
existed to end was still sitting on disk in both pages**, so the next reader
would have found two different values and no rendered difference. Deleted
2026-08-28. */
</style>