feat: build step 2 — the home page; close Q35, Q37, Q39; Q39's answer corrected the register

Pouya's rulings and the page are one commit, not two, because they are not
separable: Q37 changed the credential label the page renders, Q39 scoped the
arbitration copy, and Q35 gave Med-Arb the row that lets the footer link stand.
Splitting them would produce a commit where the register says one thing and the
page says another.

RULINGS

Q39 — ANSWERED, and my assertion was FALSE as a universal. Pouya checked
rather than defended. Family arbitration in Ontario is gated: prescribed
training, 14h on screening for domestic violence and power imbalances, 30h of
Ontario family law for arbitrators outside the bar, 10h ongoing per two years.
claims-auditor produced that counter-example from inside the repo on 2026-08-26
and it was right. The source is now IN the repo per R14 —
docs/reference/ontario-family-arbitration-training.md, retrieved with its
provenance — and §4 states which half it establishes: the family gate directly,
the commercial half only by absence. So "commercial arbitration in Ontario
requires no licence and no designation" is recorded as Pouya's stated position,
unstamped. What disposes of the question is the scope exclusion: he does not do
family arbitration, so it is NOT OFFERED and the gate never bears on the
practice.

The false universal survived in three more live places, found by grep, not
recall: §4's NOT-NEGOTIABLE boundary bullet (the boundary paragraph breaching
its own boundary), §9's Q33 closure row, and a comment in SiteHeader.astro.

Q35 — ANSWERED, all four items, and the answer supplied a gate that was
missing. Med-Arb gets an Offerings row and stays in the footer sitemap. The six
subject-matter labels pass test 1. §4 gains "Subject-matter areas — the
publication gate": competence to accept an appointment, AND a page that frames
it as positioning without claiming history. Nothing in the nav or footer is
unrowed any more.

Q37 — ANSWERED. "Law and engineering" becomes "Legal training and engineering
practice". A degree is not a practice; the parallel was doing the implying.

Q38/R13 — the committed SVG does not close it; the walk-back went too far. It
renders faithfully BECAUSE it is the raster. R13 stays open.

R1 — surfaced and acknowledged; the interim licensure framing is now carried by
a shipped page rather than a spec, which raises the stakes.

BUILD STEP 2

Seven of docs/01's eight home sections, six new components, zero JavaScript.
Section 7 (Latest insights) ships at step 7 with the collection it lists —
recorded in docs/01 and in the page, not just here. Four credential slots, not
three: §4's paired-disclosure condition requires the Q.Arb stage on any page
that offers arbitration. No booking link (R6). The masthead tagline is
suppressed on / (it duplicated the hero eyebrow). The step-1 proof sheet is
deleted, and five live references to it were found by grep.

WHAT THE REVIEWS FOUND — 24 findings across the two passes

claims-auditor FAILED it on ten, every one implication or scope rather than
fabrication, which is where D13 says the risk lives. The four that mattered:
"I mediate and arbitrate" asserted a track record §4 does not hold for
arbitration; the JSON-LD asserted arbitration twice and stated the stage
nowhere (a crawler-only claim is still a claim); "at one published rate with
preparation time included" misdescribed money against docs/07's two day rates
and capped prep allowance; and "Law and engineering are not blended here" was
Q37's struck parallel relocated into prose one day after Pouya struck it.
It also found the Canadian Tax Foundation missing from §9 Q8 — a stale second
copy of a fact that would have bitten at step 3.

adversarial-reviewer found fourteen, three blocking. The first was
class="section-head" on <SectionHeading> never matching — the parent-cannot-
style-a-child defect for the FOURTH time, written into a diff where I had just
added fresh warnings about it to two other components. Measured: 0px margin,
0px gap, headings over the card edges, with astro check and eslint both clean.
I had looked at a screenshot of that section and passed over it. Fixed with a
page-owned wrapper (48px, measured) and the prop is deleted from six components
so it is now a build error. Also: the credential row was never "two up on a
phone" and its comment said it was; PROCESS was hardcoded in the page against
the reason written in site.ts; 83px of residual overflow at a 200% default font
size, now 3px.

Seven more I found myself first, including <Picture widths> declaring the
untouched 1600px master as the <img src> fallback (254,626 B for a 476px slot,
and the build log said "before: 349kB" either way), and a prop named `as`
silently turning off type-checking for a whole component.

VERIFICATION — run, not asserted. Full figures in AGENTS.md entry (w).

npx tsc 6.0.3 · check 0/0/0 · lint clean · build clean · audit 0
1 <script> and it is JSON-LD; non-JSON-LD scripts 0; no JS bundle; identical
page with script execution disabled (444 nodes, 6,578 chars)
Phase 5 minifier check: no `animation` shorthand beside animation-timeline
overflow 0 at 14 widths, AND every mark measured at 1.5654-1.5657 vs 1.5657
one h1, no heading skips, focus order == DOM order across 44 focusables
31 painted contrast pairs at 3 widths, 0 failures
72/72 hit-test points across 8 cards resolve to the card's link
print 0 hidden, reduced-motion 0 hidden
Lighthouse NOT RUN — tool unavailable until step 7 (R11)
HTML validator NOT RUN, and 4 of 12 srcsets carry a duplicate 1x descriptor

