Files
adr-sml/src/pages/index.astro
T
Pouya LajevardiandClaude Opus 5 fd5f610982 feat: D19 bounds the review loop; apply nine rulings; close Q46(a) and Q48–Q53
D19 into §3 and swept to all six destinations Pouya named: both agent briefs
(scope + severity filter), /build Phases 2–4 (non-gating questions, scope, stop
signal, two-round cap), /wrap, and CLAUDE.md (comments record decisions, not
history). Sweep verified by command, not recalled.

Rulings applied:
  Q46(a) PUBLISHABLE — three §4 Offerings rows flipped; gate 1 records
         [Pouya's stated basis], never [verified]. Step 5 unblocked.
  Q48    closed, not site-relevant.
  Q49    one §4 row, "Mediator"; read as declining Q49(b), so worksFor stays out.
  Q50    DEVIATES — ships as name + slogan, not the concatenation. Flagged.
  Q51    OBA sections stay; the regulator/voluntary distinction recorded.
  Q52    docs/reference/deploy-credential-verification.md — 18 read-only AWS
         calls, re-run rather than transcribed, access key ID redacted.
  Q53    memberOf emitted on /about/'s Person node.

Two review rounds. The headline finding was this session's own: the Q53 sweep
was asserted and never run, leaving six in-scope records saying memberOf was
withheld — including §12 R10, which is read aloud every session. Round 2 then
found that round 1's simplification had put memberOf on / as well; the per-page
opt-in is restored, because Pouya's ruling turns on /about/'s visible HTML.

Also fixed: MEMBERSHIP_ORGS had orphaned BOUTIQUE's D16 JSDoc; /'s title now
derives from the constants; §7's deploy row stated and retracted three facts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148NztQskLKKApP5SzAA78e
2026-08-28 15:52:52 -04:00

947 lines
42 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 NOT BUILT, DELIBERATELY, and this is the only spec item this
* page does not deliver. `src/content/insights/` is empty: the collection ships
* at build step 7, which is also where `ArticleCard` and the drafted slate
* arrive (docs/01 §Build order; docs/03 §Launch article slate, D9). Rendering
* the section now means importing a component with nothing to render — its
* scoped CSS ships to every visitor for an empty block — and a props surface
* with no call site, which is already an open finding against InfinityMark.
* SiteHeader gates the Insights NAV item on the same collection, so the page
* and the nav appear together. Do not "finish" this by hardcoding placeholders.
*
* 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 Q.Arb stage is stated on the page and not
* only in the footer, 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 SectionHeading from '../components/SectionHeading.astro';
import portrait from '../assets/pouya-lajevardi.jpg';
import ogDefault from '../assets/og-portrait.jpg';
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 THE FOURTH IS NOT DECORATIVE.
*
* docs/01 §`/` item 2 says "Three slots". §4 Offerings is the higher authority
* on claims and attaches a PAIRED-DISCLOSURE CONDITION to offering arbitration
* at all: the site "makes the first while stating the second plainly", and
* "neither half may be dropped". This page says *arbitrator* in its second
* sentence, so the stage of the arc belongs on this page rather than only in
* the site footer. docs/03 authorises the fourth slot; docs/03 has been amended
* to record that on `/` it is required. Q.Arb reads as commenced — never as
* held or nearing completion (§4 Forbidden).
*/
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);
---
<BaseLayout
title={`${SITE.name} · ${SITE.tagline}`}
description="Commercial mediation and 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>
{
/* TWO CORRECTIONS FROM `claims-auditor`, 2026-08-27, both about
implication rather than assertion — which is where D13 says the risk
actually lives.
(a) This read "I mediate and arbitrate commercial disputes". §4
verifies that he ACCEPTS arbitral appointments, and separately
verifies "multiple completed sole mediations" — there is no
counterpart row for a completed arbitration. Present-indicative
"arbitrate" beside "mediate" invites the reader to supply a track
record for both. The offering-shaped form is what the register
actually holds, and it is already the form the arbitration card
below uses.
(b) "facts most neutrals take on faith" is a COMPARATIVE assertion
about a population of third parties, and **Q41(b) CLOSED 2026-08-27:
it is not restored, and the reason is not only compliance.** Pouya:
*"That is an unverifiable empirical claim about other practitioners,
and comparative claims must be factual and verifiable. It is also
weaker copy: assert his capability, not the field's incapability."*
It is struck from `docs/03`'s core positioning statement too — the
approved-copy defence is gone, because the approved copy changed.
His replacement wording is used verbatim: *"built for disputes that
turn on the contract, the code, and the engineering documents"*. The
interim ("the documents rather than the pleadings") is also gone; it
said nothing about other neutrals but it still worked by contrast.
ON THE ECHO OF THE HEADLINE, because it is deliberate and one edit
from being reversed if he reads it as a stumble. The `<h1>` ends
"the contract, the code, and the room"; this sentence re-runs the
triad and swaps the third term for "the engineering documents". Two
of three words repeat forty words apart. Read as a rhyme it does the
work of the whole positioning statement in one move; read as an
oversight it looks careless. Judged the first, flagged as the
second. */
}
<p class="hero-lede">
I mediate commercial disputes 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>
{
/* THE FEE CLAIM IS GONE, and it was wrong on two counts —
`claims-auditor`, 2026-08-27, checked against `docs/07-fees.md`.
"at one published rate" reads as ONE PRICE for half and full day;
D14's card sets TWO ($2,000 half, $4,000 full). And "preparation
time included" was unqualified where `docs/07` bundles a CAPPED
allowance and says in terms: *"must be stated on the page —
'including 2 hours of preparation' ... Do not quietly fold it into
the hours figure. At these rates, saying preparation is included
is the selling point, not a footnote."*
A home card is the wrong place to state it properly, and stating
it improperly misdescribes money. `/mediation/` (step 4) and
`/fees/` (step 9) carry the card. "Published" was also
forward-looking: `/fees/` does not exist yet. */
}
<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>
{
/* docs/03's model sentence, and BOTH halves are required: §4
Offerings — "neither half may be dropped". Pouya's instruction:
being open about the stage is the differentiator, so it is not
hedged into vagueness and not dropped. Commercial matters only
(Q39): family arbitration is not offered. */
}
<p class="feature-body">
I accept sole, party-appointed and co-arbitration appointments in
commercial matters. The Q.Arb pathway commenced in August 2026;
C.Med-Arb is the endpoint.
</p>
<span class="feature-arrow" aria-hidden="true">&rarr;</span>
</article>
</div>
{
/* docs/01 §`/` item 4: "Med-Arb named here as the long-term arc,
linking to /med-arb/." It has had its own §4 Offerings row since
2026-08-27 (Q35), so it is named as offered rather than only as an
aspiration — but the arc is what docs/01 asks this page to carry. */
}
<p class="pair-note">
<strong>Med-Arb</strong> combines the two: one neutral mediates, then arbitrates
whatever has not settled. I accept those appointments, and C.Med-Arb is the
designation endpoint. 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: NOT BUILT AT STEP 2. See the header note. -- */}
{/* ---- 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>
/* --- 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;
}
.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);
}
.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>