feat: build step 3 — /about/, minus its memberships; close Q40–Q43
Build and deploy / build-and-deploy (push) Failing after 6s

Applies Pouya's rulings on Q42, Q41(a)(b)(c), Q43 and Q40, then builds
`/about/` — six of docs/01's seven items.

`/about/` ships WITHOUT a memberships group. R10 is a prohibition on
shipping a page that lists memberships before they are re-confirmed; the
re-confirmation is a fact only Pouya holds and was not obtained. The
first version published all four and disclosed the gap in five places
instead; both review agents rejected that. Q44 carries the question.

Rulings:
- Q42 — ENE, dispute-system design and pre-dispute technical advisory
  rowed; settlement counsel struck as a partisan role. The strike
  exposed a hole in the offering test, which now states the prior
  question it was missing.
- Q41(a) — Q37 reaches prose, and prose is held to a higher bar. The
  sentence is now one constant, ASYMMETRY_LINE, because two hand-typed
  copies had already diverged inside one session.
- Q41(b) — not restored; the implication turned out to be in three
  places, two of which survived the sweep that closed it.
- Q41(c) — verified against the LAT's own Rules and extracted into
  docs/reference/lat-case-conference.md. Rule 2.4 makes "Pre-Hearing
  Conference" the Tribunal's own term for a case conference; the Rules
  contain zero occurrences of `mediat` in 66,593 characters.
- Q43 — the timings are service commitments; PROCESS_FRAMING renders
  adjacent to them, not in a lede above.
- Q40 — bundled to step 7 as R15, blocking cutover.

Four review passes, 43 findings, nine of them defects in their own
predecessors' fixes. The worst was mine: the false universal Q39 struck
reached a public page. Also fixed a portrait ladder that upscaled 1.93x
at 1024/DPR2 on BOTH pages — the shipped home page included — because
its 960 ceiling was derived from the layout range where the image is
narrowest.

Verified: check/lint/build/audit clean; 0 upscaling across 11 device
profiles; 0 overflow and 0 over-wide elements at 13 widths; 0 contrast
failures across 127 and 88 painted pairs; 0 print failures against white
paper; reveal 0 hidden under reduced-motion and print; zero JavaScript.
Lighthouse NOT RUN — tool unavailable until step 7 (R11). HTML validator
NOT RUN.