Opened for Pouya: Q40 (one OG image for nineteen pages), Q41 (does Q37 reach
prose; may the comparative be restored; what LAT pre-hearing mediation means),
Q42 (the four "also offered" processes have no row), Q43 (the process timings
are published commitments with no row).

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-27 16:31:56 -04:00
co-authored by Claude Opus 5
parent 8a2f513577
commit 165d259f5c
26 changed files with 2636 additions and 742 deletions
+638 -32
View File
@@ -216,7 +216,8 @@ condition below, which is a **condition**, not a formality.
**Pouya's ruling, and the reasoning to keep.** His stated basis: ADRIC and ADRIO
designations — Q.Med, Q.Arb, C.Med-Arb — are **voluntary credentials, not
licences**, and **commercial arbitral appointment in Ontario is not gated behind
a designation**. So the constraint on offering arbitration was never *may he
a designation** — scoped and sourced as the box below now sets out, after the
unscoped version of that sentence turned out to be false. So the constraint on offering arbitration was never *may he
act* — only *will parties appoint someone at this stage of the arc*, which is
**positional** and his to answer. He answers it: **he accepts arbitration
appointments now**, and that — his acceptance — is what the rows below record.
@@ -224,20 +225,41 @@ appointments now**, and that — his acceptance — is what the rows below recor
> ⚠️ **The legal proposition underneath is the architect's, not this register's,
> and it is deliberately NOT stamped `[verified]`.** `[verified — Pouya]` means
> *the subject reported it*, which can establish what he offers and cannot
> establish a proposition of Ontario law. No statute or regulation is cited here
> and none is in the repo, so **no reviewer can check it** — R14 again, in a
> place that matters more than a logo.
> establish a proposition of Ontario law. That much is unchanged.
>
> It is also **not a universal, and must never be restated as one.** An earlier
> draft of this paragraph wrote "Anyone may be appointed an arbitrator in
> Ontario. Nothing in law gates the role behind a designation." `claims-auditor`
> produced a counter-example from inside this repo: the site ships a
> **Shareholder & Family Business** practice area, and **family** arbitration in
> Ontario is understood to be one of the places where arbitrator qualification
> *is* prescribed. This record does not resolve that — it is **Q39** — but it
> will not carry an unqualified universal that the site's own nav may already
> contradict. The rows below are scoped to **commercial** arbitration for that
> reason.
> **What HAS changed: there is now a source in the repo**
> `docs/reference/ontario-family-arbitration-training.md`, the extract Pouya's
> Q39 ruling named, retrieved 2026-08-27 with its provenance and the method that
> produced it. R14 is satisfied for the half of the proposition it covers, and
> **it does not cover both halves.** Read the next paragraph before relying on
> either.
>
> **THE UNIVERSAL IS STRUCK, AND IT WAS FALSE.** This paragraph carried
> *"Anyone may be appointed an arbitrator in Ontario. Nothing in law gates the
> role behind a designation."* `claims-auditor` produced a counter-example from
> inside this repo — the site ships a **Shareholder & Family Business** practice
> area, and family arbitration looked like a place where qualification *is*
> prescribed — and **Pouya checked it and confirmed the counter-example.** His
> words, 2026-08-27: *"My assertion was FALSE as a universal."* Family
> arbitrators in Ontario must complete prescribed training: **14 hours** on
> screening for domestic violence and power imbalances, **30 hours** of Ontario
> family law for arbitrators outside the Ontario or another Canadian bar, and
> **10 hours ongoing per two-year period**, five of them on domestic violence or
> power imbalance. Sourced, per the extract above.
>
> **The scoped replacement, and exactly whose it is.** *Commercial arbitration
> in Ontario requires no licence and no designation.* That is **Pouya's stated
> position** — `[Pouya's stated basis 2026-08-27]`, deliberately **not**
> `[verified]`. The extract establishes the **family** gate directly; it
> establishes the **commercial** half only by *absence*, because a government
> page about family arbitrators is not authority for what a commercial
> arbitrator needs. It neither mentions nor excludes commercial arbitration.
> **Do not cite it as if it did**, and do not let the scoped form drift back
> toward a universal because it now has a footnote attached.
>
> **The reason nothing on the site turns on any of this** is the scope exclusion
> two paragraphs down, not the reasoning above: family arbitration is not
> offered.
### The offering test
@@ -288,8 +310,16 @@ footer pending Q35(a).*
licence gates the activity.** It is not a general licence-free zone, and it must
never be read as one:
- **Mediation and arbitration are ungated.** No designation is required to be
appointed. The offering test governs, and Pouya has answered it.
- **Mediation, and COMMERCIAL arbitration, are ungated.** No designation is
required to be appointed. The offering test governs, and Pouya has answered it.
*Scoped 2026-08-27 (Q39).* This bullet read "Mediation and arbitration are
ungated" — an unqualified statement about arbitration as a class, in the
paragraph headed **NOT NEGOTIABLE**, which is the worst place in this file for
one. **Family arbitration in Ontario carries prescribed training**
(`docs/reference/ontario-family-arbitration-training.md`), so the class
statement was false and the boundary paragraph was the thing breaching its own
boundary. Family arbitration is separately **not offered** — see the Offerings
table — so the site never relied on the false half; the record did.
- **Legal services are gated.** Providing legal services and practising law in
Ontario are restricted activities. For anything on that side of the line the
binding question is **permission**, the answer is **not established** (see the
@@ -306,9 +336,11 @@ never be read as one:
| Offering | Status |
|---|---|
| **Mediation** — sole mediator | `[verified 2026-08-26 — Pouya]`. Q.Med held; §4 also verifies multiple completed sole mediations |
| **Arbitration — sole arbitrator** (commercial) | `[verified 2026-08-26 — Pouya, Q33/Q36]`. Offered **now**. Not gated by Q.Arb. **Commercial** — see Q39 on family arbitration |
| **Arbitration — sole arbitrator** (commercial) | `[verified 2026-08-26 — Pouya, Q33/Q36]`. Offered **now**. Not gated by Q.Arb. **Commercial**, and the scope is now load-bearing rather than provisional: Q39 closed 2026-08-27 and **family arbitration is excluded by Pouya's choice** — see the row below |
| **Arbitration — party-appointed** | `[verified 2026-08-26 — Pouya, Q33/Q36]`. Offered **now** |
| **Arbitration — co-arbitration** | `[verified 2026-08-26 — Pouya, Q33/Q36]`. Offered **now**. This closes Q36, which had flagged the claim as asserted in three specs and resting on no §4 row — the row is here |
| **Med-Arb** — mediation-arbitration | `[verified 2026-08-27 — Pouya, Q35]`. Offered **now**. Both halves are live: Q.Med is held, and arbitration appointments are accepted (rows above). Ungated by the same reasoning as arbitration. This closes the "Still unanswered" note that used to sit below this table, and it is why `/med-arb/` may stay in the footer sitemap |
| ~~**Family arbitration** under the *Family Law Act*~~ | **NOT OFFERED — a scope exclusion, not a gating problem** `[verified 2026-08-27 — Pouya, Q39]`. Pouya has confirmed he does not do family arbitration. It is therefore out of scope by his choice, and the prescribed-training question never arises for this practice. Recorded because the register reasoned from a false universal for one day and must not do so again — see the box above and `docs/reference/ontario-family-arbitration-training.md`. **`Shareholder & Family Business` does not touch this:** it means commercial disputes among family shareholders — shareholder and partnership disputes, co-founder breakdowns, business succession — and its page must say plainly that family law matters are not accepted |
| ~~Tribunal secretary~~ | **Not offered.** D14 removed the rate and bars offering it |
**How the arbitration page must read** (`docs/01` §`/arbitration/`, step 4):
@@ -318,10 +350,39 @@ about the stage is the differentiator — do not hedge it into vagueness.* So
neither half may be dropped. Naming the stage is not a disclaimer bolted onto an
offering; it is the reason a sophisticated reader believes the offering.
**Still unanswered — do not infer from this section.** Whether **Med-Arb** is
offered now is not recorded here; Pouya named the three arbitration forms and
mediation, and nothing else. Med-Arb is ungated by the same reasoning, so the
question is his competence call alone — but it is still a question. See Q35.
### Subject-matter areas — the publication gate (Q35, 2026-08-27)
**This gate did not exist, and its absence is what let `Med-Arb` sit in the
site-wide footer as an unrowed offering three sentences below a paragraph
telling readers not to infer exactly that.** Pouya's words: *"Add the
publication gate you identified was missing."* Same force as the two gates
above.
> **A practice area may be named — in the nav, in the footer, in a card — where
> BOTH hold:**
>
> 1. **He can competently accept an appointment in that subject matter.** His
> call, and only his. A subject-matter area is not a gated activity (the
> processes conducted in it are mediation and arbitration, both ungated), so
> the offering test's question 1 is already answered for it and question 2 is
> what remains.
> 2. **The page frames it as positioning, not as claimed history.** `docs/03`:
> *"Built to facilitate procurement and subcontract disputes on Ontario's
> megaproject pipeline"* — not *"extensive experience resolving"*. A page that
> claims volume it does not have fails this gate even though the label passes
> condition 1.
>
> Condition 1 is about the **label**; condition 2 is about the **page**. A label
> may ship before its page exists only where the page is already specified to
> meet condition 2 — which `docs/01` does for all six areas, `/practice/energy/`
> most explicitly (*"Write it as a genuine position, not a claim of existing
> volume"*).
**Applied, 2026-08-27 — Q35 CLOSED.** `Energy, Grid & Regulatory`,
`Shareholder & Family Business`, and the other four labels **pass condition 1**
on Pouya's answer; condition 2 is each page's job at build step 5. `Med-Arb`
now has an Offerings row of its own and stays in the footer sitemap. Nothing in
the nav or footer is unrowed or ungated any more.
### Forbidden — must not appear on the site
@@ -394,12 +455,14 @@ the audience it targets. Revisit at month 1218. `[verified 2026-08-25 — dec
| Thing | Value |
|---|---|
| Framework | **Astro 7.2.7**, `output: 'static'` `[verified 2026-08-26 — npm view astro version, D1 as amended]`. `@astrojs/mdx` 7.0.8, `@astrojs/sitemap` 3.7.3, `sharp` 0.35.4 |
| Lint toolchain | ESLint **10.9.1**, `@eslint/js` 10.0.1, `globals` 17.11.0, `eslint-plugin-astro` **3.1.0**, `eslint-plugin-jsx-a11y` 6.10.2, `typescript-eslint` 8.68.0, `typescript` 5.9.3**every pin at the registry's `latest` except `typescript`** `[verified 2026-08-26 — npm view, all 14 pins]`. `@eslint/js` and `globals` are declared explicitly; before 2026-08-26 `eslint.config.js` imported them and they resolved only by npm hoisting accident. **Accessibility linting is on**`flat/jsx-a11y-recommended`, 36 rules `[verified 2026-08-26 — 7 rules fired on a deliberately inaccessible .astro file]`. `eslint-plugin-jsx-a11y@6.10.2` declares a stale `eslint ^3..^9` peer range; a one-line `overrides` entry in `package.json` resolves it. ⚠️ **`eslint-plugin-astro@3.1.0` declares `node ^22.22.3 \|\| ^24.16.0 \|\| >=26.3.0`**, which excludes Pouya's Node 25.6.0 — `npm install` prints EBADENGINE there. Dev-time only, and `nvm use` (Node 22 LTS, per `.nvmrc`) clears it. `typescript` is held at 5.x: `typescript-eslint` peers `<6.1.0` and `@astrojs/check` peers `^5 \|\| ^6`, so TypeScript 7.0.2 is unusable here `[verified 2026-08-26 — npm view]` |
| Framework | **Astro 7.2.9**, `output: 'static'` `[verified 2026-08-27 — npm view astro version, D1 as amended]`. `@astrojs/mdx` 7.0.8, `@astrojs/sitemap` 3.7.3, `sharp` 0.35.4 — all three at `latest`. Bumped from 7.2.7 at the step 1 → step 2 phase boundary under R11: 7.2.8 published 2026-08-26 and 7.2.9 on 2026-08-27, i.e. **two patches appeared inside 48 hours**, which is the argument for checking at boundaries rather than on failure. `engines` unchanged at `node >=22.12.0`, `npm >=9.6.5` `[verified 2026-08-27 — npm view astro@7.2.9 engines]` |
| Lint toolchain | ESLint **10.9.1**, `@eslint/js` 10.0.1, `globals` 17.11.0, `eslint-plugin-astro` **3.1.0**, `eslint-plugin-jsx-a11y` 6.10.2, `typescript-eslint` 8.68.0, `typescript` **6.0.3****every pin at the registry's `latest` except `typescript`** `[verified 2026-08-27 — npm view, all 14 pins]`. `@eslint/js` and `globals` are declared explicitly; before 2026-08-26 `eslint.config.js` imported them and they resolved only by npm hoisting accident. **Accessibility linting is on**`flat/jsx-a11y-recommended`, 36 rules `[verified 2026-08-26 — 7 rules fired on a deliberately inaccessible .astro file]`. `eslint-plugin-jsx-a11y@6.10.2` declares a stale `eslint ^3..^9` peer range; a one-line `overrides` entry in `package.json` resolves it. ⚠️ **`eslint-plugin-astro@3.1.0` declares `node ^22.22.3 \|\| ^24.16.0 \|\| >=26.3.0`**, which excludes Pouya's Node 25.6.0 — `npm install` prints EBADENGINE there. Dev-time only, and `nvm use` (Node 22 LTS, per `.nvmrc`) clears it. **`typescript` — the hold, and it was too wide by a whole major until 2026-08-27.** `latest` is **7.0.2** and it is unusable here: `typescript-eslint@8.68.0` peers `typescript >=4.8.4 <6.1.0` and `@astrojs/check@0.9.10` peers `^5.0.0 \|\| ^6.0.0`, so taking 7 breaks **both** the linter and `astro check` `[verified 2026-08-27 — npm view <pkg> peerDependencies]`. **But this row previously read "held at 5.x", and 6.0.3 is a stable release both peers accept** — so the pin sat a full major behind for no reason the record could name, in a row whose whole job is to name the reason. Now at **6.0.3**, the newest version compatible with both peers; `npx tsc --version` reports 6.0.3 and the full gate is green `[verified 2026-08-27]`. The 6.x listing is mostly `-dev` and `-beta` tags; **6.0.2 and 6.0.3 are the only stable 6.x releases**, which is why `npm view typescript version` (7.0.2) is not the number to pin against here. Re-check at the next phase boundary: the hold ends when `typescript-eslint` widens its peer range past `<6.1.0` |
| Lighthouse CI | **Not installed, and that is now stated wherever the check is claimed.** `@lhci/cli` was the sole source of all 10 `npm audit` findings (7 high, via `lighthouse → puppeteer-core → extract-zip`), 0.15.1 is `latest` so there was no clean upgrade, and it cannot run at all — no pages, no `lighthouserc`. Removed 2026-08-26; **`npm audit` now reports 0 vulnerabilities** `[verified 2026-08-26 — npm audit]`. Removing it left a **documented control that no longer exists**`docs/04`, `CLAUDE.md` and `/build` Phase 5 all required a run that had silently become impossible, which is Q22's defect in a different costume. Fixed 2026-08-26: the unavailability is stated in `docs/04-seo-spec.md` (budget table, the callout under Performance, and the post-launch checklist), `CLAUDE.md` (performance budget and the definition of done), `.claude/commands/build.md` Phase 5, `docs/06-deployment.md` (PR checks and the cutover checklist), and `.claude/agents/adversarial-reviewer.md` §4 — which is also told **not** to raise the absence as a finding. **Re-add at build step 7** with a pin verified that day and a check for a patched release; do not assume 0.15.1 is still the ceiling (R11) |
| Node | **22 LTS line**, `.nvmrc` holds `22` so it tracks the latest 22.x rather than freezing at a floor. `package.json` declares **`engines.node` = `^22.13.0 || >=24`** (verbatim — it excludes Node 23, an EOL odd line, which a flat `>=22.13.0` would have admitted) and **`engines.npm >=9.6.5`** — the intersection of every dependency's own floor: Astro 7 and `@astrojs/mdx` 7 need `>=22.12.0`, ESLint 10 needs `^20.19.0 \|\| ^22.13.0 \|\| >=24` `[verified 2026-08-26 — npm view astro@7.2.7 engines, npm view eslint@10.9.1 engines]`. **Pouya's machine reports Node v25.6.0** `[verified 2026-08-26 — node -v]`, an odd-numbered non-LTS line; it satisfies the floor, but `nvm use` keeps local and CI on the same runtime |
| Styling | Plain CSS with custom properties. No Tailwind, no CSS-in-JS `[verified 2026-08-25]` |
| Client JS | **Zero. Not "minimal" — none.** `dist/type-scale/index.html` contains **0 `<script>` tags** and `dist/_astro/` contains no JS bundle `[verified 2026-08-26built, counted, and confirmed in headless Chrome]`. The reveal was an inline `IntersectionObserver` in `<head>` until 2026-08-26; it was removed because `docs/05-backend-spec.md` specifies `script-src 'self'` with no `unsafe-inline`, so the only script on the site was the one thing the site's own CSP would refuse to run. `animation-timeline: view()` in `global.css` replaces it. **Both scroll-driven animations must use `animation-*` LONGHANDS** — Lightning CSS folds the `animation` shorthand together with `animation-timeline` into an invalid declaration and discards it, which works in `npm run dev` and is dead in `npm run build`. It happened twice in one session: once in the header condense, then again in the fix written for it. `/build` Phase 5 now greps `dist` for it. Islands remain available where a feature genuinely cannot be CSS or progressive HTML — none has been needed |
| Known HTML-conformance error | **Astro's `<Picture>` + `densities` emits a duplicate-density `srcset`, and it is upstream of this repo.** The pattern is `url, url 1x, url 2x, url 3x` — the first candidate carries no descriptor and therefore defaults to `1x`, so two candidates claim the same density, which puts the document in error per the HTML spec. **Behaviour is benign** and selection is correct at DPR 1, 2 and 3 `[verified 2026-08-27measured in headless Chrome]`. It affects **4 of the 12 `srcset` attributes on `/`** — the `<source>` elements of every `<Picture>` using `densities`, i.e. the three `InfinityMark` instances. The hero portrait uses `widths` + `sizes` and emits clean `w` descriptors. **Consequence for the record: do not claim "HTML validator clean" for any page carrying a mark**, and do not re-open this as a defect in this repo — it is `<Picture>`'s output, not ours. It would go if `InfinityMark` moved to `widths` + `sizes`, which would mean every call site supplying a `sizes` string for a fixed-size decorative image; not worth it for a conformance nit. Recorded on `adversarial-reviewer`'s request, 2026-08-27; first raised as step-1 review finding #8 |
| Deploy ordering | **`/` must not be deployed before `/about/` exists**, and the reason is machine-readable rather than visible. The home page's `Person` JSON-LD sets `url` and `@id` to `https://adr.smlcompany.ca/about/`, which is a **404 until build step 3**. The `@id` resolves inside the page's own `@graph`, so `provider` is fine; `Person.url` is the exposure, and it is the one part of the page a crawler acts on rather than renders. Every internal link is in the same position, which is inherent to a nineteen-page build order — **but D11 already governs this: "Build everything, one clean cutover", and nothing deploys until step 11.** Recorded because `npm run deploy` is a single command from this machine and the guard is a decision rather than a mechanism. Raised by `adversarial-reviewer`, 2026-08-27 |
| Client JS | **Zero. Not "minimal" — none.** `dist/index.html` contains **1** `<script>` tag and it is `type="application/ld+json"`; non-JSON-LD scripts **0**, and `dist/_astro/` contains no JS bundle `[verified 2026-08-27 — built, counted, and re-confirmed with script execution disabled at the CDP level: 444 DOM nodes, 6,578 characters of visible text, 5,665 px of content, i.e. the identical page]`. *(This row cited `dist/type-scale/index.html` until 2026-08-27; that page was the step-1 proof sheet and step 2 deleted it, so the row named a file that no longer exists. `/` is now the page to count.)* The reveal was an inline `IntersectionObserver` in `<head>` until 2026-08-26; it was removed because `docs/05-backend-spec.md` specifies `script-src 'self'` with no `unsafe-inline`, so the only script on the site was the one thing the site's own CSP would refuse to run. `animation-timeline: view()` in `global.css` replaces it. **Both scroll-driven animations must use `animation-*` LONGHANDS** — Lightning CSS folds the `animation` shorthand together with `animation-timeline` into an invalid declaration and discards it, which works in `npm run dev` and is dead in `npm run build`. It happened twice in one session: once in the header condense, then again in the fix written for it. `/build` Phase 5 now greps `dist` for it. Islands remain available where a feature genuinely cannot be CSS or progressive HTML — none has been needed |
| Content | Astro **Content Layer API** collections, MDX for Insights. Config lives at **`src/content.config.ts`**; collections declare a `loader`, not a `type`; `z` imports from `astro/zod`. **Astro 5 introduced** the Content Layer API and this location; **Astro 6 removed** the legacy `src/content/config.ts` fallback — moving the file back now raises `LegacyContentConfigError` `[verified 2026-08-26 — reproduced by moving it back]` |
| Fonts | Instrument Serif + Geist + Geist Mono, **self-hosted**, `font-display: swap`. **Files present as of 2026-08-26** — six `.woff2` cuts, **123,804 bytes** total, in `public/fonts/` (`du -sh` says 136K; that is disk blocks, not transfer, and this row is read in a performance context); provenance, licences (all SIL OFL 1.1) and the reason they are committed rather than pulled at build are in `docs/reference/fonts-provenance.md`. Before this they were *declared* in `global.css` and **absent from disk**, so every face silently fell back to Georgia and the system sans `[verified 2026-08-26 — built and listed]`. Instrument Serif 400 latin (21,032 B) and Geist latin (29,400 B) are preloaded in `BaseLayout.astro` — about 50 kB at first paint; nothing else is. Every font URL carries **`?v=1`**, because the deploy script serves `/fonts/*` `immutable` for a year: replacing a cut means bumping that query in `global.css` and in the preload together `[verified 2026-08-26 — byte counts]` |
| AWS account | `327082975128` `[verified 2026-08-26 — inventory]` |
@@ -473,7 +536,7 @@ Nothing below can be invented. Each needs an answer from Pouya.
| Q5 | Booking tool — **parked 2026-08-26 at Pouya's request.** Build `/contact/` with the form only and a clean slot for the embed. Now tracked as standing reminder R6 | `/contact/` — non-blocking |
| ~~Q6~~ | **ANSWERED 2026-08-26.** Supplied and committed: `src/assets/pouya-lajevardi.jpg` (1600×1600 master) and `src/assets/og-portrait.jpg` (1200×630 link-preview crop) | — |
| ~~Q7~~ | **ANSWERED 2026-08-26.** Pouya reverted to generic. The boutique is **never named**; refer to it as *a Toronto litigation and ADR boutique* throughout | — |
| ~~Q8~~ | **ANSWERED 2026-08-26.** ADRIC, ADRIO, OBA sections. Not OCNI. Not LSO (see §4) | — |
| ~~Q8~~ | **ANSWERED 2026-08-26.** ADRIC, ADRIO, OBA sections, **and the Canadian Tax Foundation** (CTF added 2026-08-26; this row omitted it until `claims-auditor` found the omission on 2026-08-27 — a second, stale copy of the memberships fact in a place nobody re-reads, which is the failure mode §7's single-source rule exists to stop. §4 is the source; this row is a pointer). Not OCNI. Not LSO (see §4) | — |
| ~~Q9~~ | **ANSWERED 2026-08-26.** Gitea, repo `adr-sml`, clone at `/Users/pouya/Dev/Websites/adr-sml` | — |
| ~~Q10~~ | **ANSWERED 2026-08-26.** Full inventory captured; values in §7 | — |
| ~~Q18~~ | **ANSWERED 2026-08-26.** `smlcompany.ca` is verified for sending — `true`. The `None` in the earlier listing was a query artefact, not a fault | — |
@@ -487,16 +550,23 @@ Nothing below can be invented. Each needs an answer from Pouya.
| ~~Q29~~ | **ANSWERED 2026-08-26 by implementation.** Guard widened on both counts: the two secrets are checked (emptiness only, never echoed), and `INTAKE_ENDPOINT` is promoted to job-level `env:` and guarded. Every `vars.*` and `secrets.*` the workflow consumes is now covered except `BOOKING_URL`, empty by decision (R6) | — |
| ~~Q30~~ | **ANSWERED 2026-08-26 — federally, under the CBCA**, and it does **not** appear in the footer. Recorded in §4 with the caution that it implies nothing about licensure or about where the practice may operate. The footer reads `© <year> SML Company Ltd` and stops there (Pouya's direction, given twice). `src/data/site.ts` un-nulled, `TODO(pouya)` removed. Note the spelling: §4 writes *SML Company Ltd.* with a terminal period; the rendered string has none, because that is how Pouya specified it | — |
| ~~Q31~~ | **ANSWERED 2026-08-26 — Plausible**, and decided rather than defaulted. Pouya checked and found his own assumption wrong: Fathom is Canadian-owned but stores non-EU traffic on US servers, isolating in the EU only for EU visitors; Plausible holds everything in the EU. D15 amended with that reasoning. The value that was guessed and nulled is now the value that was chosen — which is not the same thing, and the difference is why it was nulled | — |
| **Q35** | **NARROWED 2026-08-26 — two of the four items are resolved; two are not.** `Arbitration` is answered (Q33) and is now a **§4 Offerings** row. What remains: **(a) Med-Arb** — ungated by the same reasoning, so it is purely a competence call, and Pouya named mediation and the three arbitration forms without naming it. Do not infer it. **(b) `Energy, Grid & Regulatory` and `Shareholder & Family Business`** — subject-matter areas rather than gated activities, so the offering test asks only whether he is competent to mediate or arbitrate in them, which is his answer to give. `docs/03` already fixes how the pages must read once answered — *"Frame as positioning, not as history"* — so this is not a blocker on writing them, only on claiming capability in a one-word nav label before the page exists. Original framing follows | Med-Arb (step 4); the practice pages (step 5) |
| ~~Q35~~ | **ANSWERED 2026-08-27, all four items, and the answer added a gate that was missing.** **(a) Med-Arb** — *"he holds Q.Med and now accepts arbitration appointments, so both halves are live. Keep it in the nav. Removing it unilaterally orphaned the page — restoring it was right."* It now has its own row in **§4 Offered now**, and stays in the footer sitemap (`docs/01` keeps it out of the *primary* nav deliberately; that is unchanged). **(b) `Energy, Grid & Regulatory`, `Shareholder & Family Business` and the other four** — *"subject-matter labels, already governed by `docs/03`'s 'positioning, not history'. They pass test 1; test 2 is the page's job."* **(c) THE GATE.** Pouya: *"Add the publication gate you identified was missing: a practice area may be named where he can competently accept an appointment in that subject matter, and where the page frames it as positioning without claiming history. That hole is why Med-Arb sat in the footer under a paragraph telling readers not to infer it."* Written into **§4 → Subject-matter areas — the publication gate.** Nothing in the nav or footer is now unrowed or ungated. Narrowed framing follows | — |
| ~~Q35-narrowed~~ | **NARROWED 2026-08-26 — two of the four items are resolved; two are not.** `Arbitration` is answered (Q33) and is now a **§4 Offerings** row. What remains: **(a) Med-Arb** — ungated by the same reasoning, so it is purely a competence call, and Pouya named mediation and the three arbitration forms without naming it. Do not infer it. **(b) `Energy, Grid & Regulatory` and `Shareholder & Family Business`** — subject-matter areas rather than gated activities, so the offering test asks only whether he is competent to mediate or arbitrate in them, which is his answer to give. `docs/03` already fixes how the pages must read once answered — *"Frame as positioning, not as history"* — so this is not a blocker on writing them, only on claiming capability in a one-word nav label before the page exists. Original framing follows | Med-Arb (step 4); the practice pages (step 5) |
| ~~Q35-orig~~ | **May the header nav and footer sitemap name a service or practice area the register does not establish as a present capability, before the page that qualifies it exists?** This is the architecture-versus-register question that `claims-auditor` has now raised twice, and it is not one an implementer should settle. The nav and footer ship on all nineteen pages and today carry **`Arbitration`**, **`Med-Arb`**, **`Energy, Grid & Regulatory`** and **`Shareholder & Family Business`** — none of which trace to a §4 row. Every one of them is also **locked architecture**: D5 and §6 fix the sitemap, and `docs/01` specifies each page. The specs already anticipate the tension and answer it at page level — `docs/03`: *"Frame as positioning, not as history"*; `docs/01` on energy: *"Write it as a genuine position, not a claim of existing volume"* — so the question is precisely whether a **one-word nav label**, which has no room to frame anything, inherits that licence. Three ways out: accept the labels as taxonomy and rely on the pages to qualify (status quo), rename toward capability-neutral wording, or hold items out of the nav until their page ships. **Claude Code declined to act unilaterally on either audit pass**; recorded so the judgement is visible rather than missed | The header and footer on every page; sharpest at step 5 |
| ~~Q36~~ | **ANSWERED 2026-08-26 — yes, and so are sole-arbitrator and party-appointed appointments.** Now a row in **§4 Offerings** rather than an unsourced assertion in three specs. `docs/01`, `docs/03` and `docs/07-fees.md` no longer need to cite an open question against it. Original text follows | — |
| ~~Q36-orig~~ | **Is co-arbitration work available now?** Asserted in three specs — `docs/01` §`/arbitration/`, `docs/03` §Voice as model copy, and `docs/07-fees.md`, which instructs *"The page must say so"* — and **it is in no §4 row.** §4 records the Q.Arb pathway as commenced August 2026 and nothing about what is available in the meantime. The sentence is offered under the heading *"Name the limits"*, so the unverified half travels inside advice about honesty, which is how it survived. All three files now cite this question; none has been deleted, because the *shape* of the sentence is right and only its second clause is unsourced. Raised by `claims-auditor`, 2026-08-26 | `/arbitration/` (step 4) and `/fees/` (step 9) |
| **Q37** | **Is "Law and engineering" the right label under `JD + ML`?** §4's substitution principle supplies the three credential-row **values** (`Q.Med` / `JD + ML` / `EN · FA`); the labels come from `docs/03` and are not themselves registered. `claims-auditor` reads this one as a D13 implication risk: "engineering" is shorthand for a **verified practising** claim, so the parallel invites "Law" to be read the same way, and D13 bars implication as hard as assertion. Left as written — it is approved copy and changing approved copy is not an implementer's call — with a `TODO(pouya)` on it in `src/data/site.ts`. Nothing renders it yet. Raised by `claims-auditor`, 2026-08-26 | The credential row on `/` (step 2) and `/about/` (step 3) |
| ~~Q33~~ | **ANSWERED 2026-08-26 — the question rested on a false premise.** It assumed the constraint on describing arbitration was about *entitlement*. ADR designations are voluntary credentials, not licences, and **anyone may be appointed an arbitrator in Ontario** — so the constraint was always positional, never legal, and it was Pouya's to answer. He accepts arbitration appointments now. `Mediation · Arbitration · Toronto` is **restored to the masthead one-liner**, and `docs/04`'s specified home `<title>` stands as written. See **§4 Offerings**, added for exactly this class of statement. The original question text follows for the record | — |
| ~~Q37~~ | **ANSWERED 2026-08-27 — the label changes.** `JD + ML` is now labelled **"Legal training and engineering practice"**, not "Law and engineering". Pouya's reasoning, and it is the finding rather than the fix: *"The parallel was doing the implying — a degree and a practice under one noun. The asymmetry is the honest part."* A degree is not a practice, and a label that renders them as two instances of one thing invites the reader to supply the missing symmetry — which for "Law" is precisely the licensure implication D13 bars. The new label is longer and less tidy, deliberately. `docs/03`'s credential-row table is amended, and the `TODO(pouya)` is gone from `src/data/site.ts`. Original question follows | — |
| ~~Q37-orig~~ | **Is "Law and engineering" the right label under `JD + ML`?** §4's substitution principle supplies the three credential-row **values** (`Q.Med` / `JD + ML` / `EN · FA`); the labels come from `docs/03` and are not themselves registered. `claims-auditor` reads this one as a D13 implication risk: "engineering" is shorthand for a **verified practising** claim, so the parallel invites "Law" to be read the same way, and D13 bars implication as hard as assertion. Left as written — it is approved copy and changing approved copy is not an implementer's call — with a `TODO(pouya)` on it in `src/data/site.ts`. Nothing renders it yet. Raised by `claims-auditor`, 2026-08-26 | The credential row on `/` (step 2) and `/about/` (step 3) |
| ~~Q33~~ | **ANSWERED 2026-08-26 — the question rested on a false premise.** It assumed the constraint on describing arbitration was about *entitlement*. ADR designations are voluntary credentials, not licences, and **commercial arbitral appointment in Ontario is not gated behind a designation** — so the constraint was always positional, never legal, and it was Pouya's to answer. *(This row said "anyone may be appointed an arbitrator in Ontario" until 2026-08-27. **Pouya checked it and it is false as a universal** — family arbitration is gated; see Q39. Corrected in place, because §9 is Current Truth. The Change Log entries that quote the original stand unedited, which is where the history lives.)* He accepts arbitration appointments now. `Mediation · Arbitration · Toronto` is **restored to the masthead one-liner**, and `docs/04`'s specified home `<title>` stands as written. See **§4 Offerings**, added for exactly this class of statement. The original question text follows for the record | — |
| ~~Q33-orig~~ | **How is arbitration described in site-wide copy before Q.Arb is conferred?** Not a wording question — a positioning one, and it recurs on every page. §4 records the Q.Arb pathway as **commenced August 2026**, "never as held or nearing completion". `SITE.tagline` reads `Mediation · Arbitration · Toronto`, and `docs/01` specifies that exact string as the **Home hero eyebrow** — where the hero copy beneath it can qualify it. Build step 1 promoted it to a masthead line under Pouya's name on every page, where nothing qualifies it and it reads as a designation strip; `claims-auditor` flagged it and **it has been removed from the header**. But the question it exposed is still open and lands at build step 2: does the hero eyebrow keep `Arbitration` as specified, qualify it, or hold it until designation? `/arbitration/` (step 4) is separately specced to state the position plainly, which is the honest model — the issue is the one-line forms that have no room to. **Second pass, same day:** the removal from the header did not sweep the proof sheet, where the same string was **hand-typed** as a type specimen and rendered ten times at up to 96px. Both defects fixed — the specimen is now a neutral pangram — and the lesson is the sweep rule again: a removal is a `grep`, not a memory. The nav item `Arbitration` also survives, deliberately; that is Q35. Raised by `claims-auditor`, 2026-08-26 | The Home hero (step 2); `docs/04`'s specified home `<title>`; any bio or one-line descriptor |
| **Q34** | **Do §2's measurements of the previous build still hold, and were they measured against the deployed file?** Two of `claims-auditor`'s findings were unverified old-site assertions published in copy, which sent Claude Code to measure the live URL on 2026-08-26. What came back does not straightforwardly match §2. **Reproduced:** the response is a single **2,206,032-byte** file, and `<title>` is still `SML Company · Dispute Resolution`. **Could not be reproduced from the served HTML:** there are **zero** `base64,` occurrences anywhere in it — §2 records "two logo PNGs ~1 MB, base64-inlined" — and no reference to `unpkg` or a React development build. **Why this is a question and not a correction:** the served document is a *bundler harness*. 2,210,921 bytes of it are inline `<script>`, the real `<head>` sits JSON-escaped inside a `<script type="__bundler/template">`, and the application is loaded from **nine UUID-named script files that were not fetched**. The logos and the CDN references may well be in those bundles. So §2 may be right, may be stale, or may have been measured against the local source rather than the deployment — this record cannot tell which. **Why it matters beyond tidiness:** `docs/02` and `CLAUDE.md` both cite the base64-logo figure as the *reason* for a standing convention, and §12 R8 exists because unverified premises propagate. Nobody should quote these numbers in public copy until the question closes. Raised by Claude Code, 2026-08-26 | Nothing structural. It governs whether §2's figures may be cited as fact |
| **Q39** | **Does the arbitration offering extend to FAMILY arbitration, and is that activity gated?** Raised by `claims-auditor` 2026-08-26 as a counter-example from inside this repo, and it is a good one. §4 Offerings rests on the proposition that arbitral appointment in Ontario is not gated behind a designation. The site ships a **`Shareholder & Family Business`** practice area — and **family** arbitration in Ontario is understood to be one of the places where arbitrator qualification *is* prescribed by statute and regulation. If so, the offering test's own boundary applies: where an activity is gated, the question is **permission**, and permission is not established. **This record does not resolve it** — Claude Code will not assert a proposition of Ontario law, and neither may a self-report. What is needed: (a) does the practice-area page contemplate family-law arbitration at all, or only shareholder, partnership and family-*business* disputes, which is a different thing; and (b) if it does, the gating question needs a checkable source in the repo, per R14. **Interim:** §4's arbitration rows are scoped to **commercial** arbitration and the universal has been removed from the reasoning. Nothing ships that turns on it | The `Shareholder & Family Business` page (step 5); `/arbitration/` (step 4) |
| **Q38** | **A true vector master for the infinity mark.** The mark is a shaded ribbon — variable-width band, maroon flowing into champagne, twisting in three dimensions and passing over itself at the crossing. That is gradient-mesh artwork, and there is no honest way to express it as the flat vector paths `docs/02` assumes. An SVG **is** held — `src/assets/brand/sml-logo-source.svg` — and **it renders faithfully**: rasterised at 8333 px it reproduces the master exactly, at the same 1.566:1 `[verified 2026-08-26 — rendered and measured]`. *An earlier draft of this row called it "a raster in a vector wrapper", which was unfair and is corrected.* What rules it out is **payload and composition**: 257,278 bytes against **3,063** for the AVIF a Retina browser actually takes — 84× — plus **seven embedded base64 PNGs** and a 1,225-stop gradient mesh, so inlining it would breach `CLAUDE.md`'s rule against base64-inlining images. What is wanted is a master that is faithful **and** light. **Pouya is commissioning a proper vector master.** Until it lands, `InfinityMark.astro` renders an optimised raster and that is a **documented, temporary exception to `docs/02`'s inline-SVG rule** (R13 keeps it from becoming permanent). When the master arrives: replace the component's `<Picture>` with inline SVG, regenerate the favicons from it, and delete the exception from `docs/02` and this row. Raised by Claude Code 2026-08-26; scoped by Pouya the same day | Nothing — the mark renders correctly. This is fidelity and payload, not function |
| ~~Q39~~ | **ANSWERED 2026-08-27 — and the answer is that the register's premise was FALSE.** Pouya checked it rather than defending it. His words: *"My assertion was FALSE as a universal."* **(a) Is it gated? YES.** Family arbitrators in Ontario must complete prescribed training — 14 hours on screening for domestic violence and power imbalances, 30 hours of Ontario family law for arbitrators outside the Ontario or another Canadian bar, and 10 hours ongoing per two-year period. **Sourced, and the source is now in the repo** per R14: `docs/reference/ontario-family-arbitration-training.md`, retrieved 2026-08-27 with provenance. **(b) Does the offering extend to it? NO** — and that is what actually disposes of the question. Pouya does not do family arbitration, so it is a **scope exclusion, not a gating problem**, and the prescribed-training requirement never bears on this practice. `Shareholder & Family Business` means **commercial disputes among family shareholders** — shareholder and partnership disputes, co-founder breakdowns, business succession — and its page must state plainly that family law matters are not accepted. *One sentence, not a section: Pouya's instruction is that it earns its place only by saving a wasted intake call.* **(c) One honest limit on the source.** The extract establishes the family gate directly and the commercial half only by absence — it neither mentions nor excludes commercial arbitration. §4 therefore carries *"commercial arbitration in Ontario requires no licence and no designation"* as **Pouya's stated position, unstamped**, not as a sourced fact. Do not let the footnote promote it. **Also swept:** the false universal survived in three live places outside the paragraph that was corrected — §4's NOT-NEGOTIABLE boundary bullet, §9's Q33 closure row, and a comment in `SiteHeader.astro`. All three fixed; the `grep` and its output are pasted in **Change Log entry (w), §Sweeps**. *(`adversarial-reviewer` flagged this citation while it still pointed at nothing: the entry was written at Phase 6, after review, so for the duration of the review a §9 row asserted in the present tense that evidence "is in the Change Log" before it was. Fair catch. The row now names the entry, and the reviewer independently re-ran the sweep and confirmed it was genuinely complete.)* Original question follows | — |
| ~~Q39-orig~~ | **Does the arbitration offering extend to FAMILY arbitration, and is that activity gated?** Raised by `claims-auditor` 2026-08-26 as a counter-example from inside this repo, and it is a good one. §4 Offerings rests on the proposition that arbitral appointment in Ontario is not gated behind a designation. The site ships a **`Shareholder & Family Business`** practice area — and **family** arbitration in Ontario is understood to be one of the places where arbitrator qualification *is* prescribed by statute and regulation. If so, the offering test's own boundary applies: where an activity is gated, the question is **permission**, and permission is not established. **This record does not resolve it** — Claude Code will not assert a proposition of Ontario law, and neither may a self-report. What is needed: (a) does the practice-area page contemplate family-law arbitration at all, or only shareholder, partnership and family-*business* disputes, which is a different thing; and (b) if it does, the gating question needs a checkable source in the repo, per R14. **Interim:** §4's arbitration rows are scoped to **commercial** arbitration and the universal has been removed from the reasoning. Nothing ships that turns on it | The `Shareholder & Family Business` page (step 5); `/arbitration/` (step 4) |
| **Q40** | **All nineteen pages will share ONE Open Graph image, and it is a portrait crop.** `docs/04-seo-spec.md` specifies OG images *"generated at build with `satori` or `astro-og-canvas`... One template: display headline on cream, infinity mark, designation line. Never a screenshot."* What ships is `src/assets/og-portrait.jpg`, the 1200×630 crop Pouya supplied under Q6 — not a screenshot, so the prohibition is not breached, but not the generated template either. The consequence is specific: `docs/04` requires a **unique** title and description per page and every link preview will nonetheless look identical, which is the surface a general counsel actually sees when a colleague pastes the URL into Teams or Slack. **Two decisions, neither an implementer's:** whether to build the generator at all (it adds a dependency and an `npm audit` surface, against R11), and what the card looks like (brand). **Interim:** the portrait crop ships on `/` and is fine on its own terms. Raised by Claude Code, 2026-08-27 | Nothing today. Sharpest once several pages exist — steps 35 |
| **Q41** | **Three copy questions the step-2 claims audit escalated rather than let an implementer settle.** All three were changed to a defensible interim form so `/` could ship; each interim is marked in the source with this question number. **(a) Does Q37's reasoning extend to PROSE, or is it scoped to the credential label?** The approach section opened *"Law and engineering are not blended here"* — the exact parallel Pouya struck one day earlier, relocated from the label into body copy and strengthened by attributing both halves to him personally. `docs/01` §`/` item 3 and `docs/03` §Home both specify the section as *"law and engineering converging on the same dispute"*, so the framing is his; only the construction carrying the implication was changed (to *"what the documents say, and what the engineering says"*). Extend Q37, or scope it to the label and say so. **(b) May the comparative be restored?** *"facts most neutrals take on faith"* is `docs/03`'s core positioning statement and is therefore approved copy — but it is an assertion about a population of third parties, `docs/03`'s own checklist requires *"any comparative claim is factual and verifiable"*, and §4 has no row for it. Interim: *"the documents rather than the pleadings"*, which says the same thing about the work without saying anything about other neutrals. **(c) What does `LAT pre-hearing mediation` mean as an offering?** `docs/01` §`/practice/insurance/` lists that phrase as a **search intent**; 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. Interim: *"private mediation of matters before the LAT"*. Confirm the intent before the page ships. Raised by `claims-auditor`, 2026-08-27 | (a) and (b): `/` is live with the interim wording — his ratification or reversal. (c): `/practice/insurance/` at step 5 |
| **Q42** | **The four "also offered" processes have no §4 Offerings row, and `/practice/` is specified to offer them.** `docs/01` §`/practice/` calls that page *"the natural home for the 'also offered' strip: early neutral evaluation, settlement counsel, dispute-system design, and pre-dispute technical advisory"*, and `FEES.hourly` in `src/data/site.ts` prices them at $500/h. **None has a row**, and §4 is explicit: *"An offering may not be published until it has a row in 'Offered now'... or it is a category that authorises everything by saying nothing."* Found on `/` first: the practice-areas link read *"All six areas, and what else is offered"*, which asserted offerings beyond the six — the Med-Arb-in-the-footer defect in a new place. **Reworded to "All six practice areas" and nothing is claimed today.** Each of the four is a **process**, so each needs its own row and its own competence answer; they are ungated the same way mediation and arbitration are, so this is a competence call and his alone. Raised by `claims-auditor`, 2026-08-27 | The `/practice/` index and its "also offered" strip (step 5); `/fees/` prices them (step 9) |
| **Q43** | **The five process timings are published commitments with no §4 row.** `Day 0 · Days 17 · Days 721 · Days 2130 · Day 30 onward` come from `docs/01` §`/process/` and are now on a public page. They are **not** a forbidden statistic — no matter count, no settlement rate, no median time to award — but they are checkable, specific, and about how the practice runs, which is precisely the class §4 exists to gate. `docs/03` §Process requires *"five steps with real timing"* and warns against evasions, so softening them to "typical" is barred and inventing them was never on. **Interim:** the numbers are published exactly as `docs/01` specifies, and the editorial claim *"with real timing"* — which asserted a quality about them rather than stating them — is gone from `/`. What is wanted is either an Offerings-style row confirming them for publication, or his explicit sign-off. Raised by `claims-auditor`, 2026-08-27 | `/process/` (step 6) states them in full; `/` states them now |
| **Q38** | **A true vector master for the infinity mark.** The mark is a shaded ribbon — variable-width band, maroon flowing into champagne, twisting in three dimensions and passing over itself at the crossing. That is gradient-mesh artwork, and there is no honest way to express it as the flat vector paths `docs/02` assumes. An SVG **is** held — `src/assets/brand/sml-logo-source.svg` — and **it renders faithfully**: rasterised at 8333 px it reproduces the master exactly, at the same 1.566:1 `[verified 2026-08-26 — rendered and measured]`. *The characterisation has now moved twice and Pouya has settled it.* This row first called the file **"a raster in a vector wrapper"**; a later draft withdrew that as unfair. **The withdrawal went too far, and Pouya's ruling of 2026-08-27 restores the substance:** *"It renders faithfully because it IS the raster. Your first characterisation was accurate and the walk-back went too far."* Both things are true at once and the row must hold both — the file is a faithful rendering **and** it is faithful because seven base64 PNGs are carried inside it, which is why fidelity was never the question. **The Canva SVG does not close this question.** Pouya: *"Keep it committed, keep the AVIF render path. R13 stays open for a true vector master."* What rules it out is **payload and composition**: 257,278 bytes against **3,063 B** for the AVIF a Retina browser takes in the header — **84×** — plus **seven embedded base64 PNGs** and a 1,225-stop gradient mesh, so inlining it would breach `CLAUDE.md`'s rule against base64-inlining images. *Restated 2026-08-27, because the single number stopped covering the case:* build step 2 added a **232 px** call site (the home page's approach section, rendering at 225.5 px) beside the existing 64 px one, and at that size a DPR-2 device takes **14,555 B** and DPR-3 **22,639 B** — a ratio of ~11×, not 84×. `adversarial-reviewer` caught the three copies of the old figure going stale together, which is the SES-DKIM duplication in a new place. Both ladders are in `docs/reference/brand-assets.md`; **quote the size with the call site, never on its own.** Also worth knowing before someone reads it as a regression: the PNG fallback at 232 px runs **38,094 / 91,618 / 150,215 B**. Nothing that negotiates content types takes it — a DPR-3 device with neither AVIF nor WebP does not exist in practice — and the AVIF above is what real clients fetch. Accepted deliberately rather than capped, because capping it would blunt the mark on the high-DPI desktops that are the only place the 696 px variant is used at all. What is wanted is a master that is faithful **and** light. **Pouya is commissioning a proper vector master.** Until it lands, `InfinityMark.astro` renders an optimised raster and that is a **documented, temporary exception to `docs/02`'s inline-SVG rule** (R13 keeps it from becoming permanent). When the master arrives: replace the component's `<Picture>` with inline SVG, regenerate the favicons from it, and delete the exception from `docs/02` and this row. Raised by Claude Code 2026-08-26; scoped by Pouya the same day | Nothing — the mark renders correctly. This is fidelity and payload, not function |
| ~~Q32~~ | **ANSWERED 2026-08-26 — and the answer was that the reconstruction was WRONG.** Pouya rendered the committed path against the master (`SML Company Just Logo Transparent.png`, 3000×3000) and rejected it on three counts. Two reproduce from the path alone: **(a) TANGENT, NOT CROSSING** — all four cubic branches meet the origin at exactly 90°, so the loops are mutually tangent on a vertical line and at stroke-width 28 render as *two kissing circles*, the one thing an infinity mark must not be `[verified 2026-08-26 — tangent vectors computed per segment, independently reproduced]`. Both lobes are strictly confined to their own half-plane (x is monotone on every segment), so the signed crossing number is **0** — they cannot cross anywhere, not merely at the node. Worse at the size actually shipped: the two strokes stay fused into one mass wherever the centreline separation `y²/192` is under 28, i.e. across **61% of the mark's height** — at 2rem that is a 16.8px blob out of a 27.4px-tall mark. No stroke-width, viewBox or scale change can produce a crossing from this centreline; **(b) WRONG PROPORTION** — the master's ink bounding box is **2668 × 1704 = 1.5657:1** `[verified 2026-08-26 — measured]`, and **(c)** the master is a shaded ribbon where the trace was two flat uniform strokes. ***1.23:1 — RECONCILED, and it was a real measurement, not a slip.*** It is the bounding box of the path's **coordinates** rather than of the **curve**. The control points sit at y = ±160 while the curve only reaches ±120 — the standard 3/4 pull-in of a cubic — so a coordinate-hull box is 400 × 320 = 1.2500, and with stroke-width 28 applied on all four sides it is **428 × 348 = 1.2299**. Pouya's figure to four significant figures, from one method applied consistently `[verified 2026-08-26 — derived]`. **It is a trap rather than a slip:** because x is monotone on every segment, the control points give the *correct* width (±200) and a 33% inflated height, so the obvious sanity check — "does the width look right?" — passes. Any tool that reads a bbox from parsed coordinates lands here; `getBBox()` would have returned 400 × 240. **And the direction is inverted from how it read:** measured from the curve, the traced path is 6.4% *wider and flatter* than the master, not squatter. **Consequence worth keeping:** the declared viewBox 440:280 = 1.5714 is within **0.35%** of the master's 1.566, so re-tuning the layout to the true ratio was ~0.18px of work at the shipped 2rem — and **re-tuning to 1.23 would have actively broken it** — at any given width 1.2299 allocates 1.5657/1.2299 = **27% more height** than the mark occupies, so the header would have been rebuilt around a box a quarter too tall. The ratio was never sufficient grounds on its own; **(a) tangency is, and it is exact.** **The traced path is deleted, not kept as a fallback**, on Pouya's instruction: *a wrong mark that renders is worse than a missing one, because it stops looking wrong.* Now closed by Q38 for the vector master | — |
| ~~Q32-orig~~ | *(superseded — the original wording of this question is preserved in entry (v))* | `src/components/InfinityMark.astro` is built from geometry lifted verbatim from the deployed site's own loading-thumbnail SVG — the element it labels `smlMark`, fetched from `https://adr.smlcompany.ca/` on 2026-08-26. Path, both stroke widths (28 / 6) and the 0.7 inner opacity are the source's; the only change is folding a `translate(60 0)` into the coordinates. So it is SML's own artwork rather than a redrawing — but a loading placeholder is not necessarily the canonical file, and D7 says the mark carries over *unchanged*. If a master SVG or AI/EPS exists, supply it and the component gets replaced. Raised by Claude Code, 2026-08-26 | Nothing — the mark renders. This is about fidelity, not a blocker |
| **Q23** | **Is a Gitea `act_runner` registered, and are Actions enabled in `app.ini`?** *The version half is closed:* the instance reports **1.27.2** `[verified 2026-08-26 — /api/v1/version]`, so the `vars` context is supported and the first-step guard is belt-and-braces rather than load-bearing. **What remains is not a fact to look up but a dependency on a person:** git.larsnolden.com is **jointly administered**, and both enabling Actions and registering a runner need the second administrator. Until then `npm run deploy` is the path, and it is documented rather than treated as a workaround | How deploys are triggered — not what they do |
@@ -548,7 +618,7 @@ never being raised again.
| # | Reminder | Raised | Why it must keep coming back |
|---|---|---|---|
| R1 | **Licensure.** No site exists yet — `src/pages/` does not exist. **As specified**, the copy will assert the JD only and describe the boutique role as *active litigation exposure*, never *practice*. Pouya flagged that as an interim position and asked to be reminded to change it. §4 now records licence status as `[unestablished]`, which is the honest state, not a resolution | 2026-08-26 | If he is licensed and in good standing, it is the first credential appointing bodies and opposing counsel look for, and its absence from a detailed credentials page is conspicuous. If licensure is in progress, the copy should be rewritten the moment it completes. Either way this is a **temporary** framing that will otherwise ossify |
| R1 | **Licensure — RAISED 2026-08-27 and acknowledged by Pouya; still open, and the stakes are higher than when it was written.** His words: *"The interim licensure framing is now carried by shipped pages rather than specs, which raises the stakes. Keep surfacing it."* So it is surfaced, and it will be surfaced again next session. **The opening sentence below is now false and is kept only because §12 rows are not rewritten silently:** `src/pages/` exists, `/` ships at build step 2, and the framing is on a public page rather than in a document. Original text follows. **Licensure.** No site exists yet — `src/pages/` does not exist. **As specified**, the copy will assert the JD only and describe the boutique role as *active litigation exposure*, never *practice*. Pouya flagged that as an interim position and asked to be reminded to change it. §4 now records licence status as `[unestablished]`, which is the honest state, not a resolution | 2026-08-26 | If he is licensed and in good standing, it is the first credential appointing bodies and opposing counsel look for, and its absence from a detailed credentials page is conspicuous. If licensure is in progress, the copy should be rewritten the moment it completes. Either way this is a **temporary** framing that will otherwise ossify |
| R2 | **Matter counts stay off the site until they are independently credible.** Revisit once there is a number a sophisticated GC would find persuasive on its own | 2026-08-26 | §4 forbids them now. That rule has an expiry date nobody has set |
| R3 | **The month 1218 practice-area review — now two candidates, not one.** *(a)* **Indigenous engagement**, deliberately omitted at launch (§6). *(b)* **Tax-adjacent disputes**, added 2026-08-26 on the Canadian Tax Foundation membership: it is the one credential none of the six areas touch, and valuation, earn-out, indemnity and shareholder disputes that turn on a tax position are ADR territory. Reasoning for both is in `docs/01-architecture.md`. One review, two candidates | 2026-08-26 | The brief rates the Indigenous niche the most valuable single one, and omission there is a timing call rather than a permanent one. The tax case is the same shape and fails for the same reason today: a practice page is a claim of present capability, and a membership is a credential, not a caseload. Both stop being true at some point, and nothing will tell you when |
| R4 | **Insights cadence.** D9 commits to monthly. A blog that stops is worse than one that never started | 2026-08-26 | The section's whole value is compounding |
@@ -558,7 +628,7 @@ never being raised again.
| R10 | **Annually-renewing memberships.** §4 verifies the **OBA sections** (Construction & Infrastructure, ADR, Civil Litigation) and the **Canadian Tax Foundation** as of 2026-08-26 — *for now*. Both renew yearly. *Widened 2026-08-26 when CTF was added; it was OBA-only before* | 2026-08-26 | A credential that lapses quietly is the failure mode §4 exists to prevent, and OCNI already did exactly this (§4 records it as "not current, do not publish"). Re-confirm at each renewal, and before any page listing memberships ships — `/about/` at build step 3 is the first one that will |
| R11 | **Re-check dependency currency at every phase boundary in the build order** (`docs/01-architecture.md` §Build order, 11 steps). Run `npm view <pkg> version` across **every** pin in `package.json` and compare; do not wait for something to break. Verified does not mean latest — record the reason for any deliberate hold in §7. **Carries one specific re-add trigger: at step 7, put `@lhci/cli` back.** Check for a patched release — do not assume `0.15.1` is still the ceiling just because it was on 2026-08-26 — pin it against `npm view` that day, write a `lighthouserc` with the budgets from `docs/04-seo-spec.md`, and delete the UNAVAILABLE notices this repo now carries in six places (§7 lists them). If the advisories are still unfixed, that is a decision to take deliberately, not a reason to leave the gap unstated | 2026-08-26 | `astro: "^5.0.0"` was recalled rather than checked and was two majors stale the day it was written, which meant a framework carrying high-severity XSS advisories. Between phases is cheap; after a phase of pages is written is not. The build order has ten more boundaries |
| R12 | **`compressHTML: true` is a deliberate deviation from the Astro 7 default (`'jsx'`).** Measured 2026-08-26: in an `.astro` template an inline pair split across two lines renders as `<em>a</em><strong>b</strong>` under the default — the space is silently deleted. MDX prose is unaffected | 2026-08-26 | It is a deviation, and undocumented deviations become folklore. Revisit **with a measurement**, not a preference — and re-measure after any Astro major, since the behaviour could change again |
| R13 | **The infinity mark ships as a RASTER, and that is temporary.** `InfinityMark.astro` renders an optimised AVIF/WebP from `src/assets/brand/sml-infinity-mark.png` — a deliberate, documented exception to `docs/02`'s "inline SVG, never a PNG", because the mark is gradient-mesh artwork and no true vector master exists yet (Q38). **Removal trigger: the commissioned vector master lands.** Then replace the `<Picture>` with inline SVG, regenerate `favicon.ico` and `apple-touch-icon.png` from it, and delete the exception from `docs/02`, from the component, and from Q38 | 2026-08-26 | Pouya flagged this himself when he made the ruling: *an interim raster is exactly the kind of temporary measure that becomes permanent by never being raised.* It costs ~8 KB and works, which is precisely why nobody will notice it again. There is no build error to prompt anyone — only this row |
| R13 | **The infinity mark ships as a RASTER, and that is temporary. RAISED 2026-08-27; Pouya ruled the committed SVG does NOT close it***"Keep it committed, keep the AVIF render path. Your own measurement is the reason: 257 KB wrapping seven embedded base64 PNGs. It renders faithfully because it IS the raster."* So the exception stands and the reminder stays live. `InfinityMark.astro` renders an optimised AVIF/WebP from `src/assets/brand/sml-infinity-mark.png` — a deliberate, documented exception to `docs/02`'s "inline SVG, never a PNG", because the mark is gradient-mesh artwork and no true vector master exists yet (Q38). **Removal trigger: the commissioned vector master lands.** Then replace the `<Picture>` with inline SVG, regenerate `favicon.ico` and `apple-touch-icon.png` from it, and delete the exception from `docs/02`, from the component, and from Q38 | 2026-08-26 | Pouya flagged this himself when he made the ruling: *an interim raster is exactly the kind of temporary measure that becomes permanent by never being raised.* It costs ~8 KB and works, which is precisely why nobody will notice it again. There is no build error to prompt anyone — only this row |
| R14 | **Anything a spec makes a claim about must be reachable from the repository.** If the artefact lives only in Drive, in a console, or on someone's disk, no reviewer can check the claim and the claim is unverifiable by construction — see the rule now in `CLAUDE.md` | 2026-08-26 | **This has now cost twice.** Q24 was `AWS-Hosting-Guide.md`, the only record of how the infrastructure was hand-built, living outside the repo. Q32 was the infinity mark: Claude Code traced it from the old site's *loading placeholder*, wrote a scrupulous provenance comment recording exactly that doubt — and **two adversarial review passes still could not catch it**, because the artwork they would have had to compare against was not in the repo. The doubt was correctly stated and structurally uncheckable. That is the failure mode: not a missing warning, a missing artefact |
| ~~R7~~ | **RATIFIED / SUPERSEDED 2026-08-26.** (a) Cache-policy table matching the pipeline — **accepted**; documenting what the pipeline does beats documenting an intention. (b) `s3:AbortMultipartUpload` omitted — **accepted, reasoning corrected**: the lifecycle rule does not exist and is therefore not the cover; the actual cover is that `aws s3 sync` only goes multipart above 8 MB and the largest asset is a 357 KB portrait. Recorded in `docs/06-deployment.md` with a revisit trigger. (c) The `aws s3 ls` pre-flight — **superseded** by the variable guard now running as the workflow's first step | 2026-08-26 | — |
| ~~R8~~ | **PROMOTED TO A RULE 2026-08-26.** A reminder was too weak for a pattern that survived three entries. *A sweep is a command, not a claim* now sits in `CLAUDE.md` under Conventions, in `/build` Phase 6, and in `/wrap` step 3: any claim that a change was applied across files must cite the command and be written only after reading its output | 2026-08-26 | — |
@@ -567,6 +637,542 @@ never being raised again.
# Change Log
## 2026-08-27 (w) — Build step 2: `/` ships. Q35, Q37 and Q39 close, and Q39's answer was that this register had been reasoning from a falsehood
**Pouya ruled on all five parked items in one message.** Four are implemented
here; R1 is acknowledged and stays live by design. The most important of them
did not confirm this file — it corrected it.
### Q39 — ANSWERED, and my assertion was FALSE as a universal
His words: *"My assertion was FALSE as a universal; correct it, then close the
question simply."* He checked rather than defended, against
`ontario.ca/page/training-family-arbitrators`.
**Family arbitration in Ontario is gated.** Prescribed training: 14 hours on
screening for domestic violence and power imbalances, 30 hours of Ontario family
law for arbitrators outside the Ontario or another Canadian bar, and 10 hours
ongoing per two-year period with five on domestic violence or power imbalance.
**`claims-auditor` produced this counter-example from inside the repo on
2026-08-26 and it was right.**
**The source is now IN THE REPO** — `docs/reference/ontario-family-arbitration-
training.md`, retrieved 2026-08-27 with its provenance and method. That closes
R14 for the half of the proposition it covers, and §4 now says which half that
is, because the distinction matters more than the citation:
- The extract establishes the **family** gate **directly**.
- It establishes the **commercial** half only by **absence** — it neither
mentions nor excludes commercial arbitration. A government page about family
arbitrators is not authority for what a commercial arbitrator needs.
So *"commercial arbitration in Ontario requires no licence and no designation"*
is recorded as **Pouya's stated position, deliberately unstamped**, with the
citation attached to the family half only. **A footnote must not be allowed to
promote the scoped form back into a universal.**
**What actually disposes of the question is the scope exclusion, not the law.**
Pouya does not do family arbitration. §4 Offerings carries it as **NOT
OFFERED — a scope exclusion, not a gating problem**, so the prescribed-training
requirement never bears on this practice at all. `Shareholder & Family Business`
means commercial disputes among family shareholders; its page must say plainly
that family law matters are not accepted — **one sentence, not a section**, on
his instruction that it earns its place only by saving a wasted intake call.
Recorded in `docs/01` §`/practice/shareholder/` for build step 5.
### The false universal had survived in three more places, and the grep found them
Correcting the paragraph that carried it was not the fix. `grep -rn "Anyone may
be appointed"` plus a paraphrase sweep for `No designation is required`:
```
$ grep -rn "anyone may be appointed\|Anyone may be\|No designation is required" \
--include='*.md' --include='*.astro' --include='*.ts' .
AGENTS.md:238 <- my new text, quoting what was struck (correct)
AGENTS.md:313 <- §4 BOUNDARY bullet: "Mediation and arbitration are ungated" FIXED
AGENTS.md:548 <- §9 Q33 closure row, bolded as an assertion FIXED
AGENTS.md:774 <- Change Log entry (v), append-only history (correct)
src/components/SiteHeader.astro:141 <- a live comment in shipped source FIXED
```
The §4 instance is the one worth dwelling on: it was in the paragraph headed
**THE BOUNDARY, AND IT IS NOT NEGOTIABLE**, which is the worst place in this
file for an unscoped claim about a class of activity. The boundary paragraph was
breaching its own boundary.
### Q35 — ANSWERED, all four items, and the answer supplied a gate that was missing
- **Med-Arb** — *"he holds Q.Med and now accepts arbitration appointments, so
both halves are live. Keep it in the nav. Removing it unilaterally orphaned
the page — restoring it was right."* Now its own §4 Offerings row. It stays in
the footer sitemap; `docs/01` keeps it out of the *primary* nav deliberately
and that is unchanged.
- **The six subject-matter labels** — *"subject-matter labels, already governed
by `docs/03`'s 'positioning, not history'. They pass test 1; test 2 is the
page's job."*
- **THE GATE** — *"Add the publication gate you identified was missing... That
hole is why Med-Arb sat in the footer under a paragraph telling readers not to
infer it."* §4 gains **Subject-matter areas — the publication gate**: a
practice area may be named where (1) he can competently accept an appointment
in that subject matter, and (2) the page frames it as positioning without
claiming history. Condition 1 is about the label; condition 2 is about the
page. **Nothing in the nav or footer is unrowed or ungated any more.**
### Q37 — ANSWERED. The label changes, and the reasoning is the finding
`JD + ML` is labelled **"Legal training and engineering practice"**, not "Law and
engineering". Pouya: *"The parallel was doing the implying — a degree and a
practice under one noun. The asymmetry is the honest part."* A JD is a degree;
engineering is a verified practice. Rendering them as two instances of one thing
invited the reader to supply the symmetry, and for "Law" the missing half is a
licence. It is longer and deliberately lopsided; `CredentialRow` carries a
comment telling the next reader not to tidy it back into a parallel.
### Q38 / R13 — the committed SVG does not close it, and my walk-back went too far
Pouya: *"Keep it committed, keep the AVIF render path. Your own measurement is
the reason: 257 KB wrapping seven embedded base64 PNGs. It renders faithfully
because it IS the raster. Your first characterisation was accurate and the
walk-back went too far."* Q38 and `docs/02` now hold both halves at once — the
fidelity is real, and it is bought with embedded raster, which is exactly why
fidelity was never the question. R13 stays open.
### Q32 — Pouya ratifies the reconciliation, and identifies that his own step 3 would have broken the layout
*"My 1.23:1 was measured from the path's coordinate hull rather than the curve,
and step 3 of my Q32 ruling would have broken the layout had you followed it.
Your reconciliation was right. The tangency was the only sound ground of the
three."* Recorded because a ruling that turns out to be wrong in one of its three
grounds is exactly the thing this file exists to keep visible.
### Build step 2 — `/` ships. Zero JavaScript, and six components
Eight sections specified in `docs/01` §`/`; **seven are built.** Hero ·
credential row · the approach · two processes · six practice areas · process
preview · contact band. New components, all from `docs/02`'s list: `Eyebrow`,
`SectionHeading`, `Pill`, `CredentialRow`, `PracticeCard`, `ProcessStep`, plus
`src/data/schema.ts` for the JSON-LD.
**Section 7, Latest insights, is NOT built, and that is the only spec item this
page does not deliver.** `src/content/insights/` is empty; the collection, the
`ArticleCard` component and the drafted slate all arrive together at step 7
(`docs/01` §Build order, D9). Rendering it now means shipping a component's
scoped CSS to every visitor for an empty block, plus a props surface with no
call site — already an open finding against `InfinityMark`. `SiteHeader` gates
the Insights nav item on the same collection, so the page and the nav appear
together. The omission is commented in the page source, not just here.
**Deviations from spec, each deliberate and each recorded in place:**
- **The credential row has FOUR slots, not `docs/01`'s three.** §4 Offerings'
paired-disclosure condition is the higher authority and requires the stage of
the arc stated wherever arbitration is offered; `/` says *arbitration
appointments* in its second sentence, so the stage belongs on this page and
not only in the footer. `docs/03` already authorised a fourth slot and now
records that on `/` it is required.
- **No booking link in the contact band**, which `docs/01` item 8 asks for.
Booking is parked (R6) and `CONTACT.bookingUrl` is `null`. Stated, not
silently dropped.
- **The masthead tagline is suppressed on `/`.** It is the same string as the
hero eyebrow, so at ≥76 rem the page opened with the same six words twice —
step-1 review finding #10, left open to "decide at step 2". The hero keeps it
(`docs/01` puts it there and gives it copy underneath to qualify it); the
masthead drops it, because the masthead is the placement Q33-orig objected to.
This only ever removes a claim from one page.
- **The step-1 proof sheet at `/type-scale/` is deleted**, as its own comment,
`InfinityMark`'s comment and `astro.config.mjs`'s sitemap filter all said it
would be. Five live references to it survived the deletion and were found by
`grep`, including §7's Client JS row, which cited
`dist/type-scale/index.html` as the file proving the site ships no JavaScript.
### R11 at the phase boundary — two pins were stale, one hold was too wide
Run before writing any code, which is what R11 asks. `astro` **7.2.7 → 7.2.9**
(two patches inside 48 hours). `typescript` **^5.9.3 → ^6.0.3** — a full major
behind and installable. §7 had recorded the TypeScript hold as "held at 5.x",
which was **too wide by a whole major**: 7.0.2 is genuinely blocked
(`typescript-eslint@8.68.0` peers `>=4.8.4 <6.1.0`, `@astrojs/check@0.9.10`
peers `^5 || ^6`), but 6.0.3 is stable and both peers accept it. The row now
names the version, the blocker, and the condition that ends the hold. Committed
separately as `8a2f513` so step 2's verification is not confounded by a
toolchain change.
### What the claims audit found — ten defects, and every one was implication or scope
`claims-auditor` returned **FAIL**. Not one finding was a fabrication of the kind
the old site carried; all ten were implication, scope, or a fact restated
loosely — *which is exactly where D13 says the risk lives.* All ten are fixed or
escalated. The four highest-value ones:
- **`I mediate and arbitrate commercial disputes`.** §4 verifies that he
**accepts** arbitral appointments, and separately verifies *"multiple completed
sole mediations"* — **there is no counterpart row for a completed
arbitration.** Present-indicative "arbitrate" beside "mediate" invites the
reader to supply a track record for both. Now offering-shaped: *"I mediate
commercial disputes from Toronto, and I accept arbitration appointments."* The
clean form was already on the same page, in the arbitration card.
- **The JSON-LD asserted arbitration twice and stated the stage nowhere.** The
visible page satisfied §4's paired-disclosure condition with the fourth
credential slot; the machine-readable graph did not. Both node descriptions
now carry *"the Q.Arb pathway commenced August 2026"*, and `hasCredential`
stays Q.Med-only — the stage belongs in prose, never in a field that means
*holds*. **A crawler-only claim is still a claim**, and this is the first time
the register has had to say so.
- **`at one published rate with preparation time included` misdescribed money.**
Checked against `docs/07-fees.md`: D14's card sets **two** day rates ($2,000
half, $4,000 full), so "one published rate" reads as one price for both. And
`docs/07` says in terms that the bundled prep is a **capped** allowance which
*"must be stated on the page — 'including 2 hours of preparation'... Do not
quietly fold it into the hours figure."* A home card is the wrong place to
state it properly and stating it improperly is worse than silence, so the fee
claim is gone from `/` in both places it appeared. "Published" was also
forward-looking: `/fees/` ships at step 9.
- **`Law and engineering are not blended here`** — Q37's struck parallel,
relocated from the credential label into body copy **one day after Pouya
struck it**, and strengthened by attributing both halves to him personally.
The section now reads *"what the documents say, and what the engineering
says"*: asymmetric, which was Pouya's whole point. The **argument** `docs/01`
and `docs/03` specify is unchanged. Whether Q37 formally reaches prose is his
call — **Q41(a)**.
Also fixed: *"what else is offered"* asserted four processes with no §4 row
(**Q42**); *"LAT pre-hearing mediation"* was a `docs/01` **search intent** read
as a service (**Q41c**); *"facts most neutrals take on faith"* is an
unverifiable comparative about third parties, replaced with *"the documents
rather than the pleadings"* (**Q41b**); the editorial *"with real timing"* is
gone while the timings themselves stand as specified (**Q43**); and the Q.Arb
noun is now §4's own — *pathway*, not *designation*.
**And the auditor found a stale copy of a fact in §9 itself.** Q8 still listed
memberships as *"ADRIC, ADRIO, OBA sections"* — **the Canadian Tax Foundation
was missing**, added to §4 on 2026-08-26. A second copy of a fact in a place
nobody re-reads is the failure mode §7's single-source rule exists to stop, and
this is the same shape as the stale enumeration that was found inside
`claims-auditor`'s own brief. Fixed, and rewritten as a pointer to §4 rather
than a second list. It would have bitten at step 3, the first page to publish
memberships.
### Seven defects I found in my own work before the reviewers reported
Kept because the pattern in them is the point, not the count. Full list in the
verification section; the three worth reading:
- **`<Picture widths>` declared the untouched 1600 px master as the `<img src>`
fallback** — 254,626 bytes, for a 476 px slot. Passing `width`/`height`
alongside `widths` pins it to the 960 variant (78,665 B) and the 1600 px file
stops being generated. **The build log hid it completely**: it printed
"before: 349kB" for every variant either way. Found by reading the emitted
`src` and that file's real dimensions.
- **Naming a prop `as` silently turned off prop type-checking for a whole
component.** `astro check` reported it only as `ts(6196) 'Props' is declared
but never used`, which reads like lint noise. It is not: `<Eyebrow dot as="h9"
bogusProp={1} />` compiled with **0 errors**, while the same probe against the
four other new components produced `ts(2322)` on all four. Renaming the single
identifier to `tag` restored it. **Do not silence a `ts(6196)` with
`Astro.props as Props`** — that hides the warning and leaves every call site
unchecked.
- **96 px of cream sandwiched between the maroon contact band and the ink
footer.** Found by looking at a full-page screenshot, not by reading CSS.
### And one alarm of my own that was wrong, in the direction of panic
Measuring the marks, `img.naturalWidth` came back **64 at every DPR** — which
looks exactly like *the density ladder is not being generated at all*, i.e. a
shipped step-1 defect on the header mark of every page. **It is not.** For an
image chosen from a `srcset` with an `x` descriptor, `naturalWidth` is
**density-corrected**: a 192 px file selected at 3x correctly reports 64.
Ground truth from disk: 64×41, 128×82, 192×123, in all three formats.
The real defect underneath was smaller and mine: the home page added a **225.5
px** call site to a component whose ladder was sized for 50 px and 56 px, so it
upscaled 3.52× at DPR 3. `InfinityMark` now takes `width` and `loading`; the
approach mark passes `width={232}` (→ 232/464/696, covering 676 device px) and
`loading="lazy"`, since it sits a screen and a half down. All three instances
now measure ≤1.0× upscale at DPR 1, 2 and 3.
**That is the fifth time on this project that my own instrument misread**, and
the pattern across all five is the same: a number that looks like a finding, from
a probe nobody checked. `naturalWidth` joins `timeout`, the coordinate-hull
bbox, the worst-deviating-instance filter, and the document-level overflow check.
`CLAUDE.md` gains the general rule.
### What the code review found — and finding 1 is the same defect for the FOURTH time
`adversarial-reviewer` returned **fourteen findings, three blocking.** It also
noted that the working tree moved under it mid-review, because the claims fixes
above were being applied while it ran, and it re-verified each finding against
the current tree so it could say which were still live. That is the right
behaviour and it is worth recording as the reason the two reviewers should not
be run against a moving target next time — brief them, then stop editing.
**1. BLOCKING. `class="section-head"` on `<SectionHeading>` never matched, and
three section headings collided with their content.**
This is the defect `CLAUDE.md` already records twice under *"A parent cannot
style a child component's root element"*, which then recurred a third time with
`<Picture>` inside `InfinityMark` — and I wrote a fresh warning about it into
`Eyebrow` and `Pill` in this very diff and then did it again in the page.
The emitted evidence, side by side:
```
dist/_astro/index.*.css .section-head[data-astro-cid-lcdefpme]{margin-block-end:var(--space-7)}
dist/index.html <div class="heading-block section-head" data-astro-cid-ypavld2q>
```
Measured before the fix: `margin-block-end: 0px` and a **0 px gap** to the next
block on all three call sites — 48 px of intended separation gone. Not merely
tight: `.display` sets `line-height: 0.98`, so the glyphs overflow the line box
and *"Two processes."* sat over the top edge of the Mediation and Arbitration
cards. **`astro check` reported 0 errors and `eslint` was clean**, which is
exactly what `CLAUDE.md` says this failure looks like: no error, no warning, and
the CSS reads correctly in the source.
**I had looked at a full-page screenshot of this section and passed over it.**
The gap looked plausible because a serif's descender space reads as air. A
screenshot is not a substitute for a computed-style assertion, and the reverse is
also true — three of my own findings today came only from looking.
Fixed by wrapping each in a page-owned `<div class="section-head">`. Measured
after: **48 px margin, 48 px gap, all three.** And the prop is gone —
`SectionHeading` no longer accepts `class`, so passing one is now a **build
error** rather than a silent no-op. Same deletion applied to `CredentialRow`,
`Pill`, `ProcessStep`, `PracticeCard` and `Eyebrow`, all of which declared a
`class` prop with no call site: five more invitations to the same mistake.
**2. BLOCKING, already fixed in flight — the mark was upscaled 3.5× on the home
page.** Covered above. The reviewer added the follow-up that mattered: the fix
**invalidated a figure cited as load-bearing reasoning in three places.**
`docs/02` §Components and §9 Q38 both argued *"257,278 ÷ 3,063 = 84×"* against
the committed SVG, and none was updated when a second, larger ladder appeared —
the home page's mark takes **14,555 B** at DPR 2, so the ratio there is ~11×.
That is the SES-DKIM duplication in a new costume: the copy nobody re-reads goes
stale. All three now carry both ladders and the instruction to **quote the size
with the call site, never on its own.**
**3. BLOCKING, partly fixed in flight — 234 px of horizontal content loss at a
large default font size.** The three `minmax(Nrem, 1fr)` grids were mine to fix
and were fixed; the reviewer then re-measured and found **83 px still going**,
from two elements this diff introduced:
- `.feature` measured **411 px in a 342 px container**`padding: var(--space-7)`
is 192 px a side at root 32 px, and `.feature-title`'s min-content did not
shrink because **`overflow-wrap: break-word` permits a break at layout time
but does not reduce min-content size.** Now a clamped padding plus
`overflow-wrap: anywhere`.
- `.contact-action { flex: none }` is `0 0 auto`, so the button could not shrink
below its **425 px** max-content. Now `0 1 auto` with `min-inline-size: 0`.
**4. The credential row was never "two up on a phone", and its comment said it
was.** `repeat(auto-fit, minmax(11rem, 1fr))` resolved to a **single 342 px
track** at 390 px with all four items stacked and the band ~430 px tall — with
`Q.Arb / Commenced August 2026`, which §4's paired-disclosure condition puts on
this page, at the bottom of it. The arithmetic is not subtle: two 176 px tracks
plus a 24 px gap need 376 px and the container is 342. **A measured-sounding
comment that was false**, which is this project's own named failure mode, written
by me on the same day I added the rule about it to `CLAUDE.md`.
Now explicit: `repeat(2, minmax(0, 1fr))`, becoming four at 56 rem where four
genuinely fit. Measured after: **2 rows at 320 / 390 / 640 / 768** (band height
296 / 280 / 272 / 260 px, down from ~430), **1 row from 896 px.**
**5. A §9 row cited a `grep` "in the Change Log" before the entry existed.** Q39
said so in the present tense while the newest heading was still `(v)` — because
`/build` writes the entry at Phase 6, after review. Fair catch, and the reviewer
independently re-ran the sweep and confirmed it was genuinely complete. Q39 now
names the entry.
**7. `PROCESS` was hardcoded in the page, against the reason written in
`site.ts` for centralising the practice blurbs.** *"`/` and `/practice/` both
render them and two copies of a claim-bearing sentence is one copy that will
eventually be wrong."* `PROCESS` is the same shape of content, `/process/`
renders the same five steps at step 6, and the staged step-3 body carried the
fee claim the claims audit had just found to be **wrong**. It existed in one
place today and would have existed in two. Moved to `src/data/site.ts`.
**8. The section-7 omission was recorded only in a comment in the file that
deviates.** `docs/01` was edited in this same diff and not amended. Compare
`SiteHeader`'s sticky-threshold deviation, which was written back into `docs/02`
— the project's own precedent. `docs/01` §`/` item 7 now records it.
**9. Two of FOUR above-the-fold faces were not preloaded, and the comment said
"the two faces used above the fold".** Network probe on a cold cache: `/`
requests four. The **serif italic** sets `<em class="it">the room</em>` inside
the `<h1>`, and a swap there moves the last line of a 96 px headline — now
preloaded. **Geist Mono** sets the eyebrow, which is the first visible text, and
is deliberately **not**: preloading it puts 95,688 B of font on the critical path
instead of 72,560 B, for one short line of 12 px reflow. A trade, stated as one,
revisited against Lighthouse at step 7. *The reviewer was straight about its own
limit here: throttled to slow 4G + 4× CPU it measured **LCP 896 ms, CLS 0.000**
and could not reproduce a shift.*
**10. Dead API surface.** `Eyebrow`'s `tag` prop had zero call sites, so its
`<span>` branch was unreachable and the file carried two near-identical
templates for it — deleted, keeping the `as`-naming discovery. `PORTRAIT_ALT` in
`schema.ts` was exported and referenced nowhere — deleted. `SectionHeading`
exposed both a `title` prop and a `heading` slot for one job — the prop is gone.
Five `class?: string` props — gone, per finding 1.
**1114, all `consider`, all recorded rather than churned.** The
duplicate-density `srcset` and the `/about/`-404-in-JSON-LD note are now **§7
rows** so neither is re-discovered as a defect in this repo. The portrait's 1:1
declaration under a 4:5 crop stays, with its reasoning already in the source and
CLS measured at 0. The named-landmark inconsistency is fixed by **removing**
`aria-labelledby` from the four content sections that have visible `<h2>`s and
keeping `aria-label` only on the credential band, which has no heading — the rule
being *name a region only where it has no heading of its own.* That takes the
region list from eleven entries to six.
**And one of its findings was wrong, which it said itself.** The reviewer
reported seeing **two** portrait fetches at 390 px / DPR 3, flagged it
low-confidence, and named the likely artefact — `setDeviceMetricsOverride`
applied around navigation. Checked: **one fetch at 390/DPR 1, 2 and 3 and at
1280/DPR 2 and 3**, cache cleared each time. It was the artefact. A reviewer that
states its own uncertainty is doing the job; the answer was to run the probe, not
to dismiss it.
**What it checked and found clean**, so the gap is auditable: 26 real `Tab`
keydowns dispatched, all 26 focused elements matched `:focus-visible` with a
visible ring, skip link first; **72/72** hit-test points across eight cards
resolved to the card's link; heading outline dumped, 23 headings, no skips; every
computed contrast pair recomputed from scratch including `.approach-metaphor` at
**11.09:1**; zero `client:*` directives and zero non-JSON-LD `<script>` tags;
**41 KB total transfer** cold-cache at 1280/DPR 2 with **zero JS bytes**; zero
`data:` and zero `base64,` in the HTML and CSS; no third-party host; and both
minifier traps re-verified in the emitted CSS.
### Verification — run, not asserted
Harness: a zero-dependency CDP driver (Node 22+ has a global `WebSocket`)
against `--headless=new` Chrome, serving `dist/` over `python3 -m http.server`.
Every figure below came from a command whose output was read.
| Gate | Result |
|---|---|
| `npx tsc --version` | 6.0.3 |
| `npm run check` | **0 errors, 0 warnings, 0 hints** (19 files) |
| `npm run lint` | clean — ESLint + Prettier |
| `npm run build` | complete, 1 page |
| `npm audit` | **0 vulnerabilities** |
| **Lighthouse** | **NOT RUN — tool unavailable.** `@lhci/cli` removed 2026-08-26; R11's re-add trigger is step 7. Stated, not omitted |
**Zero JavaScript, in the emitted page.** `dist/index.html` carries **1**
`<script>` and it is `type="application/ld+json"`; non-JSON-LD scripts **0**;
`dist/_astro/*.js` no matches. With script execution disabled at the protocol
level: **444 DOM nodes, 6,578 characters of visible text, 5,665 px of content**
the identical page.
**Phase 5, the minifier check.** `grep -o 'animation:[^;}]*' dist/_astro/*.css`
returns only `animation:none!important` twice, which is the reduced-motion and
print override. No `animation` shorthand beside `animation-timeline`. Both
longhand blocks survived: `animation-timeline:scroll()` with
`animation-name:header-lift`, and `animation-timeline:view()` with
`animation-name:reveal-in`.
**Layout, 14 widths (320 → 1920).** Document overflow **0 at every one**, with
zero elements past the viewport. **And the elements were measured, not only the
page** — this is the correction entry (v) owed: every rendered infinity mark
came back at aspect **1.56541.5657** against the master's 1.5657, at all 14
widths. Header **189 px** ≤414, **141 px** 6401024, **81 px `sticky`** from
1056, matching `--header-h`.
**Accessibility.** One `<h1>`; heading levels never skipped (outline dumped and
read). Landmarks 1/2/1/1. **Focus order == DOM order** across 44 focusables.
Every `<img>` has `alt` and explicit `width`/`height`; 0 missing either; 0 empty
links. **Contrast: 32 distinct foreground/background/size triples computed from
what the browser actually painted, at 390 / 768 / 1280 px — 0 failures.** Print
media: 0 revealed elements at opacity < 1 (10 targets). Reduced motion: 0 hidden.
**Touch targets, and the instrument mattered here.** The eight cards report
2639 px-tall `<a>` boxes and are **not** a finding: hit-testing nine points per
card at three widths put **9/9 inside the link on all 24 cards** — the whole card
is the target via `::after { inset: 0 }`. Two standalone paragraph links **were**
real at 18 px tall and are fixed. One inline link (164 × 21) is deliberately left
under WCAG 2.5.8's sentence exception.
**Reflow, and one case that is not clean.** Page zoom passes: 1280 at 200% is the
640 column and at 400% is the 320 column, both 0 overflow, so WCAG 1.4.4 and
1.4.10 are covered. With the reader's **default font size** at 200% (root 32 px —
not page zoom) `/` started at **234 px** of overflow at 390 and came down in
three measured steps to **3 px at 390 and 65 px at 320**: the three
`minmax(Nrem, 1fr)` grids guarded with `min()`, then `.credentials` made
explicit, then `.feature`'s padding clamped with `overflow-wrap: anywhere` on its
title and `.contact-action` allowed to shrink. **`overflow-wrap: break-word`
permits a break at layout time but does not reduce min-content size** —
`anywhere` does, and that was the whole of one fix. The residual is the header's
deliberate `flex-wrap: nowrap` above 66 rem (**602 px** at 1280) plus
`white-space: nowrap` on the brand name and the headline's 104 px floor; undoing
either re-opens the measured step-1 header decision, so they stand. Beyond what
`docs/02`'s floor requires, since page zoom is clean — a robustness margin, not a
failure. The table is in `docs/02`.
**Payload.** HTML **27,334 B** · CSS **26,572 B** (linked, over the 4 kB inline
threshold) · hero portrait AVIF **5.6 / 7.3 / 11.1 / 14.8 / 21.5 kB** across five
widths, `<img src>` fallback now the 960 variant at **78,665 B** · mark AVIF
**1,720 / 3,063 / 4,843 B** at 64 px and **6,017 / 14,555 / 22,639 B** at 232 px
· fonts ~50 kB preloaded. **JS 0 B** against a 100 kB budget.
**Not verified, and named as such.** Lighthouse (above). **The HTML validator was
not run, and the page carries a known conformance error:** 4 of 12 `srcset`
attributes declare two candidates at 1x, because Astro's `densities` emits
`url, url 1x, url 2x, url 3x` and a descriptor-less candidate is implicitly 1x.
Harmless in browsers; step-1 review finding #8, still open. **Do not claim
"validator clean".** *(And my first check of it reported "ok" — it compared
`undefined` against the string `'1x'`.)*
### Sweeps — the commands, not the claims (R8)
```
$ grep -rn "anyone may be appointed\|Anyone may be\|No designation is required" \
--include='*.md' --include='*.astro' --include='*.ts' .
-> 5 hits: 2 correct (my new text quoting the struck line; entry (v) history),
3 FIXED (§4 boundary bullet, §9 Q33 row, SiteHeader comment)
$ grep -rn "Med-arb\|med-Arb" --include='*.astro' --include='*.ts' --include='*.md' .
-> 1 hit, FIXED (index.astro:313 -> Med-Arb, the form §4 and docs/01 use)
$ grep -rn "Law and engineering" --include='*.md' --include='*.ts' --include='*.astro' .
-> label FIXED in site.ts + docs/03; the PROSE instance on / is reworded and
the two spec instances (docs/01:136, docs/03:150) are Q41(a) for Pouya
$ grep -rn "type-scale\|proof sheet" ... | grep -v dist
-> 5 live references to the deleted page, all FIXED (§7 Client JS row,
robots.txt, docs/05, brand-assets.md, InfinityMark.astro). Change Log
entries (u) and the struck Q33-orig row keep theirs — append-only history
$ grep -cEi 'OCNI|Law Society|LSO|testimonial|Since 20|London|New York' dist/index.html
-> 1, and it is a FALSE POSITIVE: "I aLSO practise". Case-sensitive: 0
$ grep -cEi 'leading|premier|top-rated|best|proven|guarantee|extensive' dist/index.html
-> 2, both FALSE POSITIVES: "the pLEADINGs" twice. No superlative on the page
$ grep -cE 'LegalService|worksFor|priceRange|aggregateRating' dist/index.html
-> 0
```
### Superseded
**Entry (v)'s "⏸ RESUME HERE" section is spent.** All five items it listed as
awaiting a ruling — Q35, Q39, Q37, Q38/R13, R1 — were ruled on 2026-08-27 and are
handled above. Its six non-blocking review findings: **#7 fixed** (`--text-2xs`,
`--space-05`, `--focus-offset` added), **#8 still open and now measured** (4 of 12
srcsets; see Verification), **#9 unchanged** (`--header-h` has 0.81 px of
headroom), **#10 fixed** (the masthead tagline is suppressed on `/`), **#11
unchanged** (~3.16 MB of brand binaries), **#12 fixed, and by deletion rather than by
use** — `InfinityMark`'s `class` prop is **removed**. It had no call site and
`class:list` put it on the `<img>`, so a parent writing
`<InfinityMark class="foo" />` would have got a rule compiled against the
parent's cid that never matches: the exact defect `CLAUDE.md` records twice on
this project, offered as an API. `label` stays with its reason written down (all
three current call sites are decorative; the next one may not be), and `width`
and `loading` are new and used. Entry (v) is not edited; this is where the
state lives now.
---
## 2026-08-26 (v) — The mark was wrong; §4 gains an Offerings category; and two of my own tools lied to me
**Who:** Pouya ruled on the mark (Q32), on arbitration (Q33, Q36), and on the
+42
View File
@@ -236,6 +236,48 @@ correction reached §7 and never reached it, and the stale copy ended up telling
an operator to delete the three records that authenticate outbound mail —
under the heading "Never delete".
**A measurement is a claim about your instrument until you check the
instrument.** This has now cost five times, and the shape is identical every
time: a number that looks like a finding, from a probe nobody validated.
- `timeout 60 ls "$DRIVE"`**the command never ran.** `timeout` is not
installed on macOS. Empty output from a command that failed to start looks
exactly like empty output from a command that found nothing, and it produced a
report that the brand assets were unreachable when the directory was fully
readable.
- **`1.23:1` for the traced mark** — the bounding box of the path's *coordinate
hull*, not of the curve. A cubic's control points sit outside it, so the box
was 33% too tall while giving the *correct* width — which means the obvious
sanity check, "does the width look right?", passes.
- **"the mark renders at 24px"** — the harness reported the worst-deviating
instance on the page, not the instance under discussion, which was exact.
- **"0 overflow at every width"** — true, and it measured the *document*. A flex
child was absorbing the deficit by being crushed to aspect 0.891. **Measure
the elements, not only the page.**
- **`img.naturalWidth` = 64 at DPR 1, 2 and 3** — which reads as *the density
ladder is not being generated at all*, a shipped defect on every page. It is
**density-corrected by spec**: a 192px file selected at `3x` correctly reports
64. The files on disk were 64 / 128 / 192 all along.
So before acting on a number: say what it is a number *of*; confirm the command
actually ran and read its exit status; and check it against a second method that
cannot fail the same way — the bytes on disk, a screenshot, a hit test.
**And a grep that matches is not a finding until you read what it matched.**
A case-insensitive sweep for `LSO` hit `I aLSO practise`; a superlative sweep for
`leading` hit `the pLEADINGs`. Both on the same page on the same day. Print the
match with context before you believe it.
**Never name an Astro prop `as`.** `const { as = 'p' } = Astro.props` detaches
the `Props` interface from the component, and **every call site silently stops
being type-checked.** `astro check` reports it only as `ts(6196) 'Props' is
declared but never used`, which reads like lint noise. Measured: with the prop
named `as`, `<Eyebrow dot as="h9" bogusProp={1} />` compiled with **0 errors**;
renaming the one identifier to `tag` made the same probe fail correctly. **Do not
silence a `ts(6196)` with `Astro.props as Props`** — that hides the warning and
leaves the call sites unchecked, which is strictly worse. If that hint appears on
any component, pass it a bogus prop before believing its props are checked.
**A sweep is a command, not a claim.** Any statement that a change was applied
across files — a phrase removed everywhere, a path updated everywhere, a
decision swept through the docs — must cite the command that proves it, and be
+6 -4
View File
@@ -32,10 +32,12 @@ export default defineConfig({
integrations: [
mdx(),
sitemap({
// /legal/* is noindex by spec (docs/04). /type-scale/ is the temporary
// step-1 proof sheet — delete this half of the condition with the page.
filter: (page) =>
!page.includes('/legal/') && !page.includes('/type-scale/'),
// /legal/* is noindex by spec (docs/04) and nothing else is excluded.
// The `/type-scale/` half of this condition is gone with the page it
// named: the step-1 proof sheet was deleted at step 2, as its own comment
// and InfinityMark's both said it would be. Recoverable from git if the
// specimen is ever wanted again; it is not a route the site ships.
filter: (page) => !page.includes('/legal/'),
changefreq: 'monthly',
// No `lastmod`. It was `new Date()`, which stamped every URL with the
// build time — telling crawlers all 17 pages changed whenever one did.
+23 -1
View File
@@ -141,7 +141,15 @@ four audiences to its surface.
block on the page for search, because it distributes authority to the pages
that can actually rank.
6. **Process preview.** Compressed five-step strip → `/process/`.
7. **Latest insights.** Three most recent → `/insights/`.
7. **Latest insights.** Three most recent → `/insights/`. **NOT BUILT AT STEP
2, and it arrives at STEP 7 with the collection it lists.** `ArticleCard` and
the drafted slate (D9) land in the same step; rendering the section against an
empty collection means shipping a component's scoped CSS to every visitor for
a block with nothing in it, plus a props surface with no call site.
`SiteHeader` already gates the Insights **nav** item on the same collection,
so the page section and the nav item appear together. Recorded here as well
as in the page source, because "eight sections specified, seven built" should
not be discoverable only by reading the file that deviates.
8. **Contact band.** Intake CTA and booking link.
### `/about/` — Biography and credentials
@@ -280,6 +288,20 @@ Shareholder and partnership disputes, co-founder breakdowns, family-business
succession, SME exits. The operator angle — running SML Company Ltd. alongside
the practice — is the differentiator here.
**"Family Business" means COMMERCIAL disputes among family shareholders, and the
page must say so.** Pouya's ruling of 2026-08-27 (`AGENTS.md` Q39): the label
covers shareholder and partnership disputes, co-founder breakdowns and business
succession — **not** family law. **Family arbitration under the *Family Law Act*
is not offered**, and that activity is separately gated by prescribed training
(`docs/reference/ontario-family-arbitration-training.md`), so the exclusion has
to be legible rather than left to be inferred from the surrounding nouns.
**One sentence, not a section.** His instruction, and the reason is also the test
for whether it belongs at all: *"The page should say plainly that family law
matters are not accepted. One sentence, not a section: it saves a wasted intake
call, which is the only reason it earns its place."* A disclaimer that grows into
a paragraph reads as defensive, which is the opposite of the point.
### `/practice/cross-cultural/`
**Search intent:** `Farsi speaking mediator Toronto`,
+41 -2
View File
@@ -183,7 +183,7 @@ except the reveal of the hero.
| Component | Notes |
|---|---|
| `InfinityMark` | ⚠️ **Currently a raster — a documented, temporary exception to this rule** (`AGENTS.md` Q38, R13). The mark is gradient-mesh artwork rather than flat vector paths. An SVG *is* held and it renders faithfully — but at 257,278 bytes against 3,063 for the AVIF a Retina browser takes, carrying seven embedded base64 PNGs that would breach `CLAUDE.md`'s no-base64-images rule. The exception is about payload and composition, not about fidelity. It renders AVIF/WebP with a PNG fallback; a Retina device takes 3,063 B of AVIF, `alt=""` when decorative, aspect ratio pinned to `667 / 426`. **Restore this rule the moment the commissioned vector master lands.** Until then the rule stands unchanged for every other mark or icon: inline SVG, `currentColor`, `aria-hidden` when decorative, never a PNG |
| `InfinityMark` | ⚠️ **Currently a raster — a documented, temporary exception to this rule** (`AGENTS.md` Q38, R13). The mark is gradient-mesh artwork rather than flat vector paths. An SVG *is* held and it renders faithfully — **and it renders faithfully because it IS the raster**: 257,278 bytes wrapping **seven embedded base64 PNGs**, against **3,063 B** for the AVIF a Retina browser takes in the header — **84×**. *Figures re-stated 2026-08-27 because a second, larger call site now exists and the single number had become misleading:* the mark ships at **two intrinsic widths**, 64 px (header, footer) and 232 px (the home page's approach section, which renders at 225.5 px). At 232 px a DPR-2 device takes **14,555 B** and DPR-3 **22,639 B**, so the ratio against the SVG there is ~11×, not 84×. The full ladders are in `docs/reference/brand-assets.md`; do not quote one number as if it covered both. Inlining it would breach `CLAUDE.md`'s no-base64-images rule. *Pouya settled the characterisation on 2026-08-27: a first draft called the file "a raster in a vector wrapper", a later draft withdrew that as unfair, and **the withdrawal went too far.** Both halves are true — the fidelity is real, and it is bought with embedded raster, which is exactly why fidelity was never the question.* The exception is about payload and composition. It renders AVIF/WebP with a PNG fallback; a Retina device takes 3,063 B of AVIF, `alt=""` when decorative, aspect ratio pinned to `667 / 426`. **Restore this rule the moment the commissioned vector master lands.** Until then the rule stands unchanged for every other mark or icon: inline SVG, `currentColor`, `aria-hidden` when decorative, never a PNG |
| `SiteHeader` | **Sticky from 66 rem (1056 px) up; static below it.** Practice dropdown as CSS-only `<details>`. **(a)** The one-row header holds the brand, **seven** nav items and the CTA — Insights is the seventh, arriving on its own at build step 7. Binary search on the built page puts the true fit at **1047 px**; 66 rem is the clean token above it. Below 66 rem the nav takes its own row and the header measures **141 px** at tablet widths and **189 px** at 320375 px. **(b)** "Condenses on scroll" is a hairline rule and a shadow, **not a size change** — a `position: sticky` header stays in normal flow, so shrinking its padding lifts every page below it, against the CLS < 0.05 budget. Longhands only; see the component on what the minifier does to the `animation` shorthand. *(This row has been wrong twice, instructively. It first said 60 rem / "~115 px", a height the header never took. It then said 64 rem "with 32 px of clearance" — that 32 px was `.header-inner`'s own `column-gap` mistaken for slack; the real figure at 1024 px with seven items was **21.6 px**, and nothing overflowed only because flexbox crushed the logo inside the brand block. **Measure slack, not gaps.**)* |
| `SiteFooter` | Three-column sitemap, contact block, designations, entity line |
| `Eyebrow` | Mono label with optional maroon dot |
@@ -216,5 +216,44 @@ Not a polish pass. A build requirement.
- Forms: real `<label>` elements, `aria-describedby` for hints, errors announced
with `role="alert"` and tied to their field.
- Touch targets ≥ 44 × 44 px.
- Test at 200% zoom and at 320 px width.
- Test at 200% zoom and at 320 px width. **Both measured 2026-08-27 on `/`:
document overflow 0 at 320, 360, 390, 414, 640, 768, 900, 1024, 1056, 1200,
1216, 1280, 1440 and 1920 CSS px, with zero elements extending past the
viewport.** Page zoom at 200% of 1280 is the 640 column and at 400% is the
320 column, so WCAG 1.4.4 and 1.4.10 are both covered by that sweep.
**A stricter case is not fully clean, and it is recorded rather than left to
be discovered.** With the reader's *default font size* at 200% — root at
32 px, a real accessibility setting and not page zoom — `/` measured **234 px**
of overflow at 390. Brought down in three measured steps:
| Fix | 390 px | 320 px |
|---|---|---|
| as first built | 234 px | 304 px |
| `minmax(min(Nrem, 100%), 1fr)` on three grids | 83 px | 153 px |
| `.credentials` made explicit `repeat(2, minmax(0, 1fr))`; `.feature` padding clamped and `overflow-wrap: anywhere` on its title; `.contact-action` `flex: 0 1 auto` + `min-inline-size: 0` | **3 px** | **65 px** |
Two things worth keeping. **`overflow-wrap: break-word` permits a break at
layout time but does not reduce min-content size** — `anywhere` does, and that
distinction was the whole of one of those fixes. And 1280 px stays **602 px**
over, from the header's deliberate `flex-wrap: nowrap` above 66 rem plus
`white-space: nowrap` on the brand name; the 320 px residual is the same header
plus the display headline's 104 px floor. Undoing either re-opens the measured
step-1 header decision, so they stand. All of this is beyond what this floor
requires — page zoom is clean — so it is a robustness margin rather than a
failure. Revisit if a real reader hits it.
- **Measure the elements, not only the page.** A document-level overflow check
passes while a flex child absorbs the deficit by being crushed — that is how
step 1 shipped a logo at aspect 0.891 under a green "0 overflow at every
width". Assert the rendered geometry of the thing you care about. On `/`
every infinity-mark instance measures 1.56541.5657 against the master's
1.5657.
- **A touch-target measurement of the wrong box is not a finding.** The eight
cards on `/` report 2639 px-tall `<a>` elements and are fine: each card's
whole box is the link's hit area via `::after { inset: 0 }`, verified by
hit-testing nine points per card at three widths (24 cards, 9/9). Hit-test
before enlarging anything. WCAG 2.5.8's inline exception also applies to a
link sitting mid-sentence, and one such link on `/` is deliberately left at
164 x 21.
- Every page must be readable and navigable with JavaScript disabled.
+17 -1
View File
@@ -113,13 +113,29 @@ Three slots, never counts:
| Slot | Value | Label |
|---|---|---|
| 1 | **Q.Med** | ADRIC / ADRIO designation |
| 2 | **JD + ML** | Law and engineering |
| 2 | **JD + ML** | Legal training and engineering practice |
| 3 | **EN · FA** | Bilingual practice |
**Slot 2's label changed on 2026-08-27 (Pouya, `AGENTS.md` Q37).** It read
*"Law and engineering"*. His reasoning: *"The parallel was doing the implying — a
degree and a practice under one noun. The asymmetry is the honest part."* A JD is
a degree; engineering is a practice, and a verified one. Setting them in parallel
invited the reader to supply the symmetry, and for "Law" the missing half is a
licence — which D13 bars by implication as hard as by assertion. The replacement
is longer and deliberately lopsided. Do not tidy it back into a parallel.
Fourth slot where the layout has one: **Q.Arb — commenced August 2026.** Use
that wording, not "in progress": §4 pins it, and the weaker form drifts toward
"nearly complete", which §4 Forbidden bars outright.
**On the home page the fourth slot IS used, and it is not optional there.**
`docs/01` §`/` says "Three slots"; §4's paired-disclosure condition is the higher
authority and requires that wherever the site offers arbitration it "states
plainly" the stage of the arc. `/` says *arbitrator* in its opening sentence, so
the stage belongs on the same page rather than only in the footer. Rendered as
value `Q.Arb` over label `Commenced August 2026` — the same wording, with the
em-dash carried by the layout instead of by the string.
The substitution principle (`AGENTS.md` §4): wherever the design wants a "how
many", substitute a longer-arc credential. These are all true at launch and stay
true; none grows by closing files.
+2 -1
View File
@@ -209,7 +209,8 @@ An embedded scheduler for the 3045 minute confidential intake call
> **Note added 2026-08-26 — `style-src` has acquired a dependency.** The site
> now ships inline `style="…"` attributes that are load-bearing rather than
> decorative: `InfinityMark.astro` sets its own `block-size` that way, and the
> proof sheet renders computed swatches with it. They are fine under
> step-1 proof sheet rendered computed swatches with it (that page was deleted
> at build step 2; the mechanism is what matters here). They are fine under
> `style-src 'self' 'unsafe-inline'` as specified below. They would **not**
> survive a move to hashed or nonce'd styles — the infinity mark would collapse.
> Price that before tightening `style-src`, and read the components first.
+38 -10
View File
@@ -42,8 +42,11 @@ of this file implied it was; that was wrong.
What rules it out is weight and composition:
- **257,278 bytes**, against **9,468 bytes** for the AVIF a browser actually
takes at the size the mark renders. **27×.**
- **257,278 bytes**, against **3,063 bytes** for the AVIF a Retina device
actually takes in the header. **84×.** *(This line said "9,468 bytes... 27×"
until 2026-08-27. 9,468 was the DPR-1 figure — the number for the devices the
performance budget does **not** target. Quote the figure for the device the
budget is written for.)*
- **7 embedded base64 PNGs** (`<image>` elements), so it is a hybrid rather than
pure vector — inlining it would breach `CLAUDE.md`'s rule against
base64-inlining images, which is one of the specific faults of the build this
@@ -55,8 +58,12 @@ What `AGENTS.md` Q38 asks for is a master that is faithful **and** light.
## What the browser actually downloads
`<Picture>` emits AVIF, WebP and a PNG fallback at 1× and 2× of a 320 px
intrinsic width:
`<Picture>` emits AVIF, WebP and a PNG fallback at `densities` 1×, 2× and 3× of
whatever intrinsic `width` the call site passes. **There are now TWO ladders,
because there are two sizes of call site** `[measured 2026-08-27 — every figure
below read from the file on disk]`.
**`width={64}` — the default. The header (50.1 px wide) and footer (56.4 px).**
| | 1× (64 px) | 2× (128 px) | 3× (192 px) |
|---|---|---|---|
@@ -64,12 +71,33 @@ intrinsic width:
| WebP | 2,368 B | 5,368 B | 8,664 B |
| PNG (fallback) | 6,137 B | 16,996 B | 29,780 B |
A Retina device takes **3,063 B**. The first attempt sized the ladder at 320 px
— justified by a 4 rem sample on the proof sheet, a page step 2 deletes — and
stacked `densities` on top, so every DPR-2 device pulled a 640 px image into a
56 px slot: **20,629 B**, and the docs claimed 9 KB because that is what DPR 1
takes. Size to the largest render that survives, and quote the figure for the
devices the budget targets.
**`width={232}` — the home page's approach section, which renders at 225.5 px.**
| | 1× (232 px) | 2× (464 px) | 3× (696 px) |
|---|---|---|---|
| AVIF | **6,017 B** | **14,555 B** | **22,639 B** |
That instance is `loading="lazy"`: it sits roughly a screen and a half down, so
it is not on the LCP path. The header and footer marks stay `eager`.
**THE LADDER HAS NOW BEEN WRONG IN BOTH DIRECTIONS, which is why `width` is a
prop rather than a constant.**
- *Too big, 2026-08-26.* Sized at 320 px, justified by a 4 rem sample on the
proof sheet (a page step 2 has since deleted), with `densities` stacked on
top — so the ladder double-counted its own headroom and every DPR-2 device
pulled a 640 px image into a 56 px slot: **20,629 B**, while the docs claimed
9 KB because that is what DPR 1 took.
- *Too small, 2026-08-27.* The home page added a 225.5 px call site and
inherited the 64 px ladder, whose largest file is 192 px: **3.52× upscale at
DPR 3.** Now 232/464/696, and 696 covers the 676 device px a DPR-3 screen
asks for. All three instances measure ≤1.0× upscale at DPR 1, 2 and 3.
**Do not measure this with `img.naturalWidth`.** For an image chosen from a
`srcset` with an `x` descriptor it is **density-corrected**: the 192 px file
selected at `3x` reports 64, so reading it at DPR 1, 2 and 3 returns 64 every
time — which looks exactly like the ladder not being generated at all. Read the
files on disk.
Passing an explicit `width` is load-bearing: without it Astro emits the
untouched 2668 px master as the `<img src>` fallback — **1,146,406 bytes**
@@ -0,0 +1,77 @@
# Reference — Ontario's training requirements for family arbitrators
**Why this file exists.** `AGENTS.md` §4 Offerings rests on a proposition about
what Ontario law does and does not gate. R14: *anything a spec makes a claim
about must be reachable from the repository* — a claim whose source lives only
at a URL is one a reviewer can be asked to trust rather than check. This is the
extract, with its provenance and the command that produced it.
It is **not** legal advice and it is not a substitute for the instruments
themselves. It records what one government page said on one day.
---
## Provenance
| | |
|---|---|
| Source | `https://www.ontario.ca/page/training-family-arbitrators` |
| Retrieved | **2026-08-27** |
| Method | `WebFetch` — page converted to markdown, then queried for verbatim requirements, hour figures, the lawyer / non-lawyer distinction, ongoing-training period, and every statute or regulation named |
| Retrieved by | Claude Code, on Pouya's instruction of 2026-08-27 (Q39) |
| Cited by Pouya | Yes — this is the source named in his Q39 ruling, with the same three hour figures |
**Re-derive it:** fetch the URL and read it. If the page has changed, record the
change here rather than editing the extract — a stale extract with a date is
useful; a silently updated one is not.
---
## What the page states
Quoted as retrieved. Where the page's own wording is reproduced it is in
quotation marks.
**Screening for domestic violence and power imbalances.** "at least 14 hours
(within one week) to learn about screening parties for domestic violence and
power imbalances".
**Ontario family law — non-lawyers only.** "All family law arbitrators who are
not a part of the Ontario Bar, or another Canadian bar, must complete 30 hours
of training about Ontario family law." The page adds that "You do not need to
complete this training all at once."
**Members of the Ontario Bar.** No hour figure. The page states instead that
"you should ensure you are familiar with family law to fulfil your professional
obligation to provide services competently."
**Ongoing training.** "10 hours over any two-year period. Five of these hours
must be related to domestic violence or power imbalance issues".
**Statute named on the page.** *Arbitration Act, 1991*. **No section number and
no regulation (`O. Reg.`) number appears on the page.**
---
## What the page does NOT state — and this half matters more
**It says nothing about commercial arbitration.** Commercial arbitration is
neither mentioned nor excluded. The page does not state which arbitrations the
requirements apply to beyond describing them as family arbitration.
So the source establishes the **family** half of §4's scoped proposition
directly, and the **commercial** half only by *absence* — a page about family
arbitrators is not authority for what commercial arbitrators need. §4 records
the commercial half as **Pouya's stated position**, attributed to him and
deliberately unstamped, for exactly that reason.
**Nothing on the site turns on the gated activity.** Pouya has confirmed he does
not accept family arbitration under the *Family Law Act* (§4 Offerings, scope
exclusion). The requirements above are recorded because the register reasoned
from a false universal for a day and must not do so again — not because the
practice sits anywhere near them.
**Do not upgrade this file into an authority it is not.** If a stronger source
is ever wanted — the *Family Law Act* provisions and the regulation made under
it — fetch and extract those, name them by number, and date them. Do not write
a section number from memory.
+1 -1
View File
@@ -3,7 +3,7 @@
# is using to shortlist a neutral is the point. See docs/04-seo-spec.md.
# NOTHING IS DISALLOWED, DELIBERATELY.
# /legal/* and the temporary /type-scale/ are kept out of the index by
# /legal/* is kept out of the index by
# `<meta name="robots" content="noindex,follow">`, which is the directive that
# actually de-indexes. Disallowing them as well would defeat it: a crawler that
# is forbidden to FETCH a URL never reads the noindex on it. The legal pages are
+104
View File
@@ -0,0 +1,104 @@
---
/**
* docs/02: "Three or four credential slots. NEVER matter counts — AGENTS.md §4."
*
* THE SUBSTITUTION PRINCIPLE IS THE POINT OF THIS COMPONENT (§4). Wherever a
* design wants a "how much / how many", it takes a longer-arc credential
* instead: something already substantial and true at launch that does not grow
* by closing files. `Q.Med` / `JD + ML` / `EN · FA`, with `Q.Arb` fourth.
*
* So this component is not a stat row wearing a different name, and it must
* never be handed one. §4 Forbidden bars every count of matters closed, hours
* mediated or years in practice, plus settlement rates and dollar figures. The
* slots come from src/data/site.ts, which mirrors §4; nothing is typed here.
*
* <dl> RATHER THAN A DIV GRID. Each pair is a term and its description, which
* is what a description list is. It also fixes the reading order: a screen
* reader gets "Q.Arb — Commenced August 2026" as one associated pair, which is
* §4's paired-disclosure condition surviving into assistive technology rather
* than being a visual arrangement only. Wrapping each dt/dd pair in a <div>
* inside <dl> is valid HTML and is what makes the grid tractable.
*/
interface Props {
slots: ReadonlyArray<{ value: string; label: string }>;
}
// No `class` prop: it was declared, never passed, and a parent cannot reach this
// root anyway (SectionHeading carries the measurement; CLAUDE.md the rule).
const { slots } = Astro.props;
---
<dl class="credentials">
{
slots.map((slot) => (
<div class="credential">
<dt class="credential-value">{slot.value}</dt>
<dd class="credential-label">{slot.label}</dd>
</div>
))
}
</dl>
<style>
.credentials {
display: grid;
/* EXPLICIT COLUMNS, BECAUSE `auto-fit` NEVER DELIVERED THE ROW IT CLAIMED.
This read `repeat(auto-fit, minmax(11rem, 1fr))` under a comment saying
"two up on a phone, four up where there is room". `adversarial-reviewer`
measured it: at 390px the resolved template was a SINGLE 342px track and
all four items stacked, running the band ~430px tall — with
`Q.Arb / Commenced August 2026`, which §4's paired-disclosure condition
puts on this page, at the bottom of it. The arithmetic is not subtle: two
tracks at an 11rem (176px) floor plus a 24px gap need 376px and the
container is 342px, so `auto-fit` correctly dropped to one. A
measured-sounding comment that was false is this project's own named
failure mode.
`minmax(0, 1fr)` cannot overflow at any width or any root font size,
which also retires the `min(11rem, 100%)` guard this line briefly
carried — that guard was fixing the overflow symptom of a floor that
should not have been there. */
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-6) var(--space-5);
margin: 0;
padding-block: var(--space-7);
border-block: 1px solid var(--rule);
}
.credential {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.credential-value {
font-family: var(--font-serif);
font-size: var(--text-3xl);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
color: var(--accent);
}
/* Four across only where four actually fit, measured rather than inferred.
Two 11rem-equivalent tracks plus three 24px gaps need 776px of container;
above 48rem the gutter is 48px each side, so that is a 872px viewport. 56rem
(896px) is the clean token above it. Re-measure if --space-5 or the label
type changes. */
@media (min-width: 56rem) {
.credentials {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.credential-label {
margin: 0;
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: var(--tracking-wide);
line-height: var(--leading-snug);
text-transform: uppercase;
color: var(--text-meta);
/* "Legal training and engineering practice" is 39 characters and is
deliberately long (AGENTS.md Q37) — it wraps to two lines at every width
and must not be prevented from doing so. Do not add `white-space: nowrap`
here, and do not shorten the label to make the row tidier: the asymmetry
is the honest part. */
text-wrap: pretty;
}
</style>
+63
View File
@@ -0,0 +1,63 @@
---
/**
* docs/02: "Mono label with optional maroon dot."
*
* NO SCOPED STYLES ON PURPOSE. `.eyebrow` and `.eyebrow .dot` are already
* global (global.css) because `.section-inverse .eyebrow` has to recolour them
* from an ancestor, and a scoped rule here would be a second source for the
* same thing. This component exists to stop `<span class="eyebrow">` being
* hand-typed on nineteen pages, not to own the look.
*
* AN EYEBROW IS A LABEL, NEVER A HEADING. It renders <p> or <span>, never
* <h*>: docs/02's accessibility floor forbids skipped heading levels, and an
* eyebrow above an <h2> is exactly where an <h3>-before-<h2> creeps in.
*
* ⚠️ NEVER NAME AN ASTRO PROP `as`. The prop deleted below was briefly called
* `as`, and that name SILENTLY TURNED OFF PROP TYPE-CHECKING for every caller
* of this component. Kept here because the next person to want a dynamic tag
* will reach for `as` first.
*
* `astro check` reported it only as a hint — `ts(6196) 'Props' is declared but
* never used` — which reads exactly like lint noise and is the reason it is
* worth writing down. It is not noise: it is the compiler saying the `Props`
* interface is not attached to anything.
*
* Measured rather than assumed, 2026-08-27. With the prop named `as`,
* `<Eyebrow dot as="h9" bogusProp={1} />` compiled with **0 errors**. The same
* probe against CredentialRow, SectionHeading, PracticeCard and ProcessStep
* produced `ts(2322)` on all four, so the loss was specific to this file.
* Renaming the single identifier `as` to `tag` — one variable changed, nothing
* else — took the file from 1 hint / 0 errors to 0 hints, and the same probe
* now fails correctly: `Type '"h9"' is not assignable to type '"p" | "span" |
* undefined'`. `const { as = 'p' } = Astro.props` is read as a type assertion
* somewhere in the generated TSX and detaches the binding.
*
* A DYNAMIC `<Tag>` FROM A VARIABLE was the first hypothesis for the lost
* binding and it was WRONG — restructuring the template changed nothing, the
* rename fixed it. Recorded so the wrong cause is not re-derived.
*
* DO NOT "FIX" A ts(6196) HINT WITH `Astro.props as Props`. It silences the
* warning inside the component and leaves every call site unchecked, which is
* strictly worse than the warning. If this hint appears on another component,
* probe a bogus prop before believing the props are checked.
*/
interface Props {
/** The maroon dot. Decorative — it is a CSS box, so it is invisible to AT. */
dot?: boolean;
}
/**
* ONE PROP, AND THE OTHER TWO ARE DELETED. `tag?: 'p' | 'span'` had zero call
* sites, so its `<span>` branch was unreachable code and the file carried two
* near-identical templates for it. `class?: string` had zero call sites too, and
* a parent cannot reach this root anyway (see SectionHeading, and CLAUDE.md).
*
* The rename that produced the finding below is kept; only the prop is gone.
*/
const { dot = false } = Astro.props;
---
<p class="eyebrow">
{dot && <span class="dot" aria-hidden="true" />}
<slot />
</p>
+58 -12
View File
@@ -49,24 +49,70 @@ import mark from '../assets/brand/sml-infinity-mark.png';
interface Props {
/** Rendered height. Width follows 1.5657:1. */
size?: string;
/** Accessible name. Omit for decorative use — the default. */
/**
* Intrinsic width of the 1x variant, in px. RAISE IT FOR A LARGE CALL SITE.
*
* The default 64 is sized for the header (50.1px wide) and footer (56.4px) —
* with densities [1,2,3] that gives 64/128/192 and both are sharp to DPR 3.
* The home page's approach section renders the mark at 225.5px, five times
* larger, and inherited the same 64: measured 1.17x upscale at DPR 1, 2.35x
* at DPR 2, 3.52x at DPR 3. A prop, because the component cannot infer this
* from `size` — `size` may be a `clamp()`.
*/
width?: number;
/**
* `eager` for the two above-the-fold marks; `lazy` for anything below it.
* The component hardcoded `eager`, which is right for a masthead and wrong
* for a 700px-wide decorative anchor two screens down.
*/
loading?: 'eager' | 'lazy';
/**
* Accessible name. Omit for decorative use — the default, and the case at all
* three current call sites: the header and footer marks sit beside the name
* they stand for, and the home page's mark sits beside a paragraph that says
* what it is. Kept because the moment the mark appears without adjacent text
* it needs one, and a component that cannot take an accessible name invites
* `alt=""` on an informative image.
*/
label?: string;
class?: string;
}
const { size = '1.75rem', label, class: className } = Astro.props;
/**
* NO `class` PROP, AND ITS REMOVAL IS THE POINT. It existed, had no call site,
* and `class:list` put it on the <img> — so a parent writing
* `<InfinityMark class="foo" />` would get a rule compiled against the PARENT's
* cid that never matches the child's root. That is the exact defect CLAUDE.md
* records twice on this project. The prop was an invitation to reproduce it.
* To position a mark, style a wrapper the parent owns.
*/
// Sized to the largest render that SURVIVES, which is the footer at 2.25rem =
// 56.4 CSS px wide. The first attempt used 320px, justified by the 4rem sample
// on the proof sheet — a page step 2 deletes — and then stacked densities on
// top, so the ladder double-counted its own headroom and every Retina device
// pulled a 640px image into a 56px slot.
const { size = '1.75rem', width, loading = 'eager', label } = Astro.props;
// THE DEFAULT IS SIZED TO THE LARGEST *DEFAULT* CALL SITE — the footer at
// 2.25rem, i.e. 56.4 CSS px wide. With densities [1,2,3] that is 64/128/192 and
// the footer is sharp to DPR 3.
//
// PASSING A WIDTH EXPLICITLY IS STILL THE POINT. Without it Astro emits the
// It has now been wrong in both directions, which is why it is a prop:
// - Too big, 2026-08-26: 320px, justified by a 4rem sample on the step-1
// proof sheet (a page step 2 has since deleted), with densities stacked on
// top — so the ladder double-counted its own headroom and every Retina
// device pulled a 640px file into a 56px slot.
// - Too small, 2026-08-27: the home page added a 225.5px call site and
// inherited 64, upscaling 3.52x at DPR 3. Raise `width` at the call site.
//
// PASSING A WIDTH AT ALL IS STILL THE POINT. Without it Astro emits the
// untouched 2668px master as the <img src> fallback: 1,146,406 bytes, which any
// client without AVIF or WebP support would actually download, sitting in dist
// looking like an optimisation had happened.
const INTRINSIC_WIDTH = 64;
const INTRINSIC_WIDTH = width ?? 64;
// A NOTE ON MEASURING THIS, because the obvious probe lies. `img.naturalWidth`
// on an image chosen from a `srcset` with an `x` descriptor is DENSITY-
// CORRECTED: the 192px file selected at 3x reports 64. Reading it at DPR 1, 2
// and 3 therefore returns 64 every time, which looks exactly like "the density
// ladder is not being generated at all" — a far more alarming defect than the
// real one. It is generated: 64x41, 128x82, 192x123 on disk, in all three
// formats. Check the files, not naturalWidth. [verified 2026-08-27]
---
<Picture
@@ -76,9 +122,9 @@ const INTRINSIC_WIDTH = 64;
formats={['avif', 'webp']}
fallbackFormat="png"
alt={label ?? ''}
loading="eager"
loading={loading}
decoding="async"
class:list={['mark', className]}
class="mark"
pictureAttributes={{ style: `block-size:${size}` }}
/>
+44
View File
@@ -0,0 +1,44 @@
---
/**
* docs/02: "Small bordered label for designations and sector chips."
*
* A PARENT CANNOT STYLE THIS ELEMENT. Astro does not pass a parent's scope
* attribute to a child component's root, so `<Pill class="chip" />` compiles the
* parent's `.chip` rule against the parent's cid and it never matches — the
* defect CLAUDE.md records for <Button> in SiteHeader, which then recurred with
* <Picture> in InfinityMark. CLAUDE.md names Pill as the next place it will
* happen.
*
* THE HOOK IS A CUSTOM PROPERTY, and that is the one mechanism that legitimately
* crosses the boundary: custom properties inherit. An ancestor sets
* `--pill-border` / `--pill-fg` on ITSELF and this component reads it. No
* :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.
*/
---
<span class="pill"><slot /></span>
<style>
.pill {
display: inline-flex;
align-items: center;
padding-block: var(--space-1);
padding-inline: var(--space-3);
border: 1px solid var(--pill-border, var(--border));
border-radius: var(--radius-full);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
line-height: 1.4;
text-transform: uppercase;
color: var(--pill-fg, var(--text-meta));
white-space: nowrap;
}
</style>
+116
View File
@@ -0,0 +1,116 @@
---
/**
* docs/02: "Sector chip, heading, one paragraph, arrow link."
*
* ONE LINK PER CARD, AND THE WHOLE CARD IS ITS HIT AREA. The link wraps only
* the heading text, so its accessible name is "Construction & Infrastructure"
* rather than the card's entire contents; a `::after` pseudo-element stretched
* over the positioned card carries the click. Six of these on the home page
* would otherwise be six links each announcing three sentences.
*
* The arrow is `aria-hidden` and outside the link text for the same reason.
*
* THE GRID MUST NOT TRY TO STYLE THIS ROOT. A parent's `.card { block-size:
* 100% }` compiles against the parent's cid and never matches (CLAUDE.md;
* it has now cost twice). The card sizes ITSELF to its grid cell below, so a
* parent only ever needs `display: grid` and `gap` on its own element.
*/
import Pill from './Pill.astro';
interface Props {
href: string;
chip?: string;
title: string;
/** Explicit: docs/02 forbids skipped heading levels. */
level: 2 | 3;
}
// No `class` prop — declared, never passed, unreachable from a parent. The card
// sizes itself to its grid cell instead; see the note above.
const { href, chip, title, level } = Astro.props;
const H = `h${level}` as 'h2' | 'h3';
---
<article class="card">
{chip && <Pill>{chip}</Pill>}
<H class="card-title">
<a class="card-link" href={href}>{title}</a>
</H>
<p class="card-body"><slot /></p>
<span class="card-arrow" aria-hidden="true">&rarr;</span>
</article>
<style>
.card {
position: relative;
/* Sizes itself to its cell — see the note on why the grid cannot do this. */
block-size: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
padding: var(--space-6);
background: var(--bg);
border: 1px solid var(--border);
border-block-start: 2px solid var(--rule);
border-radius: var(--radius-md);
transition:
border-color var(--dur-hover) var(--ease),
box-shadow var(--dur-hover) var(--ease);
}
.card:hover {
border-color: var(--accent);
box-shadow: var(--shadow-md);
}
.card-title {
font-family: var(--font-serif);
font-size: var(--text-xl);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
}
.card-link {
color: var(--text);
text-decoration: none;
}
.card-link::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-md);
}
.card:hover .card-link {
color: var(--accent);
}
/* The focus ring belongs on the CARD, not on the heading text: the hit area
is the card, so a ring around three words would point at the wrong box.
`:focus-visible` on the descendant, ring on the ancestor. */
.card:has(.card-link:focus-visible) {
outline: 2px solid var(--focus-ring);
outline-offset: var(--focus-offset);
}
.card-link:focus-visible {
outline: none;
}
.card-body {
margin: 0;
/* Pushes the arrow to the bottom edge so a row of cards aligns on it
whatever the body length. */
flex: 1 1 auto;
font-size: var(--text-base);
line-height: var(--leading-body);
color: var(--text-secondary);
}
.card-arrow {
font-size: var(--text-lg);
line-height: 1;
color: var(--accent);
transition: transform var(--dur-hover) var(--ease);
}
.card:hover .card-arrow {
transform: translateX(var(--space-2));
}
@media (prefers-reduced-motion: reduce) {
.card:hover .card-arrow {
transform: none;
}
}
</style>
+69
View File
@@ -0,0 +1,69 @@
---
/**
* docs/02: "Numbered step, timing, body."
*
* THE NUMBER IS NOT CONTENT. It is `aria-hidden` and supplied by the caller
* rather than by a CSS counter, because the compressed strip on `/` and the
* full page at `/process/` must agree on it. The reading order for assistive
* technology is heading then timing then body; the numeral adds nothing to it
* and would be read as a bare digit before every step.
*
* <ol> IS THE CALLER'S JOB. These are ordered steps, so the parent wraps them
* in an <ol> and this renders the <li>. That keeps "step 3 of 5" available from
* the list semantics instead of from the decorative numeral.
*/
interface Props {
n: number;
title: string;
/** "Day 0", "Days 17". docs/03: "Five steps with real timing." */
timing: string;
}
// No `class` prop — declared, never passed, unreachable from a parent.
const { n, title, timing } = Astro.props;
---
<li class="step">
<span class="step-n" aria-hidden="true">{String(n).padStart(2, '0')}</span>
<h3 class="step-title">{title}</h3>
<p class="step-timing">{timing}</p>
<p class="step-body"><slot /></p>
</li>
<style>
.step {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-block-start: var(--space-4);
border-block-start: 1px solid var(--border);
}
.step-n {
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: var(--tracking-wide);
/* --gold-d, not --gold: on cream, gold measures 2.10:1 and gold-d 3.11:1.
docs/02 permits gold-d for LARGE DECORATIVE text only, 24px+ — this is
12px, so neither qualifies and the numeral is maroon. Kept as a comment
because "make the step numbers gold" is the obvious next edit. */
color: var(--accent);
}
.step-title {
font-family: var(--font-serif);
font-size: var(--text-lg);
line-height: var(--leading-tight);
}
.step-timing {
margin: 0;
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--text-meta);
}
.step-body {
margin: 0;
font-size: var(--text-sm);
line-height: var(--leading-body);
color: var(--text-secondary);
}
</style>
+80
View File
@@ -0,0 +1,80 @@
---
/**
* docs/02: "Eyebrow + display heading + optional lede, one measure."
*
* THE LEVEL IS A REQUIRED DECISION, not a default. docs/02's accessibility
* floor: "One <h1> per page; heading levels never skipped." A component that
* defaulted to <h2> would silently produce an <h2> inside an <h3> section the
* first time one is nested, and nothing would fail. `level` is explicit and
* `astro check` enforces the union.
*/
import Eyebrow from './Eyebrow.astro';
interface Props {
eyebrow?: string;
/** 1 only on the page's single H1. */
level: 1 | 2 | 3;
lede?: string;
dot?: boolean;
}
/**
* NO `class` PROP, AND NO `title` PROP. Both are deletions with reasons.
*
* `class` — a parent CANNOT reach this component's root. It was passed as
* `class="section-head"` from `/` and the page's rule compiled to
* `.section-head[data-astro-cid-<page>]` while the rendered div carried
* `data-astro-cid-<SectionHeading>`. **Measured: `margin-block-end: 0px` and a
* 0px gap on all three call sites** — 48px of intended separation missing, with
* `.display`'s 0.98 line-height putting the glyphs over the top edge of the
* cards below. `astro check` and `eslint` both passed. Fourth instance of the
* defect `CLAUDE.md` records; a `class` prop here is an invitation to a fifth.
* To space this block, wrap it in an element the page owns.
*
* `title` — two mechanisms for one job. The `heading` slot is the general one
* (it takes an italic phrase, which `docs/02` allows once per headline); a
* plain-text prop is the same thing minus a capability. One way in.
*
* `dot` defaults TRUE here and FALSE on `<Eyebrow>`, deliberately: a section
* heading's eyebrow is the pattern the dot was designed for, and a bare
* `<Eyebrow>` is used in places where it would be noise.
*/
const { eyebrow, level, lede, dot = true } = Astro.props;
const H = `h${level}` as 'h1' | 'h2' | 'h3';
const size = level === 1 ? 'size-display' : 'size-section';
---
<div class="heading-block">
{eyebrow && <Eyebrow dot={dot}>{eyebrow}</Eyebrow>}
<H class:list={['display', size]}><slot name="heading" /></H>
{lede && <p class="lede">{lede}</p>}
<slot />
</div>
<style>
.heading-block {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.size-display {
font-size: var(--text-5xl);
}
.size-section {
font-size: var(--text-4xl);
}
.lede {
max-inline-size: var(--width-prose);
font-size: var(--text-lg);
line-height: var(--leading-relaxed);
color: var(--text-secondary);
}
/* `color` INHERITS across the component boundary, which is how an inverse
section reaches this — but `--text-secondary` resolves to a cream-only
value, so it has to be overridden rather than inherited. Custom properties
DO inherit, so an ancestor setting --lede-color would work too; a global
ancestor selector is fewer moving parts for one rule. */
:global(.section-inverse) .lede {
color: var(--text-inverse);
}
</style>
+15
View File
@@ -152,6 +152,21 @@ const aboutLinks = [
padding-block: var(--space-9) var(--space-6);
margin-block-start: var(--space-9);
}
/* WHEN THE LAST SECTION IS ALREADY DARK, DROP THE MARGIN. The 96px above the
footer is page rhythm against cream; where the page ends on the maroon
conversion band or an ink section it becomes a 96px CREAM STRIPE sandwiched
between two dark blocks, which reads as a layout bug rather than as air.
Found by looking at a full-page screenshot of `/`, not by reading the CSS.
`:global()` on the ancestor half is the mechanism: Astro appends this
component's cid to `.site-footer` and leaves the globalised part alone, so
the selector can reach out of the component to <main> without a parent
needing to style a child's root — the thing that cannot be done the other
way round (CLAUDE.md). */
:global(main:has(> :last-child.section-accent)) + .site-footer,
:global(main:has(> :last-child.section-inverse)) + .site-footer {
margin-block-start: 0;
}
.footer-top {
display: flex;
+36 -8
View File
@@ -32,6 +32,26 @@ const items = PRIMARY_NAV.filter(
const path = Astro.url.pathname;
const isCurrent = (href: string) => path === href;
const inSection = (href: string) => path === href || path.startsWith(href);
/**
* THE MASTHEAD TAGLINE IS SUPPRESSED ON `/`, and this closes a step-1 review
* finding rather than being a preference.
*
* SITE.tagline is `Mediation · Arbitration · Toronto`, and docs/01 specifies
* that exact string as the home HERO EYEBROW. At >=76rem the header shows the
* tagline too, so `/` opened with the same six words twice, 300px apart —
* `adversarial-reviewer` #10, left open at step 1 to "decide at step 2".
*
* The hero keeps it, because the hero is where docs/01 puts it and where there
* is copy underneath to qualify it. The masthead drops it, because the masthead
* is the placement Q33-orig objected to in the first place: a line under his
* name with nothing to qualify it, reading as a designation strip.
*
* This only ever REMOVES a claim from one page, so no §4 disclosure condition
* is touched — the footer's designation strip carries `Q.Arb — commenced
* August 2026` on every page including this one.
*/
const isHome = path === '/';
---
<header class="site-header">
@@ -40,7 +60,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
<InfinityMark size="2rem" />
<span class="brand-text">
<span class="brand-name">{SITE.name}</span>
<span class="eyebrow brand-tagline">{SITE.tagline}</span>
{!isHome && <span class="eyebrow brand-tagline">{SITE.tagline}</span>}
</span>
</a>
@@ -138,11 +158,19 @@ const inSection = (href: string) => path === href || path.startsWith(href);
the outcome. It was removed on 2026-08-26 because `Arbitration` under
Pouya's name read as a held capability, and §4 records Q.Arb as merely
commenced. Q33 answered that the same day, and the premise was wrong:
ADR designations are voluntary credentials, not licences. Anyone may be
appointed an arbitrator in Ontario, so the constraint was always
positional, never legal Pouya accepts sole, party-appointed and
co-arbitration work today. See §4 Offerings, which is where the register
now draws the line between a CREDENTIAL claim and an OFFERING. */
ADR designations are voluntary credentials, not licences, and COMMERCIAL
arbitral appointment in Ontario is not gated behind a designation — so the
constraint was always positional, never legal, and Pouya accepts sole,
party-appointed and co-arbitration work today. See §4 Offerings.
SCOPED 2026-08-27 (Q39). This comment said "Anyone may be appointed an
arbitrator in Ontario", which Pouya checked and found FALSE as a universal:
family arbitration carries prescribed training. It does not touch what this
header renders — the masthead says Mediation · Arbitration · Toronto and
family arbitration is not offered at all — but a false proposition of law
sitting in a source comment is how one reaches a page. §4's Forbidden scope
note: no file in this repo may assert as fact what the register has not
verified, internal or not. */
.brand {
display: flex;
align-items: center;
@@ -159,7 +187,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
.brand-text {
display: flex;
flex-direction: column;
gap: 2px;
gap: var(--space-05);
}
/* The tagline appears only where there is room for it — see the 76rem block.
Measured: at 11px with 0.18em tracking the string is ~285px wide, and
@@ -169,7 +197,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
same words as the hero eyebrow (docs/01). */
.brand-tagline {
display: none;
font-size: 0.6875rem; /* 11px — the eyebrow floor in docs/02 */
font-size: var(--text-2xs); /* 11px — the eyebrow floor in docs/02 */
white-space: nowrap;
}
.brand-name {
+141
View File
@@ -0,0 +1,141 @@
/**
* JSON-LD builders. Spec: docs/04-seo-spec.md §Structured data.
*
* WHY THIS FILE EXISTS. Structured data is a factual claim in machine-readable
* form, and it is the one place a claim can be wrong without any human reading
* it. AGENTS.md §4 governs it exactly as it governs visible copy — docs/04 says
* so in as many words: "Marking an unheld credential as held in structured data
* is a misrepresentation that happens to be machine-readable."
*
* So the Person node is built ONCE, here, from src/data/site.ts, and every page
* that needs it references the same @id. `/` (step 2) and `/about/` (step 3)
* would otherwise hand-type it twice.
*
* THREE THINGS ARE DELIBERATELY ABSENT. Each is a decision, not an omission:
*
* 1. `LegalService` — NEVER. docs/04: schema.org defines it as a business
* providing legal advice and *representation*, which asserts in
* machine-readable form exactly what D13 bars. `ProfessionalService`.
* 2. `worksFor` — omitted. Populating it either names the boutique (D16) or
* misstates the employer. `jobTitle` carries the role on its own.
* 3. `priceRange` — omitted until `/fees/` exists (build step 9). docs/04
* gates it on that page being real.
*
* AND Q.Arb IS NOT IN `hasCredential`. It commenced August 2026 and is not
* held. Q.Med is. That asymmetry is the whole point of the property.
*/
import { CONTACT, CREDENTIALS, ROLE, SITE } from './site';
/** Stable node ids, so pages cross-reference rather than duplicate. */
export const PERSON_ID = `${SITE.url}/about/#person`;
export const SERVICE_ID = `${SITE.url}/#practice`;
/**
* `Person`. docs/04 anchors this on /about/ and references it site-wide, which
* is why PERSON_ID points at /about/ regardless of which page emits the node.
*
* `description` IS OFFERING-SHAPED, NOT ROLE-SHAPED, and that was a correction.
* It read "Mediator and commercial arbitrator in Toronto", which `claims-auditor`
* flagged 2026-08-27: §4 verifies that he **accepts** arbitral appointments and
* separately verifies "multiple completed sole mediations" — there is **no
* counterpart row for a completed arbitration**, so "arbitrator" as a practised
* role asserted something the register does not hold.
*
* AND IT CARRIES THE Q.Arb STAGE. Same finding, and it is the sharper half:
* §4 Offerings permits the arbitration offering only while the site states the
* stage of the arc plainly — "neither half may be dropped". The VISIBLE page
* satisfied that with the fourth credential slot; this graph asserted
* arbitration twice (here and in `serviceType`) and stated the stage nowhere.
* A machine-readable claim is still a claim.
*
* `hasCredential` stays Q.Med-only regardless — the stage belongs in prose, not
* in a field that means "holds".
*
* Every clause traces: Q.Med [verified], the JD [verified], engineering
* practice [verified], Toronto [verified], the Q.Arb pathway commenced August
* 2026 [verified]. It claims no licensure and implies none — D13 bars
* implication as hard as assertion, and a crawler summary is a place where an
* implication travels unedited.
*/
export function personNode(imageUrl?: string) {
return {
'@type': 'Person',
'@id': PERSON_ID,
name: SITE.name,
url: `${SITE.url}/about/`,
jobTitle: ROLE.title,
description:
'Mediator in Toronto, accepting commercial arbitration appointments. ' +
'Q.Med designation through ADRIC and ADRIO; the Q.Arb pathway commenced ' +
'in August 2026. JD, Bond University; practising machine-learning and ' +
'infrastructure engineer.',
knowsLanguage: ['en', 'fa'],
alumniOf: { '@type': 'CollegeOrUniversity', name: 'Bond University' },
// Q.Med only. See the header comment.
hasCredential: {
'@type': 'EducationalOccupationalCredential',
name: CREDENTIALS.designations[0],
credentialCategory: 'Professional designation',
recognizedBy: [
{ '@type': 'Organization', name: 'ADR Institute of Canada' },
{ '@type': 'Organization', name: 'ADR Institute of Ontario' },
],
},
sameAs: [CONTACT.linkedin],
email: `mailto:${CONTACT.email}`,
...(imageUrl ? { image: imageUrl } : {}),
};
}
/**
* `ProfessionalService` for the home page, with the Person as `provider`.
*
* `serviceType` lists what §4 Offerings actually records as offered now —
* mediation, arbitration, med-arb. **Arbitration is scoped to commercial**
* (Q39, 2026-08-27): family arbitration in Ontario carries prescribed training
* and is separately not offered, so an unscoped "Arbitration" here would be the
* struck universal reappearing in a machine-readable field where nobody reads
* it. Do not widen these strings without a row to widen them from.
*
* No `priceRange`, no `aggregateRating`, no `review` — the last two have no
* underlying data and §4 Forbidden bars the fabricated testimonial that the
* previous site carried.
*/
export function professionalServiceNode(imageUrl?: string) {
return {
'@type': 'ProfessionalService',
'@id': SERVICE_ID,
name: `${SITE.name} — Mediation & Arbitration`,
url: `${SITE.url}/`,
description:
'Commercial mediation and arbitration for construction, technology, ' +
'energy, insurance, shareholder and cross-border disputes. Toronto, ' +
'by appointment. Q.Med held; the Q.Arb pathway commenced August 2026.',
provider: { '@id': PERSON_ID },
areaServed: [
{ '@type': 'City', name: 'Toronto' },
{ '@type': 'AdministrativeArea', name: 'Ontario' },
],
serviceType: [
'Mediation',
'Commercial arbitration',
'Mediation-arbitration (med-arb)',
],
availableLanguage: ['en', 'fa'],
email: `mailto:${CONTACT.email}`,
...(imageUrl ? { image: imageUrl } : {}),
};
}
/**
* The home page's graph: the service and the person it resolves to.
*
* A `@graph` rather than two `<script>` blocks, so `provider: {'@id': …}`
* resolves inside one document instead of relying on a crawler joining two.
*/
export function homeGraph(imageUrl?: string) {
return {
'@context': 'https://schema.org',
'@graph': [professionalServiceNode(imageUrl), personNode(imageUrl)],
};
}
+163 -13
View File
@@ -118,22 +118,48 @@ export const ROLE = {
export const CREDENTIAL_ROW = [
{ value: 'Q.Med', label: 'ADRIC / ADRIO designation' },
/**
* TODO(pouya): AGENTS.md Q37 — is "Law and engineering" the right label?
* Q37 CLOSED 2026-08-27. This label read 'Law and engineering' and it is now
* 'Legal training and engineering practice'. Pouya's reasoning, kept because
* it is the finding rather than the fix:
*
* §4's substitution principle supplies the three VALUES (`Q.Med / JD + ML /
* EN · FA`); the labels come from docs/03 and are not themselves registered.
* `claims-auditor` reads this one as a D13 implication risk: "engineering" is
* shorthand for a verified *practising* claim, so the parallel invites "Law"
* to be read the same way — and D13 bars implication as hard as assertion.
* "The parallel was doing the implying — a degree and a practice under one
* noun. The asymmetry is the honest part."
*
* Left as written, because it is docs/03's approved copy and changing
* approved copy is not an implementer's call. Nothing renders it yet. Answer
* before step 2 builds the credential row.
* A JD is a degree. Engineering is a practice, and a verified one (§4).
* Setting the two in parallel invited the reader to supply the symmetry, and
* for 'Law' the missing half is a licence — which D13 bars by implication as
* hard as by assertion.
*
* IT IS DELIBERATELY LOPSIDED AND LONGER. Do not tidy it back into a
* parallel, and do not shorten it to fit a layout; change the layout.
*/
{ value: 'JD + ML', label: 'Law and engineering' },
{ value: 'JD + ML', label: 'Legal training and engineering practice' },
{ value: 'EN · FA', label: 'Bilingual practice' },
] as const;
/**
* The FOURTH credential slot — separate on purpose, so a three-slot layout
* cannot be handed four by accident and a page has to opt in.
*
* docs/03: 'Fourth slot where the layout has one: Q.Arb — commenced August
* 2026. Use that wording, not "in progress"' — the weaker form drifts toward
* 'nearly complete', which §4 Forbidden bars outright.
*
* REQUIRED on any page that offers arbitration, not decorative. §4 Offerings
* carries a paired-disclosure condition: the site may make the offering only
* while 'stating the second plainly', and 'neither half may be dropped'. The
* footer's designation strip satisfies it site-wide; a page whose opening
* sentence says 'arbitrator' should not make the reader scroll to the footer
* for the stage.
*
* The em-dash in docs/03's string is carried by the layout (value over label),
* not by the text. Same wording, same pairing.
*/
export const CREDENTIAL_ROW_ARB = {
value: 'Q.Arb',
label: 'Commenced August 2026',
} as const; // [verified 2026-08-26 — Pouya]
/** Analytics: privacy-first and cookieless (D15). No GA4, no consent banner. */
export const ANALYTICS = {
/**
@@ -241,29 +267,98 @@ export const PRACTICE_SLUGS = [
export type PracticeSlug = (typeof PRACTICE_SLUGS)[number];
/**
* The six areas, with the one-line blurb each card renders.
*
* THE BLURBS LIVE HERE, not in the pages, because `/` and `/practice/` both
* render them and two copies of a claim-bearing sentence is one copy that will
* eventually be wrong. Same reasoning as ROLE and CREDENTIAL_ROW above.
*
* EVERY BLURB IS DISPUTE TYPES, NOT HISTORY. docs/03: "Frame as positioning,
* not as history" — 'Built to facilitate ... on Ontario's megaproject pipeline',
* never 'extensive experience resolving'. AGENTS.md §4 (Q35, 2026-08-27) makes
* that condition 2 of the publication gate for naming a practice area at all.
* A blurb that claims volume fails the gate even though the label passes.
*
* Nothing here may carry a count, a value, a settlement rate, or a superlative
* (§4 Forbidden). Dispute types are not claims of caseload.
*/
export const PRACTICE_AREAS = [
{
slug: 'construction',
name: 'Construction & Infrastructure',
chip: 'Construction',
blurb:
'Liens, delay and change-order claims, scheduling, subcontract and ' +
"deficiency disputes. Built for Ontario's megaproject pipeline.",
},
{
slug: 'technology',
name: 'Technology, AI & Data',
chip: 'Technology',
blurb:
'Software contracts, SLA and MSA breakdowns, data residency and ' +
'processing, AI vendor diligence, IP and licensing.',
},
{
slug: 'energy',
name: 'Energy, Grid & Regulatory',
chip: 'Energy',
blurb:
'Grid connection and allocation, leave-to-construct, ' +
'proponentmunicipality disputes, IESO market participation.',
},
{
slug: 'insurance',
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.
*
* 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.
*/
blurb:
'Accident benefits and SABS entitlement, MIG disputes, and private ' +
'mediation of matters before the LAT.',
},
{ slug: 'technology', name: 'Technology, AI & Data', chip: 'Technology' },
{ slug: 'energy', name: 'Energy, Grid & Regulatory', chip: 'Energy' },
{ slug: 'insurance', name: 'Insurance, SABS & LAT', chip: 'Insurance' },
{
slug: 'shareholder',
name: 'Shareholder & Family Business',
chip: 'Shareholder',
/**
* "Family Business" means COMMERCIAL disputes among family shareholders.
* The blurb says "family-held companies" and names commercial dispute types
* for that reason — AGENTS.md Q39, 2026-08-27.
*
* The explicit exclusion — family law matters are not accepted — belongs on
* the PAGE, one sentence, at build step 5 (docs/01 §/practice/shareholder/).
* Pouya scoped it there. Do not add it to this blurb: on a six-card grid it
* unbalances the row and reads defensively, and the wording here already
* makes the area unambiguously commercial.
*/
blurb:
'Shareholder and partnership disputes, co-founder breakdowns, and ' +
'business succession in family-held companies.',
},
{
slug: 'cross-cultural',
name: 'Cross-Border & Diaspora',
chip: 'Cross-cultural',
blurb:
'Diaspora business succession, dual-jurisdiction shareholder disputes, ' +
'and cross-cultural commercial matters. Conducted in English or Farsi.',
},
] as const satisfies ReadonlyArray<{
slug: PracticeSlug;
name: string;
chip: string;
blurb: string;
}>;
/**
@@ -285,6 +380,61 @@ type _SlugsWithoutAnArea = Exclude<
>;
export type _SlugCoverage = _AssertNever<_SlugsWithoutAnArea>;
/**
* The five process steps. **HERE, NOT IN THE PAGE**, for the reason written
* against PRACTICE_AREAS above and applied by `adversarial-reviewer` 2026-08-27:
* `/` renders a compressed strip of these and `/process/` renders them in full
* at build step 6, so a copy typed into one page is a copy that will eventually
* disagree with the other. The proof is already in the history — the step-3 body
* below carried a fee claim that the same session's claims audit found to be
* wrong against `docs/07`, and it existed in exactly one place. After step 6 it
* would have existed in two.
*
* 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.**
*
* 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.
* "Binding conclusion" alone would read as though a mediation binds, which it
* does not until the parties sign.
*
* NO FEE CLAIM IN ANY BODY. `docs/07` bundles a CAPPED preparation allowance
* (2 h in the half day, 3 h in the full day) and says in terms that it "must be
* stated on the page... Do not quietly fold it into the hours figure." A
* five-word strip cannot state it properly, and stating it improperly
* misdescribes money. `/fees/` at step 9.
*/
export const PROCESS = [
{
title: 'Confidential intake',
timing: 'Day 0',
body: 'A scheduled call to scope the matter, identify the parties, and run conflicts.',
},
{
title: 'Engagement and framing',
timing: 'Days 17',
body: 'Terms of appointment, the issues in dispute, and who attends.',
},
{
title: 'Pre-session exchange',
timing: 'Days 721',
body: 'Briefs and documents, exchanged in advance so the session starts informed.',
},
{
title: 'The session',
timing: 'Days 2130',
body: 'Half day or full day, in person or by video.',
},
{
title: 'Conclusion',
timing: 'Day 30 onward',
body: 'Minutes of settlement — or an award, where the process is arbitral.',
},
] as const;
/** Seven items is the ceiling before a nav stops being scannable. */
export const PRIMARY_NAV = [
{ href: '/about/', label: 'About' },
+31 -3
View File
@@ -35,9 +35,30 @@ const lang = SITE.locale.replace('_', '-');
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
{
/* Only the two faces used above the fold, per docs/02. Fonts are always
fetched in CORS mode, so a preload without `crossorigin` is a second,
wasted request rather than a warmed cache. */
/* THREE of the FOUR faces used above the fold on `/`, and the comment used
to say "the two faces used above the fold", which was wrong on the count.
Measured by network probe on a cold cache: `/` requests four faces above
the fold —
instrument-serif latin 400 normal 21,032 B preloaded — display type
geist latin variable 29,400 B preloaded — body, the LCP element
instrument-serif latin 400 italic 22,128 B preloaded — see below
geist-mono latin variable 23,128 B NOT preloaded — see below
THE ITALIC IS PRELOADED because it sits inside the `<h1>`: `<em class="it">
the room</em>`. With `font-display: swap` a CSS-discovered face renders in
a fallback first, and a serif-to-fallback swap inside a 96px headline
moves the whole last line.
GEIST MONO IS DELIBERATELY NOT. It sets the eyebrow — 12px, uppercase,
0.18em tracking — and the credential labels. A swap there costs one short
line of reflow at a size where the fallback is metrically close, and
preloading it would put 95,688 B of font on the critical path instead of
72,560 B. Revisit against real Lighthouse numbers at step 7; it is a
trade, not a certainty.
Fonts are always fetched in CORS mode, so a preload without `crossorigin`
is a second, wasted request rather than a warmed cache. */
}
<link
rel="preload"
@@ -53,6 +74,13 @@ 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
/>
{
/* No script tag. Not "no framework", not "minimal JS" — none.
+780
View File
@@ -0,0 +1,780 @@
---
/**
* `/` — Home. Build step 2 (docs/01 §Build order): "proves the design system
* end to end."
*
* JOB (docs/01 §`/`): "establish the unusual stack in under ten seconds, and
* route each of the four audiences to its surface." Leans in-house counsel.
*
* SECTIONS, against docs/01's outline:
* 1 Hero · 2 Credential row · 3 The approach · 4 Two practices ·
* 5 Practice areas · 6 Process preview · 7 Latest insights · 8 Contact band
*
* SECTION 7 IS NOT BUILT, DELIBERATELY, and this is the only spec item this
* page does not deliver. `src/content/insights/` is empty: the collection ships
* at build step 7, which is also where `ArticleCard` and the drafted slate
* arrive (docs/01 §Build order; docs/03 §Launch article slate, D9). Rendering
* the section now means importing a component with nothing to render — its
* scoped CSS ships to every visitor for an empty block — and a props surface
* with no call site, which is already an open finding against InfinityMark.
* SiteHeader gates the Insights NAV item on the same collection, so the page
* and the nav appear together. Do not "finish" this by hardcoding placeholders.
*
* EVERY FACTUAL CLAIM ON THIS PAGE TRACES TO AGENTS.md §4, and the ones that
* carry risk are constants from src/data/site.ts rather than typed here.
* Notably: arbitration is scoped to COMMERCIAL matters throughout (§4
* Offerings, Q39 2026-08-27), the Q.Arb stage is stated on the page and not
* only in the footer, and nothing claims or implies licensure (D13).
*/
import { Picture, getImage } from 'astro:assets';
import BaseLayout from '../layouts/BaseLayout.astro';
import Button from '../components/Button.astro';
import CredentialRow from '../components/CredentialRow.astro';
import Eyebrow from '../components/Eyebrow.astro';
import InfinityMark from '../components/InfinityMark.astro';
import PracticeCard from '../components/PracticeCard.astro';
import ProcessStep from '../components/ProcessStep.astro';
import SectionHeading from '../components/SectionHeading.astro';
import portrait from '../assets/pouya-lajevardi.jpg';
import ogDefault from '../assets/og-portrait.jpg';
import { homeGraph } from '../data/schema';
import {
CONTACT,
CREDENTIAL_ROW,
CREDENTIAL_ROW_ARB,
PRACTICE_AREAS,
PORTRAIT,
PROCESS,
SITE,
} from '../data/site';
/**
* FOUR SLOTS, NOT THREE, AND THE FOURTH IS NOT DECORATIVE.
*
* docs/01 §`/` item 2 says "Three slots". §4 Offerings is the higher authority
* on claims and attaches a PAIRED-DISCLOSURE CONDITION to offering arbitration
* at all: the site "makes the first while stating the second plainly", and
* "neither half may be dropped". This page says *arbitrator* in its second
* sentence, so the stage of the arc belongs on this page rather than only in
* the site footer. docs/03 authorises the fourth slot; docs/03 has been amended
* to record that on `/` it is required. Q.Arb reads as commenced — never as
* held or nearing completion (§4 Forbidden).
*/
const credentials = [...CREDENTIAL_ROW, CREDENTIAL_ROW_ARB];
/**
* docs/04 lists `image` on the Person node. This generates it with EXACTLY the
* transform SEO.astro already applies to the same source — jpeg, 1200 x 630 —
* so Astro's asset cache returns the same hashed file rather than emitting a
* second copy for the crawler's benefit. Verified by asset count: dist holds
* one `og-portrait.*.jpeg` with this line present, not two.
*
* JPEG on purpose, for the same reason SEO.astro uses it: link-preview and
* structured-data consumers are not browsers and several still do not decode
* WebP, let alone AVIF.
*/
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = homeGraph(new URL(ldImage.src, Astro.site).href);
---
<BaseLayout
title={`${SITE.name} · Mediation & Arbitration · Toronto`}
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}
>
{/* ---- 1. Hero ------------------------------------------------------- */}
<section class="hero">
<div class="wrap hero-inner">
<div class="hero-copy">
{
/* docs/01 specifies this exact string as the hero eyebrow. The
masthead no longer repeats it on this page — see SiteHeader. */
}
<Eyebrow dot>{SITE.tagline}</Eyebrow>
{
/* docs/03 §Approved headline options, option 1 — the recommended one.
Option 3 is struck there: "every side" asserts having acted as
party, as counsel and as neutral, and §4 verifies one of the three.
The italic is the single flourish docs/02 allows in a headline. */
}
<h1 class="display hero-h">
A mediator who reads the contract, the code, and <em class="it"
>the room</em
>.
</h1>
{
/* TWO CORRECTIONS FROM `claims-auditor`, 2026-08-27, both about
implication rather than assertion — which is where D13 says the risk
actually lives.
(a) This read "I mediate and arbitrate commercial disputes". §4
verifies that he ACCEPTS arbitral appointments, and separately
verifies "multiple completed sole mediations" — there is no
counterpart row for a completed arbitration. Present-indicative
"arbitrate" beside "mediate" invites the reader to supply a track
record for both. The offering-shaped form is what the register
actually holds, and it is already the form the arbitration card
below uses.
(b) "facts most neutrals take on faith" is a COMPARATIVE assertion
about a population of third parties. 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. */
}
<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.
</p>
<div class="hero-cta">
<Button href="/contact/" variant="primary"
>Request a consultation &rarr;</Button
>
<Button href="/process/" variant="ghost">How I work</Button>
</div>
</div>
{
/* `widths` + `sizes` rather than `densities`: the portrait is fluid, and
a density ladder would size it from one assumed CSS width. The largest
real render is ~476px (content 1280 - 96 gutter - 64 gap, x 0.425),
so 960 is the 2x ceiling.
`width` AND `height` ARE PASSED ALONGSIDE `widths`, AND THAT IS NOT
REDUNDANT. With `widths` alone, Astro emits the UNTOUCHED 1600px
master as the <img src> fallback — measured 254,626 bytes sitting in
dist as the declared fallback for a 476px slot. Passing width/height
pins it to the 960 variant instead (78,665 B), and the 1600px file
stops being generated at all. Same defect InfinityMark's comment
records for a missing `width`, in the one shape that survives passing
`widths`. Verified by reading the emitted `src` attribute and that
file's real dimensions — not the build log, which reported "before:
349kB" for every variant either way.
What a browser actually takes is the AVIF: 5.6 / 7.3 / 11.1 / 14.8 /
21.5 kB across the five widths [measured 2026-08-27].
eager + fetchpriority=high because this is the LCP candidate on the
page docs/04 budgets hardest. */
}
<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"
formats={['avif', 'webp']}
fallbackFormat="jpeg"
alt={PORTRAIT.alt}
loading="eager"
decoding="sync"
fetchpriority="high"
class="portrait-img"
/>
</div>
</div>
</section>
{/* ---- 2. Credential row --------------------------------------------- */}
<section class="wrap credential-band" aria-label="Credentials">
<CredentialRow slots={credentials} />
</section>
{
/* NO `aria-labelledby` ON THE CONTENT SECTIONS, and that is the fix for an
inconsistency rather than a removal of information.
Four of them carried one and this one did not, which put five named
regions plus header / nav / main / footer / footer-nav in a screen
reader's region list — eleven entries for a marketing page — with the only
unnamed content section being the odd one out. A `<section>` without an
accessible name is not exposed as a region at all, and the visible `<h2>`s
already give heading navigation, which is how a reader moves through a
page like this.
THE ONE EXCEPTION IS THE CREDENTIAL BAND, which has no visible heading, so
`aria-label` is the only thing that can name it. The rule is: name a region
only where it has no heading of its own. */
}
{/* ---- 3. The approach ----------------------------------------------- */}
<section class="section section-inverse approach reveal">
<div class="wrap approach-inner">
<div class="approach-copy">
<SectionHeading eyebrow="The approach" level={2}>
<span slot="heading">Two directions at once.</span>
</SectionHeading>
{
/* THIS PARAGRAPH USED TO OPEN "Law and engineering are not blended
here", and `claims-auditor` flagged it as Q37's struck parallel
relocated from the credential label into prose — a degree and a
practice under one noun, one day after Pouya struck exactly that
construction. It is a fair reading and the fix is the same fix:
make the two halves asymmetric. What a neutral does with a contract
is read it; what an engineer does is engineering. Neither sentence
now sets "Law" beside "engineering" as two instances of one thing.
`docs/01` §`/` item 3 and `docs/03` §Home both specify this section
as "the 'two directions at once' argument — law and engineering
converging on the same dispute", so the ARGUMENT is unchanged and
still delivered; only the construction that carried the implication
is gone. Whether Q37's reasoning formally extends to prose is
Pouya's to say — Q41. */
}
<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.
</p>
<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.
</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.
</p>
<p class="approach-metaphor">
My mark is an infinity loop, and it is the argument in one line:
disputes are loops. The work is redrawing the loop into a line.
</p>
</div>
</div>
{
/* Decorative: the paragraph beside it says what it is, so an
accessible name here would be read twice. Measured on this ground —
the ribbon's champagne half carries the silhouette against ink, the
maroon half against cream; it reads on both. */
}
<div class="approach-mark">
{
/* width=232 and loading=lazy are both measured, not defaults.
`size` caps at 9rem tall, so the mark renders at up to 144 x 225.5
CSS px — five times the header's 50.1px, and the component's default
64px ladder tops out at 192px. Measured upscale before this: 1.17x
at DPR 1, 2.35x at DPR 2, 3.52x at DPR 3. 232 with densities
[1,2,3] gives 232/464/696, and 696 covers the 676 device px a DPR-3
screen asks for. Lazy because this sits roughly a screen and a half
down; the component defaults to eager for the masthead. */
}
<InfinityMark
size="clamp(4rem, 14vw, 9rem)"
width={232}
loading="lazy"
/>
</div>
</div>
</section>
{/* ---- 4. Two practices --------------------------------------------- */}
<section class="section practices reveal">
<div class="wrap">
{
/* The id goes on a span INSIDE the h2 via the `heading` slot, so
`aria-labelledby` names the visible heading. The first version passed
`title=` and additionally rendered a `.visually-hidden` span carrying
the id, which put the same words in the accessibility tree twice —
found in the heading-outline dump, not by reading the source. */
}
{
/* THE WRAPPER IS THE FIX, NOT DECORATION. This was
`<SectionHeading class="section-head">`, and the page's rule compiled
to `.section-head[data-astro-cid-<page>]` while the rendered root
carried SectionHeading's own cid — so it never matched. Measured:
`margin-block-end: 0px` and a 0px gap to the cards on all three call
sites, with `.display`'s 0.98 line-height putting the glyphs over the
card's top edge. `astro check` 0 errors, `eslint` clean, and the source
looked right. Fourth instance of this on the project; SectionHeading no
longer accepts a `class` at all, and passing one is now a build
error. */
}
<div class="section-head">
<SectionHeading eyebrow="What I do" level={2}>
<span slot="heading">Two processes.</span>
</SectionHeading>
</div>
<div class="pair">
<article class="feature">
<h3 class="feature-title">
<a href="/mediation/">Mediation</a>
</h3>
{
/* THE FEE CLAIM IS GONE, and it was wrong on two counts —
`claims-auditor`, 2026-08-27, checked against `docs/07-fees.md`.
"at one published rate" reads as ONE PRICE for half and full day;
D14's card sets TWO ($2,000 half, $4,000 full). And "preparation
time included" was unqualified where `docs/07` bundles a CAPPED
allowance and says in terms: *"must be stated on the page —
'including 2 hours of preparation' ... Do not quietly fold it into
the hours figure. At these rates, saying preparation is included
is the selling point, not a footnote."*
A home card is the wrong place to state it properly, and stating
it improperly misdescribes money. `/mediation/` (step 4) and
`/fees/` (step 9) carry the card. "Published" was also
forward-looking: `/fees/` does not exist yet. */
}
<p class="feature-body">
Sole mediator, Q.Med through ADRIC and ADRIO, with multiple
completed sole mediations. Half day or full day, in person or by
video.
</p>
<span class="feature-arrow" aria-hidden="true">&rarr;</span>
</article>
<article class="feature">
<h3 class="feature-title">
<a href="/arbitration/">Arbitration</a>
</h3>
{
/* docs/03's model sentence, and BOTH halves are required: §4
Offerings — "neither half may be dropped". Pouya's instruction:
being open about the stage is the differentiator, so it is not
hedged into vagueness and not dropped. Commercial matters only
(Q39): family arbitration is not offered. */
}
<p class="feature-body">
I accept sole, party-appointed and co-arbitration appointments in
commercial matters. The Q.Arb pathway commenced in August 2026;
C.Med-Arb is the endpoint.
</p>
<span class="feature-arrow" aria-hidden="true">&rarr;</span>
</article>
</div>
{
/* docs/01 §`/` item 4: "Med-Arb named here as the long-term arc,
linking to /med-arb/." It has had its own §4 Offerings row since
2026-08-27 (Q35), so it is named as offered rather than only as an
aspiration — but the arc is what docs/01 asks this page to carry. */
}
<p class="pair-note">
<strong>Med-Arb</strong> combines the two: one neutral mediates, then arbitrates
whatever has not settled. I accept those appointments, and C.Med-Arb is the
designation endpoint. The page on it meets the procedural-fairness objection
head on rather than around it &mdash;
<a href="/med-arb/">how med-arb works &rarr;</a>
</p>
</div>
</section>
{/* ---- 5. Practice areas -------------------------------------------- */}
{
/* docs/01: "the most important block on the page for search, because it
distributes authority to the pages that can actually rank." */
}
<section class="section section-alt areas reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="Practice areas"
level={2}
lede="Six areas, chosen because the disputes in them turn on documents I can read without an intermediary."
>
<span slot="heading">Where the work is.</span>
</SectionHeading>
</div>
{
/* reveal-stagger is capped at six children by design (docs/02) and there
are exactly six. A seventh would land with the sixth, not break. */
}
<div class="area-grid reveal-stagger">
{
PRACTICE_AREAS.map((area) => (
<PracticeCard
href={`/practice/${area.slug}/`}
chip={area.chip}
title={area.name}
level={3}
>
{area.blurb}
</PracticeCard>
))
}
</div>
{
/* THIS SAID "All six areas, and what else is offered", which asserted
offerings beyond the six. 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. */
}
<p class="areas-more">
<a href="/practice/">All six practice areas &rarr;</a>
</p>
</div>
</section>
{/* ---- 6. Process preview ------------------------------------------- */}
<section class="section process reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="How it runs"
level={2}
lede="Five steps, from the first call to the conclusion. Counsel want the shape of the downside before they commit a client's day."
>
<span slot="heading">From first call to conclusion.</span>
</SectionHeading>
</div>
<ol class="process-strip" role="list">
{
PROCESS.map((step, i) => (
<ProcessStep n={i + 1} title={step.title} timing={step.timing}>
{step.body}
</ProcessStep>
))
}
</ol>
<p class="process-more">
<a href="/process/">What happens if the matter does not settle &rarr;</a
>
</p>
</div>
</section>
{/* ---- 7. Latest insights: NOT BUILT AT STEP 2. See the header note. -- */}
{/* ---- 8. Contact band ---------------------------------------------- */}
{
/* 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. */
}
<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>
</BaseLayout>
<style>
/* --- 1. Hero -------------------------------------------------------- */
.hero {
padding-block: var(--space-8) var(--space-9);
}
.hero-inner {
display: grid;
gap: var(--space-7);
align-items: center;
}
.hero-copy {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.hero-h {
/* --text-6xl is the display ceiling. At 360px it is 52px and the headline
runs four lines; `text-wrap: balance` (global.css) keeps them even. */
font-size: var(--text-6xl);
max-inline-size: 22ch;
}
.hero-lede {
max-inline-size: var(--width-prose);
font-size: var(--text-lg);
line-height: var(--leading-relaxed);
color: var(--text-secondary);
}
.hero-cta {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin-block-start: var(--space-2);
}
/* The 4:5 crop is CSS, not a build step: Astro's image service does not crop,
and cropping offline would mean committing a second derived binary on top
of the ~3.16 MB of brand and portrait masters already in the repo. The cost
is that a browser downloads the full square and shows 80% of it; the source
is 1600 x 1600 and the largest variant generated is 960 wide. */
.hero-portrait {
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: var(--radius-lg);
background: var(--bg-raised);
}
/* The <picture> wrapper is the box that gets sized, NOT the <img> — the exact
defect CLAUDE.md records for <Button> and then for <Picture> inside
InfinityMark. `class="portrait-img"` lands on the <img>, so the <img> rule
below is reached via Astro's cid on the emitted element, and the wrapper is
targeted by the bare `picture` selector, which the markup does carry. */
.hero-portrait :global(picture) {
display: block;
block-size: 100%;
}
.portrait-img {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
/* Above centre: the subject's head is in the upper half of a square crop. */
object-position: 50% 22%;
}
@media (min-width: 66rem) {
.hero {
padding-block: var(--space-9);
}
.hero-inner {
/* 1.15 / 0.85 — the copy column carries a 22ch headline and a 68ch lede;
an even split starves the headline and leaves the portrait oversized. */
grid-template-columns: 1.15fr 0.85fr;
gap: var(--space-8);
}
}
/* --- 2. Credential row ---------------------------------------------- */
.credential-band {
/* No .section wrapper: the row owns its own padding-block and border, and
stacking --section-y on top would put 160px of air around a 4-line band. */
padding-block-end: var(--space-4);
}
/* --- 3. The approach ------------------------------------------------ */
.approach-inner {
display: grid;
gap: var(--space-7);
align-items: center;
}
.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);
}
.approach-metaphor {
padding-block-start: var(--space-5);
border-block-start: 1px solid var(--rule);
font-family: var(--font-serif);
font-size: var(--text-xl);
line-height: var(--leading-snug);
color: var(--text-inverse-2);
}
.approach-mark {
display: flex;
justify-content: center;
}
@media (min-width: 66rem) {
.approach-inner {
grid-template-columns: 1fr auto;
gap: var(--space-9);
}
/* Mark second in the DOM and second visually. No `order` anywhere on this
page: reordering flex or grid items puts focus order out of step with
visual order, which is what WCAG 2.4.3 and docs/02 both forbid, and it
already cost a header rebuild at step 1. */
}
/* --- 4. Two practices ---------------------------------------------- */
.section-head {
margin-block-end: var(--space-7);
}
.pair {
display: grid;
gap: var(--space-5);
}
.feature {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
/* CLAMPED, not a flat --space-7. At a 200% default font size (root 32px)
`3rem` is 96px a side — 192px of horizontal padding inside a 342px
container, which was most of the 83px residual overflow
`adversarial-reviewer` measured. The vw term keeps it at 48px on any real
viewport and lets it collapse when the rem is doubled. */
padding: clamp(var(--space-5), 4vw, var(--space-7));
background: var(--bg-raised);
border-radius: var(--radius-lg);
transition: background-color var(--dur-hover) var(--ease);
}
.feature:hover {
background: var(--cream-2);
}
.feature-title {
font-family: var(--font-serif);
font-size: var(--text-3xl);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
/* `overflow-wrap: break-word` (global.css) permits a break at layout time
but does NOT reduce min-content size, so "Arbitration" at a 60px
--text-3xl held the card open. `anywhere` does reduce it. Only reachable
at a large default font size; at every real size the word never breaks. */
overflow-wrap: anywhere;
}
.feature-title a {
color: var(--text);
text-decoration: none;
}
/* One link per card, hit area the whole card, accessible name the heading —
same pattern and same reasoning as PracticeCard. */
.feature-title a::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-lg);
}
.feature:hover .feature-title a {
color: var(--accent);
}
.feature:has(a:focus-visible) {
outline: 2px solid var(--focus-ring);
outline-offset: var(--focus-offset);
}
.feature-title a:focus-visible {
outline: none;
}
.feature-body {
flex: 1 1 auto;
max-inline-size: 46ch;
color: var(--text-secondary);
}
.feature-arrow {
font-size: var(--text-xl);
line-height: 1;
color: var(--accent);
}
.pair-note {
max-inline-size: var(--width-prose);
margin-block-start: var(--space-6);
color: var(--text-secondary);
}
@media (min-width: 56rem) {
.pair {
grid-template-columns: 1fr 1fr;
gap: var(--space-6);
}
}
/* --- 5. Practice areas --------------------------------------------- */
.area-grid {
display: grid;
/* auto-fit with an 18rem floor: 1 up on a phone, 2 up on a tablet, 3 up on
a desktop, with no breakpoint of its own. The cards size themselves to
the cell (see PracticeCard) — a parent cannot reach a child's root.
`min(18rem, 100%)`: a bare rem floor is a hard minimum, so at a large
default font size (root 32px) 18rem becomes 576px and the track will not
shrink. Measured at root 200%: this grid overflowed a 390px viewport by
234px. See CredentialRow for the same guard and the full reasoning. */
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
gap: var(--space-5);
}
/* THESE TWO LINKS STAND ALONE ON THEIR OWN LINE, so docs/02's 44px touch
floor applies to them in full. Measured before this rule: 250.8 x 18 and
287.9 x 18 — the paragraph's line box and nothing more.
`inline-flex` + min-block-size rather than padding, so the 44px IS the hit
area rather than visual air, and `inline-size: fit-content` keeps the
target the width of the words instead of the whole measure.
The INLINE link in `.pair-note` ("how med-arb works") is deliberately NOT
given this treatment: it sits mid-sentence at 164 x 21, and WCAG 2.5.8
exempts a target "in a sentence or its size is otherwise constrained by the
line-height of non-target text". Padding it out would break the paragraph's
leading to satisfy a rule that does not apply to it. A deviation from
docs/02's flat wording, taken deliberately and recorded rather than left to
look like an oversight.
The card heading links measure 26-39px tall and are NOT a finding: each
card's whole box is the link's hit area via `::after { inset: 0 }`. Verified
by hit-testing nine points per card at 390 / 768 / 1280px — 24 cards, 9/9
inside the link every time. The width sweep flagged them because it
measured the <a>'s own box, which is not the target. */
.areas-more,
.process-more {
margin-block-start: var(--space-6);
font-size: var(--text-sm);
}
.areas-more a,
.process-more a {
display: inline-flex;
align-items: center;
inline-size: fit-content;
min-block-size: 44px;
}
/* --- 6. Process preview -------------------------------------------- */
.process-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
gap: var(--space-5);
margin: 0;
}
/* --- 8. Contact band ----------------------------------------------- */
.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;
}
</style>
-652
View File
@@ -1,652 +0,0 @@
---
/**
* TEMPORARY — build step 1 only. Delete when step 2 ships `/`.
*
* A proof sheet for the design system. It exists so the type scale, the
* palette, the spacing rhythm, the focus states, and the header and footer can
* be looked at in a browser before nineteen real pages are built on top of them.
*
* It carries `noindex` and is excluded from the sitemap in astro.config.mjs.
* Remove that filter entry when you remove this page.
*
* WHAT THIS PAGE MAY CONTAIN: body copy about CSS, and nothing else. The page
* is published — `noindex` suppresses indexing, not access — so AGENTS.md §4
* governs every string on it exactly as it governs `/about/`.
*
* An earlier version of this comment read "NOTHING HERE ASSERTS A FACT ABOUT
* POUYA." It was false when written, and it is recorded here as the caution:
* the page was at that moment carrying a four-stage service timeline with day
* counts and two unverified assertions about the previous build, and the claims
* audit found them *because* a comment had certified the file clean and nobody
* re-read it. A certification is not a check — CLAUDE.md, "a sweep is a
* command, not a claim". Audit the page; do not trust this paragraph.
*
* The contrast numbers are COMPUTED, not transcribed. The frontmatter reads
* src/styles/tokens.css, pulls the hex values out of it, and runs the WCAG 2.x
* relative-luminance formula over them. Restating docs/02's measured table here
* would have created a second copy of a fact — and the copy that goes stale is
* the one nobody re-reads. This one cannot drift from the tokens, because it is
* derived from them.
*/
import BaseLayout from '../../layouts/BaseLayout.astro';
import Button from '../../components/Button.astro';
import InfinityMark from '../../components/InfinityMark.astro';
// `?raw` rather than fs.readFileSync: `import.meta.url` is rewritten to the
// emitted chunk's own location during the build, so a relative path resolves
// against dist/.prerender/chunks/ and throws ENOENT. Vite resolves this at
// bundle time, in dev and in build alike.
import tokensCss from '../../styles/tokens.css?raw';
const hex: Record<string, string> = {};
for (const match of tokensCss.matchAll(
/--([a-z0-9-]+):\s*(#[0-9a-f]{6})\b/gi,
)) {
hex[match[1]] = match[2].toLowerCase();
}
// The parser above only understands 6-digit hex. Move a palette token to
// `oklch()`, `color-mix()` or 3-digit hex and the lookup silently yields
// undefined; the luminance maths then throws an unnamed TypeError with no clue
// which token did it. Fail here instead, naming them — the posture SEO.astro
// takes with an out-of-range title.
const REQUIRED_TOKENS = [
'cream',
'ink',
'ink-soft',
'muted',
'maroon',
'maroon-l',
'gold',
'gold-d',
'gold-l',
];
const unparsed = REQUIRED_TOKENS.filter((token) => !hex[token]);
if (unparsed.length > 0) {
throw new Error(
'/type-scale/ could not read these colour tokens as 6-digit hex from ' +
`src/styles/tokens.css: ${unparsed.map((t) => `--${t}`).join(', ')}. ` +
'The contrast table on this page is computed from them. If the palette ' +
'moved to another colour syntax, teach this parser that syntax — do not ' +
'hardcode the values back in.',
);
}
const typeScale: Array<{ token: string; value: string }> = [];
for (const match of tokensCss.matchAll(
/--(text-(?:xs|sm|base|lg|xl|\dxl)):\s*([^;]+);/g,
)) {
typeScale.push({ token: `--${match[1]}`, value: match[2].trim() });
}
/** WCAG 2.x relative luminance and contrast ratio. */
const channel = (v: number) =>
v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
const luminance = (value: string) => {
const n = Number.parseInt(value.slice(1), 16);
return (
0.2126 * channel(((n >> 16) & 255) / 255) +
0.7152 * channel(((n >> 8) & 255) / 255) +
0.0722 * channel((n & 255) / 255)
);
};
const contrast = (a: string, b: string) => {
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
};
const verdict = (ratio: number) =>
ratio >= 4.5 ? 'AA body' : ratio >= 3 ? 'AA large only' : 'fails both';
const foregrounds = [
'ink',
'ink-soft',
'maroon',
'maroon-l',
'muted',
'gold-d',
'gold',
];
const onCream = foregrounds.map((token) => ({
token,
value: hex[token],
ratio: contrast(hex[token], hex.cream),
}));
const onInk = ['cream', 'gold-l', 'gold', 'muted'].map((token) => ({
token,
value: hex[token],
ratio: contrast(hex[token], hex.ink),
}));
const spacing: Array<{ token: string; value: string }> = [];
for (const match of tokensCss.matchAll(/--(space-\d+):\s*([^;]+);/g)) {
spacing.push({ token: `--${match[1]}`, value: match[2].trim() });
}
const title = 'Type scale and colour proof sheet · Pouya Lajevardi';
const description =
'A temporary reference page rendering the design system end to end: the fluid type scale, the palette with computed contrast ratios, spacing, and motion.';
---
<BaseLayout title={title} description={description} noindex>
<div class="wrap">
<section class="section">
<p class="eyebrow">
<span class="dot"></span>Build step 1 · temporary page
</p>
<h1 class="display size-5xl">
The system, <span class="it">rendered</span>.
</h1>
<p class="lede">
Everything below is read out of <code>src/styles/tokens.css</code> at build
time. The contrast ratios are computed from the token values, not copied from
the spec — if a token changes, this page changes with it.
</p>
<p class="note">
This page is <strong>temporary</strong>. It is <code>noindex</code>, it
is excluded from the sitemap, and build step 2 deletes it.
</p>
</section>
<hr class="rule-gold" />
<section class="section">
<p class="eyebrow"><span class="dot"></span>Display type</p>
<h2 class="display size-3xl">
Instrument Serif, <span class="it">4xl and up</span>
</h2>
<p class="note">
docs/02: display type is used at <code>--text-4xl</code> and above only. It
has almost no hinting at small sizes and looks weak below 32&nbsp;px.
</p>
<div class="stack">
<p class="display size-6xl">Redraw the loop</p>
<p class="display size-5xl">Redraw the loop into a line</p>
<p class="display size-4xl">Redraw the loop into a line</p>
</div>
</section>
<hr />
<section class="section">
<p class="eyebrow"><span class="dot"></span>Type scale</p>
<h2 class="display size-3xl">Fluid, 360&nbsp;px to 1600&nbsp;px</h2>
<p class="note">
Resize the window. Every step interpolates; there are no breakpoint
jumps. The declaration beside each line is the token itself.
</p>
<dl class="scale-list">
{
typeScale.map((step) => (
<div class="scale-row">
<dt>
{/* A neutral specimen, deliberately. This was
"Mediation · Arbitration · Toronto" — hand-typed rather than
read from SITE.tagline, and rendered ten times at up to 96px
on a published page. Two defects in one string: a claim
typed inline is a claim nobody re-checks against §4 (the
rule SiteFooter states), and `Arbitration` unqualified at
display size is the open question AGENTS.md Q33 names. A
type specimen needs letterforms, not assertions. */}
<span class="sample" style={`font-size: var(${step.token})`}>
Handgloves &amp; quartz — 1234567890
</span>
</dt>
<dd>
<code>{step.token}</code>
<span class="scale-value">{step.value}</span>
</dd>
</div>
))
}
</dl>
</section>
<hr />
<section class="section">
<p class="eyebrow"><span class="dot"></span>Colour on cream</p>
<h2 class="display size-3xl">Measured, not assumed</h2>
<p class="note">
Ratios computed at build time against <code>--cream</code>
{' '}{hex.cream}. AA is 4.5:1 for body text and 3:1 for large text.
</p>
<ul class="swatches" role="list">
{
onCream.map((entry) => (
<li class="swatch">
<span class="chip" style={`background: ${entry.value}`} />
<span class="swatch-meta">
<code>--{entry.token}</code>
<span class="swatch-value">{entry.value}</span>
<span class="swatch-ratio">
{entry.ratio.toFixed(2)}:1 · {verdict(entry.ratio)}
</span>
</span>
</li>
))
}
</ul>
<div class="gold-demo">
<p class="note">
<strong>The hard rule, shown without breaking it.</strong> Gold is a rule,
a border, and an icon stroke. The identical hue fails at
{contrast(hex.gold, hex.cream).toFixed(2)}:1 as text on cream and
passes at {contrast(hex.gold, hex.ink).toFixed(2)}:1 on ink — the
colour is not the problem, the pairing is. Both blocks below are the
same <code>--gold</code>.
</p>
{
/* Shown as blocks, not as sentences. An earlier version set a real
paragraph in --gold on --bg-raised, which measures 1.76:1 — worse
than the 2.10:1 the rule exists to prevent. A reader with low vision
got an unreadable paragraph and no alternative, and the one artefact
documenting the rule was the one artefact breaking it. */
}
<div class="gold-pair">
<div class="gold-block on-cream">
<span class="gold-band"></span>
<span class="gold-caption">
<code>--gold</code> on <code>--cream</code>
<span class="swatch-ratio">
{contrast(hex.gold, hex.cream).toFixed(2)}:1 · never as text
</span>
</span>
</div>
<div class="gold-block on-ink">
<span class="gold-band"></span>
<span class="gold-caption">
<code>--gold</code> on <code>--ink</code>
<span class="swatch-ratio">
{contrast(hex.gold, hex.ink).toFixed(2)}:1 · text is fine here
</span>
</span>
</div>
</div>
</div>
</section>
<hr />
<section class="section">
<p class="eyebrow"><span class="dot"></span>Components</p>
<h2 class="display size-3xl">Buttons, mark, focus</h2>
<p class="note">
Tab through this section. Every interactive element takes a visible
focus ring — 2&nbsp;px maroon, 3&nbsp;px offset, on
<code>:focus-visible</code>. Never <code>outline: none</code> without a replacement.
</p>
<div class="row">
<Button href="/contact/" variant="primary"
>Request a consultation</Button
>
<Button href="/process/" variant="ghost">How I work</Button>
<Button href="/about/" variant="gold">Read the record</Button>
<Button type="button">A real &lt;button&gt;</Button>
</div>
<div class="row marks">
<InfinityMark size="4rem" />
<InfinityMark size="2.5rem" />
<InfinityMark size="1.5rem" />
</div>
</section>
<hr />
<section class="section">
<p class="eyebrow"><span class="dot"></span>Measure</p>
<h2 class="display size-3xl">Sixty-eight characters</h2>
{
/* `.prose` is opted into, not inherited. global.css no longer caps every
<p> on the site — long-form asks for the measure, and this block is
demonstrating the very thing it uses. */
}
<div class="prose">
<p>
Body copy is capped at <code>--width-prose</code>, 68&nbsp;ch. Past
roughly 75 characters the eye starts losing the beginning of the next
line on the return sweep, and long-form copy stops being read. This
paragraph sits at the cap. It is the single largest lever on whether a
page of prose is actually finished, and it costs nothing to set.
</p>
<p>
Line height is 1.6 for body and 0.98 for display. Headings balance
with
<code>text-wrap: balance</code>, so a two-line headline breaks evenly
rather than leaving one word stranded.
</p>
</div>
</section>
<hr />
<section class="section">
<p class="eyebrow"><span class="dot"></span>Spacing</p>
<h2 class="display size-3xl">Eight-pixel base</h2>
<ul class="spacing-list" role="list">
{
spacing.map((step) => (
<li class="spacing-row">
<code>{step.token}</code>
<span
class="spacing-bar"
style={`inline-size: var(${step.token})`}
/>
<span class="spacing-value">{step.value}</span>
</li>
))
}
</ul>
</section>
</div>
<section class="section section-inverse">
<div class="wrap">
<p class="eyebrow"><span class="dot"></span>Colour on ink</p>
<h2 class="display size-3xl">The dark panel</h2>
<p class="note-inverse">
<code>--muted</code> is the trap: it passes on cream at 5.47:1 and fails on
ink. Secondary text on dark is <code>--gold-l</code> or cream.
</p>
<ul class="swatches" role="list">
{
onInk.map((entry) => (
<li class="swatch">
<span class="chip" style={`background: ${entry.value}`} />
<span class="swatch-meta">
<code>--{entry.token}</code>
<span class="swatch-value">{entry.value}</span>
<span class="swatch-ratio">
{entry.ratio.toFixed(2)}:1 · {verdict(entry.ratio)}
</span>
</span>
</li>
))
}
</ul>
</div>
</section>
<div class="wrap">
<section class="section">
<p class="eyebrow"><span class="dot"></span>Motion</p>
<h2 class="display size-3xl">Reveal on section entry</h2>
<p class="note">
The four cards below carry <code>.reveal-stagger</code>. Scroll them
into view. With JavaScript disabled, or with reduced motion requested,
they are simply present — the animation is layered onto a page that
already reads.
</p>
{
/* Deliberately contentless. These four cards previously carried the
engagement timeline — which IS specified, at docs/01-architecture.md
§/process/, for the `/process/` page. It does not belong on a proof
sheet for a CSS stagger, where nothing qualifies it and no reader is
looking for it. The stagger demonstrates just as well with nothing to
say. */
}
<ul class="cards reveal-stagger" role="list">
<li class="card"><h3>First card</h3><p>Stagger step 1</p></li>
<li class="card"><h3>Second card</h3><p>Stagger step 2</p></li>
<li class="card"><h3>Third card</h3><p>Stagger step 3</p></li>
<li class="card"><h3>Fourth card</h3><p>Stagger step 4</p></li>
</ul>
</section>
</div>
</BaseLayout>
<style>
.size-6xl {
font-size: var(--text-6xl);
}
.size-5xl {
font-size: var(--text-5xl);
}
.size-4xl {
font-size: var(--text-4xl);
}
.size-3xl {
font-size: var(--text-3xl);
}
.lede {
max-inline-size: var(--width-prose);
margin-block-start: var(--space-5);
font-size: var(--text-lg);
color: var(--text-secondary);
}
.note,
.note-inverse {
max-inline-size: var(--width-prose);
margin-block-start: var(--space-4);
font-size: var(--text-sm);
}
.note {
color: var(--text-meta);
}
.note-inverse {
color: var(--text-inverse-2);
}
h2.display {
margin-block-start: var(--space-3);
}
code {
font-family: var(--font-mono);
font-size: 0.9em;
color: var(--text-secondary);
}
.stack {
display: flex;
flex-direction: column;
gap: var(--space-5);
margin-block-start: var(--space-6);
}
/* No `max-inline-size: none` escape hatch any more — there is nothing left to
escape from now that the measure is opt-in. */
/* --- Type scale ---------------------------------------------------------- */
.scale-list {
margin-block-start: var(--space-6);
}
.scale-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: var(--space-4);
padding-block: var(--space-4);
border-block-start: 1px solid var(--border);
}
.scale-row dt,
.scale-row dd {
/* A flex item's default `min-width: auto` is its min-content width, which
for a 96px specimen is one whole word. */
min-inline-size: 0;
margin: 0;
}
.scale-row dd {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: end;
}
.scale-value {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--text-meta);
}
.sample {
line-height: var(--leading-tight);
/* At --text-6xl a single word is ~500px wide, so at 320px it has to break
mid-word or push the page out. Measured: 8px of overflow before this,
which now has no `body { overflow-x: hidden }` to hide it — which is
exactly why that clamp was removed. */
overflow-wrap: anywhere;
}
/* --- Colour -------------------------------------------------------------- */
.swatches {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
gap: var(--space-5);
margin-block-start: var(--space-6);
padding: 0;
list-style: none;
}
.swatch {
display: flex;
align-items: center;
gap: var(--space-4);
}
.chip {
inline-size: 3.5rem;
block-size: 3.5rem;
flex: none;
border-radius: var(--radius-md);
border: 1px solid var(--border);
}
.swatch-meta {
display: flex;
flex-direction: column;
gap: 2px;
}
.swatch-value,
.swatch-ratio {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--text-meta);
}
.section-inverse .swatch-value,
.section-inverse .swatch-ratio,
.section-inverse code {
color: var(--text-inverse-2);
}
.section-inverse .chip {
border-color: var(--line-dark);
}
.gold-demo {
margin-block-start: var(--space-7);
padding: var(--space-5);
background: var(--bg-raised);
border-radius: var(--radius-lg);
}
.gold-pair {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
gap: var(--space-4);
margin-block-start: var(--space-5);
}
.gold-block {
padding: var(--space-4);
border-radius: var(--radius-md);
}
.gold-block.on-cream {
background: var(--cream);
border: 1px solid var(--border);
}
.gold-block.on-ink {
background: var(--ink);
}
/* The colour itself, carried by a block rather than by glyphs. */
.gold-band {
display: block;
block-size: var(--space-6);
background: var(--gold);
border-radius: var(--radius-sm);
}
.gold-caption {
display: flex;
flex-direction: column;
gap: 2px;
margin-block-start: var(--space-3);
font-size: var(--text-sm);
}
.gold-block.on-cream .gold-caption {
color: var(--text);
}
.gold-block.on-ink .gold-caption,
.gold-block.on-ink code,
.gold-block.on-ink .swatch-ratio {
color: var(--text-inverse);
}
/* --- Rows ---------------------------------------------------------------- */
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-4);
margin-block-start: var(--space-6);
}
.marks {
color: var(--accent);
gap: var(--space-6);
}
/* --- Spacing ------------------------------------------------------------- */
.spacing-list {
margin-block-start: var(--space-6);
padding: 0;
list-style: none;
}
.spacing-row {
display: grid;
/* minmax(0, …) on every track. A grid item's default `min-width: auto` is
its min-content size, so the 160px bar below forced the row wider than
the viewport — 72px of overflow at 320px, with three cells pushed off
screen and unreachable now that body{overflow-x:hidden} is gone from
global.css. WCAG 1.4.10 Reflow. */
grid-template-columns: minmax(0, 6rem) minmax(0, 1fr) minmax(0, 3.5rem);
align-items: center;
gap: var(--space-3);
padding-block: var(--space-2);
}
.spacing-bar {
block-size: 0.75rem;
max-inline-size: 100%;
background: var(--rule);
border-radius: var(--radius-sm);
}
.spacing-value {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--text-meta);
text-align: end;
}
/* --- Cards --------------------------------------------------------------- */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: var(--space-4);
margin-block-start: var(--space-6);
padding: 0;
list-style: none;
}
.card {
padding: var(--space-5);
background: var(--bg-raised);
border-radius: var(--radius-lg);
}
.card h3 {
font-size: var(--text-lg);
letter-spacing: var(--tracking-tight);
}
.card p {
margin-block-start: var(--space-2);
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--text-meta);
}
</style>
+38 -1
View File
@@ -237,7 +237,7 @@ a:hover {
:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 3px;
outline-offset: var(--focus-offset);
border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) {
@@ -297,6 +297,43 @@ a:hover {
.section-inverse .text-meta {
color: var(--text-inverse-2);
}
/* The conversion band. Maroon rather than ink so it reads as an action and not
as a second footer — the real footer is ink and sits directly beneath it.
Cream on maroon measures 12.29:1; gold-l on maroon 8.11:1 (docs/02). */
.section-accent {
background: var(--accent);
color: var(--text-inverse);
}
.section-accent .eyebrow,
.section-accent .text-meta {
color: var(--text-inverse-2);
}
/* Both inverse families need a focus ring that is visible ON them: the default
ring is --maroon, which is 1.00:1 against the accent band's own background
and 1.21:1 against ink. Gold-l measures 8.11:1 on maroon and 11.09:1 on ink.
Without this the ring exists and cannot be seen, which is the same failure as
not having one. */
.section-inverse :focus-visible,
.section-accent :focus-visible {
outline-color: var(--gold-l);
}
/* The eyebrow's dot is a --accent (maroon) box, so recolouring only the TEXT
for an inverse ground leaves the dot at 1.21:1 on ink and 1.00:1 on the
accent band — present in the markup, invisible on the page. Colour never
carries meaning alone here (the dot is decorative and aria-hidden), so this
is a design defect rather than a WCAG one; it is still a mark nobody can see.
Measured 2026-08-27. */
.section-inverse .eyebrow .dot,
.section-accent .eyebrow .dot {
background: var(--text-inverse-2);
}
/* Pill reads its colours from custom properties, which are the one thing that
crosses Astro's component-scope boundary (they inherit). See Pill.astro. */
.section-inverse,
.section-accent {
--pill-border: var(--line-dark);
--pill-fg: var(--text-inverse-2);
}
hr {
border: none;
+13 -1
View File
@@ -51,6 +51,10 @@
--rule: var(--gold);
--border: var(--line);
--focus-ring: var(--maroon);
/* docs/02: "outline: 2px solid var(--maroon); outline-offset: 3px". The
offset was written as a literal in global.css and then in two components,
which is three places to forget. */
--focus-offset: 3px;
/* --- Type -------------------------------------------------------------- */
@@ -59,7 +63,10 @@
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
/* Fluid scale, 360px → 1600px viewport. Ratio widens toward the display
end (1.25 → 1.333) so headlines scale harder than body copy. */
end (1.25 → 1.333) so headlines scale harder than body copy.
--text-2xs is the eyebrow floor docs/02 sets at 11px. Added 2026-08-27:
SiteHeader wrote `0.6875rem` as a literal, step-1 review finding #7. */
--text-2xs: 0.6875rem; /* 11 — eyebrow */
--text-xs: 0.75rem; /* 12 — legal */
--text-sm: 0.875rem; /* 14 — meta */
--text-base: 1rem; /* 16 — body */
@@ -90,6 +97,11 @@
/* --- Space — 8px base -------------------------------------------------- */
/* 2 — a hairline gap, below the 4px base step. It exists because the
two-line brand block needs a gap smaller than --space-1 without the header
growing; step-1 review finding #7 flagged the literal. Do not reach for it
as a general step: the 8px base starts at --space-1. */
--space-05: 2px;
--space-1: 0.25rem; /* 4 */
--space-2: 0.5rem; /* 8 */
--space-3: 0.75rem; /* 12 */