Opens Q44 (memberships), Q45 (PDF bio), Q46 (offering-test gating; the
glossary standard), Q47 (jobTitle without worksFor). Adds R15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148NztQskLKKApP5SzAA78e
This commit is contained in:
Pouya Lajevardi
2026-08-28 12:10:41 -04:00
co-authored by Claude Opus 5
parent 165d259f5c
commit c576b9a85f
17 changed files with 2603 additions and 147 deletions
+97
View File
@@ -0,0 +1,97 @@
---
/**
* The conversion band — maroon ground, one heading, one CTA. `docs/01` puts it
* at the foot of `/` (item 8) and it belongs at the foot of every page that
* wants an appointment enquiry.
*
* EXTRACTED 2026-08-28 ON `adversarial-reviewer`'S FINDING, and the finding was
* not "this is duplicated" but "this is duplicated AND HAS ALREADY DRIFTED."
* `/` and `/about/` carried identical markup and ~20 identical lines of CSS,
* except `.contact-body` — `52ch` on `/`, `46ch` plus a `line-height` on
* `/about/`. Two call sites, one already divergent, seventeen pages to come.
*
* NO PROPS AND NO SLOTS AT ALL, AND THAT IS A CORRECTION MADE ON REVIEW.
* This shipped with `eyebrow?`, `cta?` and a named `heading` slot, all
* defaulted, and **not one of the two call sites overrode any of them** — the
* exact pattern this repo has already deleted twice with the reasons written
* into the source: `Eyebrow.astro` (*"`tag?: 'p' | 'span'` had zero call sites,
* so its `<span>` branch was unreachable code"*) and `SectionHeading` (*"two
* mechanisms for one job… One way in."*). The header even argued against
* `title`/`body` props and then added `eyebrow`/`cta`. Strings are inlined; add
* a prop when a second call site actually needs one.
*
* The empty `Props` guard stays, though, and it is not decoration: without it
* an Astro component's props widen to `any` and `<ContactBand class="x" />`
* compiles clean while matching nothing — the parent-scope defect `CLAUDE.md`
* records four times, and the one `Pill` was caught by. Verified by probe.
*
* THE `<h2>` IS FIXED AT LEVEL 2 rather than taken as a prop. Every page that
* uses this band has an `<h1>` of its own and top-level sections at `<h2>`, so a
* configurable level here is a way to skip a heading level by accident. If a
* page ever needs otherwise, that is a spec question, not a prop.
*
* `CONTACT.responseTime` is rendered from the constant, never typed: §4 records
* it as **a public commitment** that *"must read identically on `/contact/`, in
* the inquirer confirmation email, and in any bio."*
*/
import Button from './Button.astro';
import Eyebrow from './Eyebrow.astro';
import { CONTACT } from '../data/site';
interface Props {
children?: unknown;
}
const _props: Props = Astro.props;
void _props;
---
{
/* NO BOOKING LINK, and that is not an omission: booking is parked
(AGENTS.md R6) and `CONTACT.bookingUrl` is null, so `/contact/` ships the
intake form with a reserved slot for an embed. Stated rather than silently
dropped, on every page that renders this band. */
}
<section class="section section-accent contact-band">
<div class="wrap contact-inner">
<div class="contact-copy">
<Eyebrow dot>Next step</Eyebrow>
<h2 class="display contact-h">Start with a call.</h2>
<p class="contact-body">
Tell me the shape of the matter and who is involved, and I will tell you
whether I am the right neutral for it. {CONTACT.responseTime}
</p>
</div>
<div class="contact-action">
<Button href="/contact/" variant="gold"
>Request a consultation &rarr;</Button
>
</div>
</div>
</section>
<style>
.contact-inner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-6);
}
.contact-h {
margin-block: var(--space-4) var(--space-4);
font-size: var(--text-4xl);
}
.contact-body {
/* 52ch, which is `/`'s value. The two call sites had drifted to 52ch and
46ch; 52 is the one that shipped first and was reviewed. */
max-inline-size: 52ch;
line-height: var(--leading-body);
}
.contact-action {
/* `0 1 auto` + `min-inline-size: 0`, NOT `flex: none`. `none` is `0 0 auto`,
which refuses to shrink below max-content and pushed the band into
overflow at 320px. This lets the button wrap instead. Measured on `/`. */
flex: 0 1 auto;
min-inline-size: 0;
}
</style>
+64 -5
View File
@@ -15,11 +15,53 @@
* :global(), no wrapper div, and no rule that silently does nothing.
*/
/**
* No props. A `class?: string` was declared and never passed, and a parent
* cannot reach this root regardless — see SectionHeading for the measurement
* and CLAUDE.md for the rule. The custom-property hooks below are how an
* ancestor influences it.
* NO PROPS — AND THE EMPTY `Props` INTERFACE IS LOAD-BEARING, NOT DECORATION.
*
* A `class?: string` was declared here once, was never passed, and a parent
* cannot reach this root regardless — see SectionHeading for the measurement and
* CLAUDE.md for the rule. It was deleted, and the deletion was written up as
* "passing one is now a build error". **It was not.** With frontmatter
* containing only comments, an Astro component's props widen to `any`, so
* `<Pill class="chip">` compiled with **zero** errors, matched nothing, and let
* the flex or grid child absorb the difference — silently.
*
* Measured by probe page, `<Eyebrow class>`, `<Pill class>`,
* `<SectionHeading class>`, `<Button bogus>`: `astro check` reported **3 errors
* — Eyebrow, SectionHeading, Button. Nothing for Pill.** Adding the three lines
* below takes the same probe to **4 errors, 0 hints** (and no `ts(6196)`,
* because the interface is referenced by the destructure below).
*
* CLAUDE.md names `Pill` as the next place the parent-scope defect will happen.
* The guard that was documented as protecting it was absent on exactly it.
*
* The custom-property hooks in the style block are how an ancestor influences
* this component: custom properties inherit, which is the one mechanism that
* legitimately crosses the boundary.
*/
/**
* `children` DECLARED, NOTHING ELSE. Getting to this line took two wrong turns
* and both are worth recording, because each looked correct:
*
* - `interface Props {}` — rejected by eslint
* (`@typescript-eslint/no-empty-object-type`), and it would have been the
* wrong tool anyway: `{}` in TypeScript means "any non-nullish value", not
* "no properties".
* - `Record<string, never>` — passes eslint and does reject `class`, but it
* also rejects `children`, so it broke the two REAL call sites
* (`PracticeCard.astro:34` and `/about/`'s arc) while the probe page went
* green on the thing it was testing. A fix that satisfies its own test and
* breaks production is exactly what `/build` Phase 4 warns about.
*
* Slot content arrives as `children`, so `children` is the one permitted
* property and every other prop is an error. Verified by probe: all six
* components now reject `class`, and `<Pill>text</Pill>` compiles. Deleting
* this re-disables checking at every call site.
*/
interface Props {
children?: unknown;
}
const _props: Props = Astro.props;
void _props;
---
<span class="pill"><slot /></span>
@@ -39,6 +81,23 @@
line-height: 1.4;
text-transform: uppercase;
color: var(--pill-fg, var(--text-meta));
white-space: nowrap;
/* `nowrap` UNTIL 2026-08-28, AND IT WAS FINE UNTIL A PILL HAD FOUR WORDS.
`/`'s six pills are one or two words (longest "Cross-cultural").
`/about/` ships `Commenced August 2026`, and at a 200% DEFAULT FONT SIZE
(root 32px — a real browser setting, not page zoom) that pill measured
382.6px wide with its right edge at 430.6 in a 390px viewport:
**41px of document overflow at 390, 111px at 320.** Injecting
`white-space: normal` took 390 to **0** and 320 to **63**, 63 being the
header residual docs/02 already accepts. WCAG 1.4.10 Reflow.
`normal` costs nothing at default size — a pill only wraps when it cannot
fit, which is exactly when wrapping is the right answer.
WHAT IT LOOKS LIKE AT THE EXTREME, recorded so it is not later read as a
new bug: at 320px with root at 32px, `Commenced August 2026` renders
**224 x 119px** inside `border-radius: 999px` — a three-line stadium. It
is ungainly and it is legible, in-viewport, and the alternative was
111px of document overflow. */
white-space: normal;
}
</style>
+44
View File
@@ -139,3 +139,47 @@ export function homeGraph(imageUrl?: string) {
'@graph': [professionalServiceNode(imageUrl), personNode(imageUrl)],
};
}
/**
* `/about/`'s graph — build step 3. This is where PERSON_ID actually resolves:
* every other page references `/about/#person`, and until now nothing served it
* from that URL.
*
* ONE NODE, AND THREE ADDITIONS WERE CONSIDERED AND DECLINED. Each is a
* decision rather than an omission, recorded so the next reader does not
* "complete" it:
*
* 1. `ProfilePage` as a wrapper, with `mainEntity` → Person. Accurate, and
* Google documents it. Declined: docs/04's structured-data table lists
* `Person` for this page and does not list `ProfilePage`, and a type not in
* the spec is a deviation that needs a reason. The marginal gain is not
* one. Revisit in docs/04, not here.
* 2. `BreadcrumbList`. docs/04 requires it on "all nested pages" and says it
* must MATCH VISIBLE BREADCRUMBS. `/about/` is one hop from the root, has
* no visible breadcrumb, and the header nav marks it as current — so
* emitting one would assert a navigation structure the page does not show.
* Breadcrumbs begin at the two-level pages: `/practice/<area>/`, `/insights/<slug>/`.
* 3. `memberOf` for the four memberships. Declined on R10 / **Q44** — and
* the visible page reached the same answer one round later, which is worth
* recording: `/about/` now publishes **no memberships group at all**,
* because R10 is a prohibition on shipping such a page and the
* re-confirmation was not obtained. So this field is not a stricter
* standard than the page; it is the same one.
*
* Two earlier versions of this comment were wrong on the facts. They said
* "the page publishes them visibly" (it does not, as of 2026-08-28) and
* "all four renew yearly" — §4 records yearly renewal for **the OBA
* sections and the CTF only** and says nothing about ADRIC or ADRIO. The
* widened form had propagated to four files.
*
* The reason a machine-readable membership claim is worse than a visible
* one stands regardless: a list on a page is corrected by editing the page,
* while a scraped claim is cached and re-served by systems that never
* re-read it. OCNI lapsed quietly once already. Add this when Q44 closes.
*/
export function aboutGraph(imageUrl?: string) {
return {
'@context': 'https://schema.org',
'@graph': [personNode(imageUrl)],
};
}
+124 -19
View File
@@ -56,12 +56,31 @@ export const CREDENTIALS = {
'Stitt Feld Handy — negotiation and ADR workshop series',
],
languages: ['English', 'Farsi'],
/** [verified 2026-08-26 — Pouya, AGENTS.md Q28 and the CTF addition of the
* same date] — and FOR NOW. Both the OBA sections and the Canadian Tax
* Foundation renew yearly, so every line below is a fact with a shelf life
* (AGENTS.md R10). Re-confirm at renewal, and before any page that lists
* memberships ships. NOT OCNI (lapsed) and NOT the Law Society — listing the
* LSO implies licensure, which D13 bars. Do not add either. */
/**
* [verified 2026-08-26 — Pouya, AGENTS.md Q28 and the CTF addition of the same
* date] — and FOR NOW.
*
* WHAT §4 ACTUALLY SAYS ABOUT RENEWAL, because a widened version of it reached
* a public page. §4: *"Both the OBA sections and the CTF renew yearly."* It
* says **nothing** about ADRIC's or ADRIO's renewal period. An earlier form of
* this comment read "Both the OBA sections and the Canadian Tax Foundation
* renew yearly, so every line below is a fact with a shelf life", which is two
* claims joined by a "so" that does not follow — and the widened form
* ("all four renew annually") then propagated into `schema.ts`, into
* `/about/`, and into §9 Q44. Exactly the SES-DKIM duplication shape: the copy
* that goes stale is the one nobody re-reads, and this copy became public copy.
*
* **NOT PUBLISHED AS OF 2026-08-28 — R10 / Q44.** R10 requires a
* re-confirmation *"before any page listing memberships ships"*, `/about/` is
* that page, and the re-confirmation is a fact only Pouya holds. It was not
* obtained, so `/about/` ships its Credentials section WITHOUT a memberships
* group and carries a `TODO(pouya)`. Do not render this array on a public page
* until Q44 closes.
*
* NOT OCNI (lapsed — §4: "not current, do not publish") and NOT the Law
* Society: listing the LSO implies licensure, which D13 bars. Do not add
* either.
*/
memberships: [
'ADR Institute of Canada (ADRIC)',
'ADR Institute of Ontario (ADRIO)',
@@ -114,6 +133,33 @@ export const ROLE = {
],
} as const;
/**
* THE Q41(a) SENTENCE. It lives here for the reason `ROLE` above lives here:
* *"these are the two where the wording IS the compliance."*
*
* It was hand-typed into `/` and then into `/about/`, and **the two copies had
* already diverged** — `/` used a comma ("one side, a working engineering
* practice on the other"), `/about/` used full stops — within the same session
* that wrote both. This is the sentence Q41(a) makes responsible for making the
* licence implication *"impossible rather than merely absent"*, so a silent
* divergence in it is the highest-consequence drift on the site.
*
* Pouya's ruling, 2026-08-27, kept because it is the finding rather than the fix:
*
* "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."
*
* Deleting the parallel is only half of it — a reader supplies the missing
* symmetry from silence, and for the legal half the missing half is a licence.
* Naming that half **training** is what forecloses it. Do not tidy this into a
* parallel, do not shorten it to fit a layout, and do not retype it into a page.
*/
export const ASYMMETRY_LINE =
'The two halves are not the same kind of thing, and the asymmetry is the ' +
'honest part. A law degree on one side. A working engineering practice on ' +
'the other. One is training I hold. The other is work I still do.';
/** The three credential slots. Never matter counts — AGENTS.md §4. */
export const CREDENTIAL_ROW = [
{ value: 'Q.Med', label: 'ADRIC / ADRIO designation' },
@@ -233,7 +279,15 @@ export const FEES = {
documentsOnlyComplex: 9500, // flat
// No tribunal-secretary rate — removed by Pouya 2026-08-26.
},
// ENE, settlement counsel, dispute-system design, pre-dispute technical advisory
/**
* THREE services at this rate, not four. Q42 CLOSED 2026-08-27 by Pouya:
* early neutral evaluation, dispute-system design, and pre-dispute technical
* advisory each gained a §4 Offerings row; **settlement counsel was REMOVED**
* — his words: *"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."* Do not restore
* it, and do not price it.
*/
hourly: 500, // [verified 2026-08-26]
cancellation: [
{ window: 'More than 30 days before', fee: 'No fee. Disbursements only.' },
@@ -313,19 +367,38 @@ export const PRACTICE_AREAS = [
name: 'Insurance, SABS & LAT',
chip: 'Insurance',
/**
* "LAT pre-hearing mediation" was flagged by `claims-auditor` 2026-08-27:
* `docs/01` §`/practice/insurance/` lists that phrase as a SEARCH INTENT,
* not as an offering, and as a service blurb it reads as conducting
* mediation inside the LAT's own statutory process — which is not what a
* privately retained neutral is appointed to do.
* Q41(c) CLOSED 2026-08-27 — and the verification changed the wording again.
*
* Reworded to the offering that is actually rowed in §4 (mediation) applied
* to the subject matter that is actually verified (insurance / SABS).
* Q41 asks Pouya to confirm the intent before the page ships at step 5.
* `LAT pre-hearing mediation` (a SEARCH INTENT in `docs/01`, never an
* offering) must never be published. Pouya's ruling: *"imprecise and must
* not imply appointment by the tribunal. Verify against LAT's own materials
* how its case-conference process is conducted and who conducts it."*
*
* Verified 2026-08-28 against the LAT Rules and the LAT-AABS process page,
* both extracted into `docs/reference/lat-case-conference.md`:
*
* - Rule 2.4: *"'Case Conference' has the same meaning as 'Pre-Hearing
* Conference' as defined in the SPPA."* **"Pre-hearing" is the
* Tribunal's own label**, and what it names is a case conference.
* - Rule 14.3: a **Member** presides, and is then disqualified from the
* hearing panel. Rule 14.6: parties must attend. The neutral is the
* Tribunal's, and a privately retained one cannot be appointed to it.
* - The Rules contain **zero** occurrences of `mediat` or `arbitrat`
* (0 in 66,593 characters). The concept is not in them.
*
* The interim read "private mediation of matters before the LAT", which is
* ambiguous in the one word that matters: `before` reads as *pending at* as
* easily as *prior to*. Replaced with the temporal frame the Tribunal's own
* page endorses — *"you may want to consider negotiation or mediation
* services... before filing at the LAT-AABS, and continuing... after a
* claim has been filed."*
*
* `/practice/insurance/` at step 5 must say the mediation is PRIVATE and is
* not the Tribunal's case conference.
*/
blurb:
'Accident benefits and SABS entitlement, MIG disputes, and private ' +
'mediation of matters before the LAT.',
'mediation alongside a LAT application, before filing or after.',
},
{
slug: 'shareholder',
@@ -392,9 +465,25 @@ export type _SlugCoverage = _AssertNever<_SlugsWithoutAnArea>;
*
* TIMINGS ARE `docs/01` §`/process/`'s, verbatim: "confidential intake (day 0) ·
* engagement and framing (17) · pre-session exchange (721) · the session
* (2130) · binding conclusion (30+)". `docs/03` §Process requires them REAL
* rather than illustrative, so they are not softened to "typically" — and they
* are not invented either. **They have no §4 row; that is Q43.**
* (2130) · binding conclusion (30+)".
*
* **Q43 CLOSED 2026-08-27, and the ruling went the other way from this
* comment's previous reasoning.** It read: *"`docs/03` §Process requires them
* REAL rather than illustrative, so they are not softened to 'typically'."*
* Pouya ruled that the five timings are **service commitments, the same class
* as Q27's response time** — not facts about him, so they need framing rather
* than a Verified row:
*
* "Present them as the TYPICAL shape of an engagement, explicitly not a
* guarantee: mediation timing depends on party and counsel availability,
* which he does not control. Published as typical, they are honest and
* useful; published as commitments, the first matter that slips makes the
* page false."
*
* The NUMBERS ARE UNCHANGED — softening was never the fix, and inventing them
* was never on. What changed is that they now ship with `PROCESS_FRAMING`
* below, which is **not optional**: any page rendering these steps renders it
* too. `docs/03` §Process has been amended to record the override.
*
* Step 5 is labelled from the spec but its body says what actually concludes —
* minutes of settlement in a mediation, an award where the process is arbitral.
@@ -435,6 +524,22 @@ export const PROCESS = [
},
] as const;
/**
* THE FRAMING THAT MAKES THE TIMINGS PUBLISHABLE — Q43, Pouya 2026-08-27.
*
* Not decoration and not a disclaimer bolted on: it is the condition on which
* the five numbers above may appear at all. *"Published as typical, they are
* honest and useful; published as commitments, the first matter that slips
* makes the page false."*
*
* RENDER IT ADJACENT TO THE STEPS, on every page that renders them — `/` now,
* `/process/` at step 6. A reader who sees `Days 2130` and not this sentence
* has read a commitment. First person, per `docs/03` §Voice.
*/
export const PROCESS_FRAMING =
'This is the typical shape of an engagement, not a commitment. Timing ' +
'depends on party and counsel availability, which I do not control.';
/** Seven items is the ceiling before a nav stops being scannable. */
export const PRIMARY_NAV = [
{ href: '/about/', label: 'About' },
+40 -9
View File
@@ -13,17 +13,44 @@ import SiteHeader from '../components/SiteHeader.astro';
import SiteFooter from '../components/SiteFooter.astro';
import { SITE } from '../data/site';
export type Props = SeoProps;
/**
* `preloadSerifItalic` — OPT-IN, AND IT IS A PER-PAGE DECISION SITTING IN A
* SHARED LAYOUT, WHICH IS WHY IT NEEDED A PROP.
*
* The italic face is preloaded because `/`'s `<h1>` contains
* `<em class="it">the room</em>` and a serif-to-fallback swap inside a 96px
* headline moves the whole last line. That reasoning is `/`-specific and the
* preload was unconditional.
*
* Measured on `/about/` (resource timing, cache disabled, 390x844 DPR 2):
* `instrument-serif-latin-400-italic.woff2` fetched as a `link` at **22,428 B**,
* while an enumeration of every element's computed `fontFamily|fontStyle` on
* that page returns `Instrument Serif|normal`, `Geist|normal`,
* `Geist Mono|normal`, `Geist|italic` — **no `Instrument Serif|italic`.** Total
* page transfer is ~162 KB, so it was ~14% of the page, preloaded ahead of the
* faces that actually render.
*
* OPT-IN RATHER THAN OPT-OUT: forgetting to opt in costs one line of reflow on a
* page that has an italic headline; forgetting to opt out costs 22 KB on the
* critical path of a page that does not. Fifteen pages remain, so the default
* is the one whose failure is cosmetic. docs/02 allows one italic phrase per
* headline, so set it wherever a headline uses `.it`.
*/
export type Props = SeoProps & { preloadSerifItalic?: boolean };
// SITE.locale is `en_CA` — Open Graph's underscore form. The lang attribute
// takes the BCP 47 hyphen form. One source, two spellings, no second constant.
const lang = SITE.locale.replace('_', '-');
// Destructured OFF the props before the spread below, so it does not reach
// <SEO>, which would reject it.
const { preloadSerifItalic = false, ...seo } = Astro.props;
---
<!doctype html>
<html lang={lang}>
<head>
<SEO {...Astro.props} />
<SEO {...seo} />
{
/* No SVG favicon. The mark is a shaded ribbon, not flat vector paths, so
@@ -74,13 +101,17 @@ const lang = SITE.locale.replace('_', '-');
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/instrument-serif-latin-400-italic.woff2?v=1"
as="font"
type="font/woff2"
crossorigin
/>
{
preloadSerifItalic && (
<link
rel="preload"
href="/fonts/instrument-serif-latin-400-italic.woff2?v=1"
as="font"
type="font/woff2"
crossorigin
/>
)
}
{
/* No script tag. Not "no framework", not "minimal JS" — none.
+971
View File
@@ -0,0 +1,971 @@
---
/**
* `/about/` — Biography and credentials. Build step 3 (docs/01 §Build order):
* "the credential spine everything else references."
*
* JOB (docs/01 §`/about/`): "be the page an appointing body or opposing counsel
* reads before agreeing to an appointment. This page carries the verifiable
* record."
*
* SECTIONS, against docs/01's seven-item outline:
* 1 Portrait, name, designation line → the hero
* 2 Narrative biography, 400600 words → §Background
* 3 Credentials, structured → §Credentials
* 4 The credentialing arc → §The arc
* 5 Languages and cross-cultural → §Language
* 6 Speaking and publications → OMITTED, per the spec itself
* 7 Person JSON-LD + PDF bio → JSON-LD ships; the PDF does not
*
* ITEM 6 IS OMITTED ON THE SPEC'S OWN INSTRUCTION, not by oversight: "Omit the
* section entirely until there is something in it. An empty 'Speaking' heading
* is worse than no heading." Nothing to list.
*
* ITEM 7'S PDF IS NOT SHIPPED, and the omission is stated rather than silent —
* AGENTS.md Q45. No such file exists, and a link to a missing file on the page
* an appointing body reads is worse than its absence. It is also not a
* formatting job: a one-page bio is a credential document circulated DETACHED
* from the site, where no build and no reviewer ever re-checks it. Two
* decisions there are Pouya's.
*
* THE PARENT/CHILD SCOPE TRAP, because this page uses <SectionHeading> four
* times. A parent CANNOT style a child component's root element — the rule
* compiles against the parent's cid and silently never matches. Every heading
* below is wrapped in a page-owned <div class="section-head">. Not defensive
* boilerplate: it is the fourth-instance defect CLAUDE.md records, and the
* components have had their `class` props deleted so passing one is a build
* error rather than a silent no-op.
*
* R10 / Q44 — THE MEMBERSHIPS GROUP IS NOT ON THIS PAGE. R10 is written as a
* prohibition — re-confirm *before* any page listing memberships ships — and
* `/about/` is the page it names. The re-confirmation is a fact only Pouya holds
* and was not obtained, so the group is withheld and a `TODO(pouya)` sits on
* CREDENTIAL_GROUPS below with the exact question. §4 is NOT re-stamped:
* nothing was re-checked. Q44.
*
* A first version of this page published all four and disclosed the gap instead.
* Both review agents rejected that; the reasoning is on CREDENTIAL_GROUPS.
*/
import { Picture, getImage } from 'astro:assets';
import BaseLayout from '../layouts/BaseLayout.astro';
import ContactBand from '../components/ContactBand.astro';
import Eyebrow from '../components/Eyebrow.astro';
import Pill from '../components/Pill.astro';
import SectionHeading from '../components/SectionHeading.astro';
import portrait from '../assets/pouya-lajevardi.jpg';
import ogDefault from '../assets/og-portrait.jpg';
import { aboutGraph } from '../data/schema';
import {
ASYMMETRY_LINE,
CREDENTIALS,
PORTRAIT,
ROLE,
SITE,
} from '../data/site';
/**
* `image` ON THE Person NODE — docs/04 lists it, and this page is where
* PERSON_ID (`/about/#person`) actually resolves.
*
* AN EARLIER VERSION OF THIS PAGE OMITTED IT and argued at length that it could
* not be supplied: "SEO.astro computes that URL internally and does not expose
* it." That reasoning talked itself into the wrong answer — the URL does not
* have to come from SEO.astro. `/` derives it in six lines with `getImage()`,
* and the same six lines work here. The result was two documents asserting the
* same `@id` with different property sets, which is worse than either choice
* made deliberately.
*
* EXACTLY the transform SEO.astro applies to the same source (jpeg, 1200x630),
* so Astro's asset cache returns the same hashed file rather than emitting a
* second copy for the crawler. JPEG on purpose: link-preview and structured-data
* consumers are not browsers and several still do not decode WebP, let alone
* AVIF.
*
* The withdrawn reasoning follows, because it is a good example of a comment
* arguing for a defect.
*
* NO `getImage()` CALL FOR THE JSON-LD IMAGE, unlike `/`.
*
* `/` generates the 1200x630 jpeg so the Person node can carry an absolute
* `image` URL, and it works because that page renders the ProfessionalService
* node too. Here the Person node is the whole graph, and SEO.astro already emits
* exactly the same transform of exactly the same source as `og:image`. Calling
* `getImage()` again would return the same cached asset — so this is not about
* duplicate files, it is about a second place that has to be kept in step with
* SEO.astro's transform. It is passed the URL by the layout instead.
*
* Except it cannot be: `SEO.astro` computes that URL internally and does not
* expose it. So the node ships WITHOUT `image` on this page and WITH it on `/`,
* which is a real inconsistency in a field docs/04 lists for the Person node.
* Both resolve to the same @id, so a crawler joining the two documents gets the
* image either way — but that is a hope about crawler behaviour, not a fact.
* Recorded rather than papered over; the fix is for SEO.astro to expose the URL
* it already computes, which is a component change and not a page change.
*/
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = aboutGraph(new URL(ldImage.src, Astro.site).href);
/**
* The designation line — docs/01 item 1. Assembled from constants so it cannot
* drift from §4, and ordered held-first.
*
* Q.Arb IS NOT IN IT, deliberately. It is not held (§4: "Describe as newly
* commenced, never as held or nearing completion"), and a designation line is
* precisely a list of things held. The arc section states the stage plainly,
* which is what §4's paired-disclosure condition requires — this page offers
* arbitration, so the stage appears on this page and not only in the footer.
*/
const designationLine = [
'Mediator',
CREDENTIALS.designations[0],
CREDENTIALS.education[0],
];
/**
* The credentialing arc — docs/01 item 4, and docs/03: "the credentialing
* pathway from Q.Med through Q.Arb to C.Med-Arb is stated openly as in
* progress. The brief treats that arc as part of the story rather than
* something to obscure."
*
* `state` is the load-bearing column. "Commenced August 2026" is §4's exact
* wording and the ONLY permitted wording — docs/03: not "in progress", because
* the weaker form drifts toward "nearly complete", which §4 Forbidden bars
* outright.
*/
/*
* FIVE CLAIMS CAME OUT OF THIS BLOCK, and the first was the worst thing in the
* step-3 diff. BOTH review agents found it independently, which is the strongest
* signal this loop produces.
*
* 1. ⚠️ "arbitral appointments are not gated behind it, which is why I accept
* them now" — **the false universal Q39 struck, on a public page.**
* Unscoped ("arbitral", not commercial), asserted as flat fact in the first
* person, and it publishes a proposition of Ontario law that §4 holds only
* in scoped form and deliberately does NOT stamp `[verified]`. Family
* arbitration is an arbitral appointment and it IS gated
* (`docs/reference/ontario-family-arbitration-training.md`). Q39 swept
* three instances of this universal on 2026-08-27; this was the fourth and
* the first outside a comment. §4 requires the STAGE be stated — never the
* register's gating rationale. Deleted rather than rescoped: this page has
* no business carrying the argument at all.
* 2. "on the same institutional pathway" and
* 3. "Three designations on one institutional pathway" (the section lede) —
* §4 attaches ADRIC / ADRIO to **Q.Med only**. Neither the Q.Arb row nor
* the C.Med-Arb row names a body.
* 4. "The senior hybrid designation" — a ranking claim about a third party's
* credential structure, with no row and no source.
* 5. The expansions — "Qualified Mediator", "Qualified Arbitrator",
* "Chartered Mediator-Arbitrator". Flagged as being in §11 Glossary but
* not in §4 Verified.
*
* ⚠️ ITEM 5 WAS REMOVED AND IS NOW RESTORED, AND IT IS THE ONE PLACE THIS
* SESSION WENT AGAINST A REVIEW FINDING. The reason is a SECOND finding, from
* the next audit pass, and it is a consistency point rather than a claim point:
* this page also publishes "Provincial Offences Act", "Statutory Accident
* Benefits Schedule" (as SABS) and "the ADR Institute of Canada and the ADR
* Institute of Ontario" — every one of them a §11 Glossary expansion, on exactly
* the ground the designation names were struck. *"One standard or the other."*
*
* The standard chosen is: **§11 Glossary is the source for DEFINITIONAL
* expansions** — what an abbreviation stands for — while §4 Verified remains the
* only source for claims ABOUT POUYA. Expanding `Q.Med` says nothing about him;
* "he holds it" is the claim, and that has a row. The alternative standard would
* have required stripping POA, SABS and the institute names from the prose and
* `recognizedBy` from the JSON-LD, which makes the page materially worse for a
* reader who does not already know the acronyms, in exchange for no reduction in
* risk. **Q46 asks Pouya to ratify that standard** and it is the only thing
* holding it up; if he declines, all four classes come out together.
*
* Sourcing them externally was tried first and failed: `adric.ca/designations/`
* redirects to `/designations-cee/` and its HTML contains **zero** occurrences
* of "Q.Med", "Qualified Mediator" or "Chartered Mediator" in 114,985 bytes —
* navigation only, body assembled client-side. So R14 cannot be met from the
* obvious URL, which is why this rests on §11 and on Q46 rather than on a
* committed extract.
*/
const ARC = [
{
name: 'Q.Med',
state: 'Held',
/* NOT "the designation I mediate under". That imported a PERMISSION framing
onto what §4 records as a voluntary credential, in a register that says
no designation is required to be appointed as a mediator — the
credential-as-licence slip §4 says produced a wrong answer twice. */
body:
'Qualified Mediator, held through the ADR Institute of Canada and the ' +
'ADR Institute of Ontario. The designation I hold as a mediator today.',
},
{
name: 'Q.Arb',
state: 'Commenced August 2026',
body:
'Qualified Arbitrator. Newly commenced — not held, and not nearing ' +
'completion.',
},
{
name: 'C.Med-Arb',
state: 'The endpoint',
body:
'Chartered Mediator-Arbitrator. The designation this practice is built ' +
'toward.',
},
];
/**
* The structured credentials — docs/01 item 3: "designations, education,
* certifications, memberships. Every line from AGENTS.md §4 Verified."
*
* LANGUAGES ARE NOT A GROUP HERE, and that is a heading decision rather than an
* omission: docs/01 item 5 gives them a section of their own, and having both
* an h3 "Languages" group and an h2 "Language" section would put the same two
* facts in the accessibility tree twice under near-identical names. The section
* wins because it carries the cross-cultural half, which a list cannot.
*
* NOT PRESENT, AND EACH IS A §4 DIRECTIVE RATHER THAN A GAP:
* - The Law Society. Listing it implies licensure, which D13 bars. §4:
* "Excluded deliberately, not by oversight."
* - OCNI. Not current (§4), so it is not published.
* - Any licence status, in either direction. §4 records it `[unestablished]`.
*
* TODO(pouya): Are ADRIC, ADRIO, the three OBA sections (Construction &
* Infrastructure, ADR, Civil Litigation) and the Canadian Tax Foundation all
* current TODAY, and in which month does each renew? — AGENTS.md Q44.
*
* ⚠️ MEMBERSHIPS ARE DELIBERATELY NOT RENDERED, AND THIS IS A REVERSAL.
*
* The first version of this page published all four on §4's 2026-08-26 stamp and
* disclosed the outstanding re-confirmation in a note, in this comment, in
* `schema.ts`, in §9 Q44 and on the cutover checklist. Both review agents
* rejected that, and they are right. §12 **R10** is written as a PROHIBITION —
* *"Re-confirm at each renewal, **and before any page listing memberships
* ships** — `/about/` at build step 3 is the first one that will"* — and
* documenting a prohibition is not discharging it. `CLAUDE.md` gives the
* procedure for a fact you do not have, and it is this one: leave
* `TODO(pouya)`, log the question, let the gap be visible. *"A build that fails
* on an unanswered question is a correct build."*
*
* The alternative was to ship them and call it disclosed. It was taken once and
* is recorded here as the decision it was, not as an oversight — and it came
* with a second defect on top: the note asserted *"Memberships are renewed
* annually and are listed as current"*, which (a) warranted currency the
* register cannot vouch for and (b) widened §4, which records yearly renewal for
* the **OBA sections and the CTF only** and says nothing about ADRIC or ADRIO.
*
* OCNI is the precedent and it is in §4: a membership lapsed, quietly, and the
* register now reads "not current, do not publish". Nothing tells you when.
*
* The other three groups ship. Restoring this one is one array entry, the moment
* Q44 closes — and re-stamp §4 and `CREDENTIALS.memberships` that day.
*/
const CREDENTIAL_GROUPS = [
{ title: 'Designations', items: CREDENTIALS.designations },
{ title: 'Education', items: CREDENTIALS.education },
{ title: 'Certifications', items: CREDENTIALS.certifications },
];
---
<BaseLayout
title="About · Pouya Lajevardi · Mediator, Q.Med · Toronto"
description="Pouya Lajevardi, JD, Q.Med — a Toronto mediator who also practises as a machine-learning and infrastructure engineer. Credentials, background, designations."
ogType="profile"
imageAlt={PORTRAIT.alt}
jsonLd={graph}
>
{/* ---- 1. Hero: portrait, name, designation line --------------------- */}
<section class="hero">
<div class="wrap hero-inner">
<div class="hero-copy">
<Eyebrow dot>About</Eyebrow>
{
/* THE H1 IS THE NAME, not a headline, and that is docs/01 item 1
("Portrait, name, designation line") agreeing with the search intent
it records for this page (`"Pouya Lajevardi"`, `Pouya Lajevardi
mediator`). The masthead carries the name as a brand mark; a bio page
needs it as the document's subject. */
}
<h1 class="display hero-h">{SITE.name}</h1>
{
/* EACH SEPARATOR IS INSIDE THE SPAN IT PRECEDES, not a sibling of it.
As siblings the flex container wrapped between them, leaving an
orphaned "·" at the end of line 1 at 390px. The NON-BREAKING SPACE
after the glyph is what keeps it attached — an earlier version used
`white-space: nowrap` on the whole item instead, which fixed the
orphan and broke reflow at a 200% default font size. */
}
<p class="designation">
{
designationLine.map((part, i) => (
<span class="designation-part">
{i > 0 && (
<span class="sep" aria-hidden="true">
{'·\u00A0'}
</span>
)}
{part}
</span>
))
}
</p>
{
/* THE ROLE LINE, and it is the single highest-risk sentence on this
page. Both strings come from `ROLE` in src/data/site.ts rather than
being typed here, for the reason that file gives: "these are the two
where the wording IS the compliance."
D13: the approved phrasing is "active litigation exposure", NEVER
"practice" in this context. The boutique is never named (D16). The
matter types are §4 verbatim and must not be extended without a row.
EXPLICITLY INTERIM — AGENTS.md R1, surfaced again 2026-08-28
precisely because this page is where the framing now does its
heaviest work. */
}
<p class="hero-lede">
I am {ROLE.title} at {ROLE.at}, with {ROLE.litigationLine} across{' '}
{ROLE.litigationAreas.slice(0, -1).join(', ')} and{' '}
{ROLE.litigationAreas.at(-1)}. I mediate commercial disputes and I
accept arbitration appointments in commercial matters; the Q.Arb
pathway commenced in August 2026. I also work as a machine-learning
and infrastructure engineer.
</p>
</div>
{
/* `widths` + `sizes` rather than `densities`, because the portrait is
fluid and a density ladder would size it from one assumed CSS width.
`width` and `height` are passed ALONGSIDE `widths` — without them Astro
declares the untouched 1600px master as the <img src> fallback, which
is the defect `/`'s comment records at 254,626 bytes.
⚠️ THIS COMMENT PREVIOUSLY MADE FOUR CLAIMS AND THREE WERE MEASURABLY
FALSE. Recorded rather than quietly replaced, because the false ones
were the confident ones.
(a) "Same ladder as `/`'s hero and the same reasoning" — true, and
that was the problem: `/`'s reasoning derived a 960 ceiling from
the TWO-COLUMN layout, which only engages at 66rem. Below 66rem
the hero is one column and the portrait is the full content
width. Measured: 592px at a 640 viewport, 672 at 768, 804 at 900,
928 at 1024 — needing 1184-1856 device px at DPR 2 against a 960
ceiling. **1.40x upscale at 768/DPR2, 1.93x at 1024/DPR2**, on
this page and on `/`. At 768/DPR1 a 760w file exists and is not
chosen, so part of the loss was purely a wrong `sizes` (52vw
declared against an ~88vw slot). Fixed two ways: `.hero-portrait`
is capped at 30rem below 66rem so the widest real slot is 480 CSS
px, which makes 960 exactly right for DPR 2; and a 1440 rung
covers DPR 3, which the ≥66rem range had also been missing
(429px x 3 = 1287 against 960).
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.
(b) "The LCP element on this page is the <h1>" — **false.**
`PerformanceObserver` at 1280x900: LCP element is
`IMG.portrait-img`, size 229,679; the <h1> box is 51,484, 4.5x
smaller. So the portrait IS the LCP element at desktop widths.
(c) "two words of 96px serif" — **false.** `.hero-h` sets
`--text-5xl`, which computes to **76px**. 96px is `--text-6xl`,
which is what `/` uses.
(d) "above the fold at every width" — **false.** Portrait top vs
viewport height: 782 vs 568 at 320, 752 vs 640 at 360 — entirely
below the fold at both, i.e. **0 visible px** at the two widths
`docs/02` names explicitly. 120px visible at 390x844.
SO WHY IS IT STILL `eager` AND NOT `fetchpriority="high"`? Because (b)
and (d) pull in opposite directions and the split is real. Re-measured
AFTER the cap, since the cap changes the element's size and therefore
the LCP candidate (`PerformanceObserver`, cache cleared per sample):
390x844 LCP = P.hero-lede 93,411
768x1024 LCP = IMG.portrait-img 168,161
1280x900 LCP = IMG.portrait-img 229,679
So the portrait is the LCP element from **768px up** — not "~1056px
up", which is what this comment said before the cap was measured — and
at phone widths LCP is the hero lede, a font-dependent text paint the
preloaded Geist already covers. `loading` and `fetchpriority` cannot be
conditioned on viewport. `eager` serves the tablet-and-desktop LCP;
`fetchpriority="high"` is withheld because at 320-360, where the image
is entirely off-screen, it would outrank that text paint. For
reference, `/` differs at 768 (LCP = H1.display, 135,289) because its
headline is a four-line sentence rather than a two-word name. */
}
<div class="hero-portrait">
<Picture
src={portrait}
width={960}
height={960}
widths={[380, 480, 640, 760, 960, 1080, 1200, 1440]}
sizes="(min-width: 80rem) 429px, (min-width: 66rem) 33vw, (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. Narrative biography ---------------------------------------- */}
<section class="section bio reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading eyebrow="Background" level={2}>
<span slot="heading">Two directions, one file.</span>
</SectionHeading>
</div>
{
/* 400600 WORDS, docs/03: "Tell the three tracks as one arc, not three
lists." Measured, not estimated — see the word-count assertion in the
verify step of this session's Change Log entry.
Q41(a) IS APPLIED THROUGHOUT AND THIS IS THE FIRST PAGE WRITTEN UNDER
IT. Pouya's ruling, 2026-08-27: the implication test reaches prose, and
prose is held to a HIGHER bar — "state the asymmetry explicitly rather
than relying on a parallel construction to carry it." So the fourth
paragraph names which half is training and which is work, in as many
words. Avoiding the noun pair "law and engineering" is not sufficient
on its own: a reader can supply the missing symmetry from silence, and
for the legal half the missing half is a licence.
EVERY CLAIM TRACES TO §4 Verified: the JD, the boutique role, active
litigation exposure and its four matter types, Q.Med, multiple
completed sole mediations, arbitration appointments (§4 Offerings,
scoped to commercial), the Q.Arb pathway commenced August 2026,
C.Med-Arb as the goal, engineering practice, SML Company Ltd, Farsi,
Iranian-Canadian. Nothing here asserts or implies licensure. */
}
<div class="prose bio-prose">
<p>
I came to dispute resolution from two directions, and I still work in
both.
</p>
<p>
The first is law. I hold a JD from Bond University, and I am{' '}
{ROLE.title} at {ROLE.at}. That role gives me {ROLE.litigationLine} — personal
injury, construction, regulatory matters under the Provincial Offences Act,
and accident benefits under the SABS. What that exposure is actually worth
in a mediation is unglamorous: I have seen how these files get built. Which
productions turn out to be thin. Where expert reports talk past each other
rather than disagree. Which issues resolve once someone puts the documents
in order, and which ones never will.
</p>
<p>
The second is engineering. I work as a machine-learning and
infrastructure engineer. That is current practice, not a former career
and not an interest: I read code, model documentation, deployment
topology, and the operational records that show what a system did
rather than what a specification said it would do.
</p>
{
/* FROM A CONSTANT — ASYMMETRY_LINE in src/data/site.ts. It was typed
here and separately on `/`, and the two copies had already diverged
(full stops here, a comma there) inside the session that wrote both.
Q41(a) makes this the sentence responsible for foreclosing the
licence implication, so it is the worst string on the site to let
drift. */
}
<p>{ASYMMETRY_LINE}</p>
<p>
Mediation is where they meet. I hold the Q.Med designation through the
ADR Institute of Canada and the ADR Institute of Ontario, and I have
completed multiple sole mediations. I accept arbitration appointments
in commercial matters — as sole arbitrator, as a party-appointed
arbitrator, and in co-arbitration. Where a matter turns on a technical
question, I read the technical material myself.
</p>
<p>
My Q.Arb pathway commenced in August 2026, and C.Med-Arb is the
designation I am working toward. I state the stage openly because an
appointing body will establish it anyway, and because a reader can do
more with the fact than with a hedge. I would rather say where I am on
the arc than leave it to be inferred.
</p>
{
/* BOTH ADDITIONS TO THIS SENTENCE CAME BACK OUT. §4 verifies exactly
one relation — *"Operator of SML Company Ltd. alongside the
practice"* — and that is now all it says.
"It is not a law firm and does not hold itself out as one" — a
negative REGULATORY statement with no row, attached to the one
§4 row carrying an express caution against being read together
with the licence row *"into an implication that neither row
makes."* Added to be helpful; it touches exactly what §4 says
not to touch.
"the company through which the engineering work is done" — a
corporate-structure claim. §4 verifies operation alongside the
practice, the jurisdiction of incorporation and the place of
business — not which work runs through which vehicle. */
}
<p>I run SML Company Ltd alongside both.</p>
<p>
I have also completed the Kompass Arbitration Certificate Program and
the Stitt Feld Handy negotiation and ADR workshop sequence. Neither is
a designation, and I name them precisely for that reason: process
training is the easiest thing in this field to assert loosely, so it
is worth stating exactly what it was.
</p>
{
/* THE NEUTRALITY LINE. It is a disclaimer and it earns its place:
docs/03 requires the equivalent on `/for-parties/`, and this is the
page an appointing body reads. It also states the negative of the
implication §4 Forbidden bars — "acts for clients", "represents
parties" — which is a stronger position than merely never asserting
it. Q42's reasoning is the same reasoning: Pouya struck settlement
counsel because a partisan role "undercuts the brand's central
claim". This sentence is that claim, stated.
The family-law exclusion is NOT here. Pouya scoped it to
`/practice/shareholder/` — "One sentence, not a section" — and
widening it to this page is his call, not an implementer's. */
}
{
/* THIS SENTENCE HAS NOW BEEN WRONG IN BOTH DIRECTIONS, WHICH IS WHY
THE THIRD VERSION AVOIDS THE AXIS ALTOGETHER.
"I do not give legal advice" — flagged because "do not" describes
an ELECTION, and an election implies the entitlement to choose.
"I cannot give legal advice" — flagged on the next pass because
"cannot" is a DENIAL of entitlement, and §4 on licence status
is explicit: *"Do not assert it, do not deny it, do not infer
it from anything else here."*
Both readings are correct and they point in opposite directions,
because both sentences make a claim about CAPACITY. So this one does
not: it states the ROLE and its consequence for the reader, which is
the form `docs/03` actually sanctions on `/for-parties/` (*"the
mediator is not your lawyer"* — role, not capacity) and the only one
that asserts nothing and denies nothing. The underlying question is
R1's. */
}
<p>
I act as a neutral. I do not act for a party in a matter I take, and
each party should have their own legal advice.
</p>
</div>
</div>
</section>
{/* ---- 4. The credentialing arc -------------------------------------- */}
{
/* SECTION 4 BEFORE SECTION 3, and the reorder is deliberate. docs/01 lists
credentials (item 3) then the arc (item 4). The arc is the part a reader
is likely to have a question about — it is the thing this practice is
candid about that others are not — and burying it under a scannable list
of things already held reads as a footnote to them. §4's paired-disclosure
condition also wants the stage stated where the offering is made, and the
offering is made in the narrative directly above. The list follows. */
}
<section class="section section-inverse arc-section reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="The arc"
level={2}
lede="Three designations. Two of them are ahead of me, and saying so is the point."
>
<span slot="heading">Where the credentials sit.</span>
</SectionHeading>
</div>
{
/* `role="list"` RESTORED, AND THE REASON I REMOVED IT WAS WRONG ABOUT
ARIA. The comment here claimed that on an <ol> the role "re-announces
an ordered list as an unordered one". It does not: **both <ul> and <ol>
map to the `list` role**, so `role="list"` on an <ol> is a no-op for
ordering, not a downgrade. What it is actually for is the WebKit
heuristic that strips list semantics from a list with
`list-style-type: none` — and `.arc` sets exactly that.
It also left the two <ol>s on this two-page site DISAGREEING, with
`/`'s `.process-strip` keeping the role. That is the state that gets
copied seventeen times.
Not verified here: whether WebKit's heuristic covers <ol> as well as
<ul>. There is no Safari instrument in this environment, so the role
stays on the precautionary side, which costs nothing. Chrome's AX tree
exposes `.arc` as `list` with three `listitem` children either way. */
}
<ol class="arc" role="list">
{
ARC.map((stage) => (
<li class="arc-item">
<h3 class="arc-name">{stage.name}</h3>
<p class="arc-state">
<Pill>{stage.state}</Pill>
</p>
<p class="arc-body">{stage.body}</p>
</li>
))
}
</ol>
</div>
</section>
{/* ---- 3. Credentials, structured ------------------------------------ */}
<section class="section section-alt creds reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading eyebrow="Credentials" level={2}>
<span slot="heading">The verifiable record.</span>
</SectionHeading>
</div>
<div class="cred-grid">
{
CREDENTIAL_GROUPS.map((group) => (
<div class="cred-group">
<h3 class="cred-title">{group.title}</h3>
<ul class="cred-items" role="list">
{group.items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
))
}
</div>
{
/* THIS NOTE IS GONE, AND IT CARRIED THREE SEPARATE DEFECTS. It read:
"Memberships are renewed annually and are listed as current. Nothing
above asserts a licence to practise law, in either direction."
(a) "renewed annually" WIDENED §4, which records yearly renewal for
the OBA sections and the CTF only and says nothing about ADRIC
or ADRIO. The widened form had already propagated to four places.
(b) "listed as current" was an affirmative public WARRANTY of
currency stacked on top of an undischarged R10 — the reminder
whose entire purpose is that no such warranty be made without a
re-confirmation. The memberships group is now off the page
(Q44), so the sentence has nothing left to warrant either.
(c) "Nothing above asserts a licence to practise law, in either
direction" READS AS A DENIAL. §4 on licence status: "Do not
assert it, do not deny it, do not infer it from anything else
here." It was also the only sentence on the site that raised
licensure at all, on the page where R1 says the D13 framing is
already doing its heaviest work — and no spec asked for it.
Both review agents flagged (c) independently and escalated it to
Pouya rather than rewriting it. That is the right destination: R1. */
}
</div>
</section>
{/* ---- 5. Language and cross-cultural practice ----------------------- */}
<section class="section language reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading eyebrow="Language" level={2}>
<span slot="heading">English and Farsi, without an interpreter.</span>
</SectionHeading>
</div>
{
/* §4 verifies "Bilingual English and Farsi" and "Iranian-Canadian;
cross-cultural fluency with diaspora business communities".
NO QUANTITY AND NO COMPARATIVE. An earlier draft opened "many of the
disputes I am best placed to take", which carries a count I do not
have and the token "best" — which §4 Forbidden bars as a superlative
and which a forbidden-terms sweep would flag on sight. Rewritten to a
claim about the work: some disputes are not separable from the
relationship, and this is what working in the parties' own language
changes. Nothing about other neutrals — Q41(b). */
}
<div class="prose">
<p>
I mediate in English and in Farsi. I am Iranian-Canadian, and some
commercial disputes are not separable from the relationship between
the parties — family-held companies and diaspora businesses in
particular, where the commercial disagreement and a much longer
history arrive together.
</p>
<p>
Working in the parties' own language, with no interpreter in the room,
changes what gets said and how early it gets said. It removes a layer
between a party and their own account of events.
</p>
</div>
</div>
</section>
{/* ---- Contact band --------------------------------------------------- */}
<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-5xl, not --text-6xl. `/`'s headline is a sentence and needs the
display ceiling; this is a two-word name, and at 96px it sets 15
characters across a line that then has nothing to balance against. */
font-size: var(--text-5xl);
/* `anywhere`, NOT `break-word`. The type scale is rem-based, so at a 200%
DEFAULT FONT SIZE this heading computes to 88px and "Lajevardi" — one
unbreakable 9-character word — is wider than the 224px content box at a
320px viewport. `break-word` permits a break at layout time but does NOT
reduce min-content size, so it would not have helped; `anywhere` does.
It has no effect at any normal size: a word only breaks when it cannot
fit. Breaking a name mid-word is ugly and it is better than a reader at
200% zoom losing the page. WCAG 1.4.4 / 1.4.10. */
overflow-wrap: anywhere;
}
.designation {
display: flex;
flex-wrap: wrap;
/* THE GAP CARRIES THE SEPARATOR'S SPACING, not a margin on .sep. The
separator is aria-hidden, so its box must not be what a sighted reader
depends on for rhythm while a screen-reader user gets nothing — with
`gap` the spacing survives the element being ignored. */
gap: var(--space-1) var(--space-3);
align-items: baseline;
font-family: var(--font-mono);
font-size: var(--text-sm);
letter-spacing: var(--tracking-tight);
color: var(--text-secondary);
}
/* NO `white-space: nowrap` — IT WAS HERE AND IT BROKE REFLOW. Gluing the whole
item together made "Q.Med (ADRIC / ADRIO)" unbreakable, and at a 200%
DEFAULT FONT SIZE that pushed this page to 108px of overflow at 320px
against `/`'s accepted 63. The separator does not need the whole item to be
unbreakable — it only needs to stay attached to the FIRST word, which the
non-breaking space in the markup does. WCAG 1.4.10. */
.designation-part {
display: inline;
}
.designation .sep {
/* NOT `var(--rule)`. Gold `#c9a876` on cream measures **2.10:1** — the one
hard constraint in `docs/02` and `tokens.css`, which say in terms that
gold is never a text colour on cream. An in-browser audit of all 100
text-bearing elements on this page returned exactly two failures and both
were this span. `aria-hidden` does not dispose of it: these separators are
the only thing dividing three credential items, and at 2.10:1 they are
invisible, so the line reads as a run-on. That is a legibility failure
before it is a rule breach. `--text-meta` measures 5.47:1 on cream and
still recedes from the 11.75:1 text beside it. */
color: var(--text-meta);
}
.hero-lede {
max-inline-size: var(--width-prose);
font-size: var(--text-lg);
line-height: var(--leading-relaxed);
color: var(--text-secondary);
}
/* `<Picture>` emits an <img> wrapped in a <picture>, and the <picture> is the
box the grid sizes — the `class` prop lands on the <img>, which is the
defect InfinityMark.astro records for its own flex sizing.
THIS COMMENT CLAIMED "the <picture> WRAPPER carries no cid, so it needs
`:global()`". **That is false.** Read from the built HTML: the emitted markup
is `<picture data-astro-cid-ta2fbyqs="true">` — Astro DOES propagate the
page's scope attribute to both elements for `astro:assets` components (it
does not for ordinary user components, which is the real rule CLAUDE.md
records). Decisive corroboration in the shipped CSS: InfinityMark's BARE
`picture` selector compiles to `picture[data-astro-cid-usztftas]` and
demonstrably works. So `:global()` was unnecessary and merely broader than
intended — it would also have matched a <picture> inside any child
component placed here. Plain selector, correct comment.
4/5 rather than 1/1 — the master is square, and 4/5 is the crop `/` uses.
One portrait treatment across the site rather than two. */
.hero-portrait {
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: var(--radius-lg);
background: var(--bg-raised);
/* THE CAP IS THE UPSCALE FIX, not a style preference — see the <Picture>
comment. Uncapped, this slot ran to 928 CSS px at a 1024px viewport, which
is 1856 device px at DPR 2 against a 960w ceiling. `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;
}
.hero-portrait picture {
display: block;
block-size: 100%;
}
.portrait-img {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
/* Above centre: the head sits in the upper half of a square crop. */
object-position: 50% 22%;
}
@media (min-width: 66rem) {
.hero-portrait {
/* Released above 66rem: the grid track is already 390-476px, so the cap
is inert — and leaving it in place would silently become the constraint
if the track ever widened. The track governs here, not this number. */
max-inline-size: none;
}
.hero-inner {
/* 1fr / 0.62fr — the portrait is smaller than `/`'s 0.72 because this
hero's copy block is a name plus two short lines and the picture would
otherwise dominate a page whose subject is the text. */
grid-template-columns: 1fr 0.62fr;
gap: var(--space-8);
}
}
/* --- 2. Background --------------------------------------------------- */
/* 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). */
/* NO `> p + p` RULE HERE. It was the only paragraph-spacing rule in this
file and it is now redundant: global.css owns `.prose` paragraph spacing as
of 2026-08-28, which is what stopped §Language below from rendering its two
paragraphs as one block. Two rules setting the same property to the same
value is one rule that will eventually disagree. */
.bio-prose {
font-size: var(--text-lg);
line-height: var(--leading-relaxed);
}
/* NO `.bio-prose i` RULE. It styled a statute-name italic that the bio no
longer uses: `Geist` ships NO italic face (every Geist `@font-face` is
`font-style: normal`), so the `<i>` rendered as SYNTHETIC OBLIQUE, and the
serif italic that would have set it properly is no longer preloaded on this
page. "Provincial Offences Act" is set in roman. If a statute name ever
needs italics here, load a face for it first. */
/* --- 4. The arc ------------------------------------------------------ */
.arc {
display: grid;
/* `min(18rem, 100%)` rather than a bare 18rem floor: a bare floor cannot
shrink below itself and overflows at a large default font size. The
credential row on `/` is the measured instance of that mistake. */
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
gap: var(--space-6);
/* NO `padding: 0` OR `list-style: none` HERE — `global.css`'s
`ul[role='list'], ol[role='list']` reset already supplies both, and this
block was re-implementing it by hand. Two rules for one job, and the
hand-written copy is the one that drifts. `margin: 0` also comes from the
global `* { margin: 0 }` reset. */
}
.arc-item {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding-block-start: var(--space-4);
border-block-start: 1px solid var(--line-dark);
}
/* <h3>, not <p>. These are the headings of the three arc items and they set
at --text-2xl serif, so marking them up as paragraphs was the fake-heading
pattern: a screen-reader user got no heading navigation for the one section
on this page a reader is most likely to jump to. `ProcessStep` on `/` uses
<h3> for exactly this shape. Outline stays h1 -> h2 -> h3 with no skips. */
.arc-name {
font-family: var(--font-serif);
font-size: var(--text-2xl);
line-height: var(--leading-tight);
}
.arc-state {
margin: 0;
}
.arc-body {
margin: 0;
font-size: var(--text-base);
line-height: var(--leading-body);
/* NOT --text-secondary. On an inverse ground `--ink-soft` measures ~1.4:1
against `--ink` — the inherited `--text-inverse` (cream, 16.81:1) is what
carries body copy here, so the colour is deliberately left alone rather
than set to a token that is correct only on cream. */
}
/* --- 3. Credentials -------------------------------------------------- */
.cred-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
gap: var(--space-7) var(--space-6);
}
/* THESE ARE HEADINGS AND THEY WERE DRESSED AS EYEBROWS. The previous rule set
11px / Geist Mono / 0.18em / uppercase / --text-meta, which is `docs/02`'s
eyebrow specification exactly — and `docs/02` says in terms: "An eyebrow is
not a heading and never carries the <h*>." `Eyebrow.astro` restates it.
The measurable consequence was worse than the rule breach: at 11px these
<h3>s were SMALLER than the 12px eyebrow above them and 5px smaller than
the 16px list items they head, so "MEMBERSHIPS" was the least legible text
on the page an appointing body reads.
Resolved by keeping the <h3> — these genuinely are the headings for their
lists, and the heading navigation is worth more than the styling — and
dropping the eyebrow treatment. The gold rule stays: gold is sanctioned for
dividers, and a 1px border is not text. */
.cred-title {
font-size: var(--text-base);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-tight);
color: var(--text-secondary);
padding-block-end: var(--space-3);
border-block-end: 1px solid var(--rule);
}
.cred-items {
margin: var(--space-4) 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-3);
font-size: var(--text-base);
line-height: var(--leading-snug);
}
/* NO `.cred-note` RULE. The note it styled was removed — three defects in one
sentence, recorded in the markup above — and the rule went with it rather
than shipping as dead CSS to every visitor. Noted so the next reader does
not re-add a note to fill a rule that no longer exists. */
/* 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>
+258 -92
View File
@@ -29,6 +29,7 @@
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';
@@ -39,12 +40,13 @@ import portrait from '../assets/pouya-lajevardi.jpg';
import ogDefault from '../assets/og-portrait.jpg';
import { homeGraph } from '../data/schema';
import {
CONTACT,
ASYMMETRY_LINE,
CREDENTIAL_ROW,
CREDENTIAL_ROW_ARB,
PRACTICE_AREAS,
PORTRAIT,
PROCESS,
PROCESS_FRAMING,
SITE,
} from '../data/site';
@@ -87,6 +89,7 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
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">
@@ -125,21 +128,35 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
below uses.
(b) "facts most neutrals take on faith" is a COMPARATIVE assertion
about a population of third parties. It comes from `docs/03`'s core
positioning statement, so it is approved copy — but `docs/03`'s own
compliance checklist requires "any comparative claim is factual and
verifiable", and §4 has no row for it. Replaced with a
non-comparative distinction that is checkable and says the same
thing: documents rather than pleadings. Q41 asks Pouya whether the
original may be restored; changing approved copy back is his call,
and shipping an unverifiable comparative in the meantime is not. */
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 arbitration
appointments. I also practise as a machine-learning and infrastructure
engineer, so the matters I take are the ones that turn on the
documents rather than the pleadings: the change order, the model card,
the System Impact Assessment, and the regulatory overlay around them.
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">
@@ -152,9 +169,39 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
{
/* `widths` + `sizes` rather than `densities`: the portrait is fluid, and
a density ladder would size it from one assumed CSS width. The largest
real render is ~476px (content 1280 - 96 gutter - 64 gap, x 0.425),
so 960 is the 2x ceiling.
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
@@ -168,24 +215,79 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
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 five widths [measured 2026-08-27].
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).
eager + fetchpriority=high because this is the LCP candidate on the
page docs/04 budgets hardest. */
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]}
sizes="(min-width: 80rem) 476px, (min-width: 66rem) 42vw, (min-width: 40rem) 60vw, 92vw"
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"
fetchpriority="high"
class="portrait-img"
/>
</div>
@@ -234,26 +336,62 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
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. Whether Q37's reasoning formally extends to prose is
Pouya's to say — Q41. */
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 has two readings: what the documents say, and
what the engineering says. They are not blended here. They run at
the same time.
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. The second half of each pair usually arrives as
a separate expert report.
and a load question.
</p>
<p>
I do both readings myself. That is the whole of it, and it is why
the technical half is not something a party has to commission and
wait for.
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:
@@ -419,14 +557,21 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
{
/* THIS SAID "All six areas, and what else is offered", which asserted
offerings beyond the six. The only candidates — early neutral
evaluation, settlement counsel, dispute-system design, pre-dispute
technical advisory (`docs/01` §`/practice/`) — have NO row in §4
"Offered now", and §4 states: "An offering may not be published until
it has a row." `claims-auditor` called this the Med-Arb-in-the-footer
defect in a new place, and it is exactly that. Reworded to what is
rowed; the four candidates need rows before `/practice/` can offer
them at step 5. */
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>
@@ -441,7 +586,7 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
<SectionHeading
eyebrow="How it runs"
level={2}
lede="Five steps, from the first call to the conclusion. Counsel want the shape of the downside before they commit a client's day."
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>
@@ -457,6 +602,17 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
}
</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
>
@@ -468,28 +624,13 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
{/* ---- 8. Contact band ---------------------------------------------- */}
{
/* docs/01 item 8 is "Intake CTA and booking link". THERE IS NO BOOKING LINK
and that is not an omission: booking is parked (AGENTS.md R6) and
CONTACT.bookingUrl is null, so /contact/ ships the intake form with a
reserved slot for an embed. Stated rather than silently dropped. */
/* 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. */
}
<section class="section section-accent contact-band">
<div class="wrap contact-inner">
<div>
<Eyebrow dot>Next step</Eyebrow>
<h2 class="display contact-h">Start with a call.</h2>
<p class="contact-body">
Tell me the shape of the matter and who is involved, and I will tell
you whether I am the right neutral for it. {CONTACT.responseTime}
</p>
</div>
<div class="contact-action">
<Button href="/contact/" variant="gold"
>Request a consultation &rarr;</Button
>
</div>
</div>
</section>
<ContactBand />
</BaseLayout>
<style>
@@ -536,6 +677,15 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
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
@@ -555,6 +705,13 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
}
@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);
}
@@ -581,10 +738,17 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
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 {
display: flex;
flex-direction: column;
gap: var(--space-5);
margin-block-start: var(--space-6);
font-size: var(--text-lg);
line-height: var(--leading-relaxed);
@@ -614,9 +778,10 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
/* --- 4. Two practices ---------------------------------------------- */
.section-head {
margin-block-end: var(--space-7);
}
/* 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);
@@ -752,29 +917,30 @@ const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
gap: var(--space-5);
margin: 0;
}
/* --space-5 rather than --space-6: this belongs to the strip above it, not to
the link below.
/* --- 8. Contact band ----------------------------------------------- */
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);
}
.contact-inner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-6);
}
.contact-h {
margin-block: var(--space-4) var(--space-4);
font-size: var(--text-4xl);
}
.contact-body {
max-inline-size: 52ch;
}
.contact-action {
/* `flex: none` is `0 0 auto`, so the button could not shrink below its
max-content width — 425px at a 200% default font size, in a 342px
container. `0 1 auto` plus `min-inline-size: 0` lets it. It still never
shrinks at a normal font size, because there is room. */
flex: 0 1 auto;
min-inline-size: 0;
}
/* 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>
+110
View File
@@ -283,6 +283,44 @@ a:hover {
.prose p {
max-inline-size: var(--width-prose);
}
/* `.prose` HAD NO PARAGRAPH SPACING, AND NOTHING ANYWHERE SUPPLIED IT.
The reset above sets `* { margin: 0 }`, so a bare `.prose` with two <p>
children rendered them as one block. Measured on `/about/` §Language:
**gap between paragraph 1 and paragraph 2 = 0.0px** — "…history arrive
together." running straight into "Working in the parties' own language…", on
screen and in the printed PDF.
It survived step 2 because BOTH of `/`'s prose blocks supply their own
spacing: `.approach-prose` uses `display:flex; gap`, and `.bio-prose` on
`/about/` has its own `> p + p`. So the only two call sites happened to opt
out of the defect. The rule belongs HERE, where `.prose` lives, or it has to
be remembered on all fifteen remaining pages.
`:where()` KEEPS THE SPECIFICITY AT ZERO so a component's own rule for the
SAME PROPERTY wins without `!important`.
⚠️ IT DOES NOT PROTECT AGAINST A FLEX `gap`, AND THIS COMMENT ONCE CLAIMED IT
DID — "Verified: with `:where()` the flex container's gap governs and this
contributes nothing." That was false and was measured false minutes later:
`:where()` lowers SPECIFICITY, which only matters when two rules set the same
property. A flex `gap` is a different property, so gap and margin both apply
and add. `/`'s `.approach-prose` went **24px -> 48px** on the strength of that
sentence. It has been converted to use this rule instead of a `gap`, and
`.bio-prose`'s duplicate `> p + p` was removed for the same reason. If a
future block needs different spacing, override `margin-block-start` — do not
reach for `gap`. */
:where(.prose) > p + p {
margin-block-start: var(--space-5);
}
/* THE PAGE-OWNED WRAPPER FOR `SectionHeading`, and it lives here because it was
byte-identical in two pages with seventeen to come. It exists only because a
parent cannot style a child component's root (`CLAUDE.md`; measured on
`SectionHeading`), so every page that uses a section heading needs a wrapper
it owns — which means every page needs this rule. Same argument that extracted
`ContactBand`, applied to a rule instead of a component. */
.section-head {
margin-block-end: var(--space-7);
}
.section {
padding-block: var(--section-y);
}
@@ -491,4 +529,76 @@ hr {
.section {
padding-block: var(--space-5);
}
/* THE INVERSE GROUNDS HAD TO BE NEUTRALISED AND WERE NOT — and this block's
own heading says why it matters: the About page is printed by people
evaluating an appointment.
`print-color-adjust` defaults to `economy`, so a UA drops the BACKGROUND
and keeps the text. Chrome's default print dialog has "Background graphics"
unchecked, so `.section-inverse` and `.section-accent` printed cream text
on white paper. Measured with `Page.printToPDF`, `printBackground: false`,
rasterised at 150 dpi: the dominant glyph colour across the whole arc block
was **rgb(166,164,161) — 2.49:1 against white**, and that grey is Chrome's
own legibility fudge. The DECLARED colour is cream at ~1.04:1, which is
what a UA without that fudge renders. With `printBackground: true` the
pages are correct, which is what isolates the cause.
What vanished was the Q.Med / Q.Arb / C.Med-Arb progression on `/about/` —
§4's paired disclosure — plus the contact band. `!important` because the
rules being overridden are class-level and these must win regardless of
which section variant a future page uses. */
/* TOKENS FIRST, THEN CLASSES — and the token half is the part that works.
A class-by-class version of this block shipped first and MISSED TWO
ELEMENTS, both measured under print-media emulation: `.approach-metaphor`
on `/` and `.btn-gold` on both pages stayed at `rgb(226,200,154)` —
gold-l, which is **1.62:1 against white paper** once the ground is dropped.
One is the paragraph carrying the infinity-mark argument; the other is the
call to action. Enumerating class names cannot work here: `--text-inverse-2`
is consumed by page-scoped and component-scoped rules this file has never
heard of, and there will be seventeen more pages of them.
Custom properties INHERIT, and that is the one mechanism that crosses
Astro's component-scope boundary (see `Pill.astro`). Redefining the three
inverse tokens on the section itself therefore reaches every descendant
rule, including ones written after this block. */
.section-inverse,
.section-accent {
background: transparent !important;
color: #000 !important;
--text-inverse: #000;
--text-inverse-2: #000;
--pill-fg: #000;
--pill-border: #000;
--rule: #000;
}
/* The belt-and-braces half. These four set a colour LITERALLY rather than
through a token, so the inheritance above does not reach them. */
.section-inverse .eyebrow,
.section-accent .eyebrow,
.section-inverse .lede,
.section-accent .lede {
color: #000 !important;
}
/* The dot is a background, not text, so it does not follow `color`. */
.section-inverse .eyebrow .dot,
.section-accent .eyebrow .dot {
background: #000 !important;
}
/* EVERY BUTTON, NOT JUST THE ONES ON AN INVERSE GROUND. Two of the three
variants set light text on a coloured background of their own
(`.btn-primary` cream-on-maroon, `.btn-gold` gold-on-ink), and a UA at
`print-color-adjust: economy` drops the background and keeps the text.
Measured against white paper: `.btn-gold` 1.62:1, `.btn-primary` **1.07:1**.
The first version of this rule was scoped to `.section-accent .btn-gold`,
which fixed the contact band and left the HERO CTA on `/` unreadable —
`.btn-primary` sits on cream, inside no inverse section at all, so nothing
in this block reached it. A print sweep of all 89 visible text elements
found it; the class-scoped version had passed its own narrower check. */
.btn {
background: transparent !important;
color: #000 !important;
border-color: #000 !important;
}
}