feat: build steps 7a-10 — the site is complete and reviewable at 22 pages

Steps 7a through 10 as one authorised run. Nothing deployed (D11).

7a  Lighthouse returns as `lighthouse@13.4.1` + `chrome-launcher`, NOT
    `@lhci/cli`. AGENTS.md §7's advisory attribution was wrong: the carriers
    were @lhci/cli's own `tmp` and @puppeteer/browsers' `extract-zip`, not
    Lighthouse, which audits clean. A deliberate deviation from R11's literal
    trigger, recorded with what it costs. Local gate; CI has no Chrome.

7b  OG card generator (satori + sharp) discharges R15 — 20 typed cards plus
    per-article cards; the portrait stays on / and /about/ by Q40. Insights
    plumbing: ArticleCard, Prose, the index, the article route, articleGraph,
    and /'s section 7. Card copy is constrained structurally because text in a
    JPEG cannot be grepped by check:claims: every headline IS its page's <h1>,
    enforced by `npm run og:proof`.

7c  Five drafted launch articles, draft: true / reviewedByPouya: false. An
    independent compliance audit returned 76 findings and 57 unsourced
    assertions; all blocking and should-fix applied.

8   /contact/, the intake form, and backend/intake/ (undeployed). Plain HTML
    POST to a same-origin /api/intake with a 303 redirect, so the form works
    with zero JavaScript. docs/05 records three deliberate deviations.

9   /fees/ on Q59's ruling — overtime runs from the session cap, and the
    reservation point ships adjacent to the rate. One-page PDF bio discharges
    R16; /bio/ is its source, so the circulated artefact stays inside the
    review apparatus.

10  /legal/privacy/ and /legal/terms/, written to the backend as built. Three
    of the policy's statements are derived and cannot drift.

Also: /about/'s inverse credentials band (approved at step 6); Q59 closed;
R15 and R16 discharged; and a fix to shipped copy — /practice/energy/ asserted
the absence of a regulation the source extract says must not be asserted.

Review: adversarial-reviewer, two rounds (D20/D19). Round 1 returned 16
findings including two blocking — an invisible ghost button on /fees/ at
1.00:1 that Lighthouse scored 100, and a privacy policy that named one data
processor when there are two. All 16 acted on.

Lighthouse, 22 pages, mobile: performance 99-100, accessibility 100,
best practices 100, SEO 100 on every indexable page, CLS 0.000.

AGENTS.md entry (ah) has the detail, including four of my own verification
commands that were wrong and what each of them nearly caused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-08-31 10:56:54 -04:00
co-authored by Claude Opus 5
parent 6cfe69033f
commit 210bc25a26
53 changed files with 8589 additions and 177 deletions
+395
View File
@@ -0,0 +1,395 @@
---
/**
* `/bio/` — the one-page bio, and the SOURCE of the PDF. Build step 9.
* Discharges `AGENTS.md` R16 / Q45.
*
* ⚠️ **R16 LEFT TWO DECISIONS OPEN AND BOTH ARE TAKEN HERE, UNDER STANDING
* AUTHORISATION. Read them before changing anything.**
*
* **(a) Generated at build, or authored once as a designed artefact?** Neither,
* exactly — and the third option is better than both. The bio is a PAGE in this
* repository, so every line of it is reviewed by the same apparatus that reviews
* every other page: `astro check`, `npm run check:claims` on the built HTML, the
* adversarial review, and the cutover claims pass. The PDF is then RENDERED from
* this page by `npm run bio:pdf`, deterministically, with no new dependency —
* `chrome-launcher` is already a devDependency because Lighthouse needs it.
*
* That answers R16's actual worry, which was never about tooling: *"It is the
* one artefact class this project's review apparatus cannot reach. A web page is
* re-reviewed by every audit; a PDF circulated with an appointment proposal is
* read once, by the reader who matters most, and never seen by a reviewer
* again."* Making the PDF a rendering of a reviewed page puts it back inside the
* apparatus. **It is not generated during `astro build`** — CI has no Chrome, and
* a build step that cannot run in CI is the Q22 shape again.
*
* **(b) Does it carry anything the site does not? NO.** Every line here renders
* from the same constants as the pages: `CREDENTIALS`, `ROLE`, `BOUTIQUE`,
* `PRACTICE_AREAS`, `FEES`, `CONTACT`. R16 flagged that both open sub-decisions
* were "each a §4 question of its own, and the matter list would collide with §4
* Forbidden directly" — so the answer that avoids both is a bio that adds
* nothing. No matter list, no referees, no figure that is not on `/fees/`. The
* fee summary IS here, because R16's own reasoning says an appointment proposal
* needs the rate card as much as the bio, and every figure in it is `/fees/`'s.
*
* `noindex`, and excluded from the sitemap in `astro.config.mjs`: it is a
* condensed duplicate of `/about/` and `/fees/`, and two URLs competing on the
* same content is the one thing `docs/04` is most concerned with.
*
* PRINT LAYOUT. `global.css`'s `@media print` block already hides the header,
* the footer and the skip link, neutralises the inverse grounds, and disables
* the reveal — all of it added because `/about/` is printed by people evaluating
* an appointment. This page adds only what makes it fit ONE sheet, and
* `scripts/bio-pdf.mjs` ASSERTS the page count rather than trusting it.
*/
import BaseLayout from '../layouts/BaseLayout.astro';
import Eyebrow from '../components/Eyebrow.astro';
import { getImage } from 'astro:assets';
import ogDefault from '../assets/og-portrait.jpg';
import { pageGraph } from '../data/schema';
import {
BOUTIQUE,
CONTACT,
CREDENTIALS,
FEES,
PRACTICE_AREAS,
ROLE,
SITE,
} from '../data/site';
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
const money = (amount: number) =>
new Intl.NumberFormat('en-CA', {
style: 'currency',
currency: FEES.currency,
maximumFractionDigits: 0,
}).format(amount);
const { halfDay, fullDay } = FEES.mediation;
/* The processes, each with a §4 Offerings row. Arbitration is scoped commercial
because Q39's gate is a legal one; mediation is unscoped because it has no
such gate (Q56). The asymmetry is designed — do not tidy it. */
const PROCESSES = [
'Mediation — sole mediator',
'Commercial arbitration — sole, party-appointed, co-arbitration',
'Med-arb — mediation converting to binding arbitration, agreed in advance',
'Early neutral evaluation — delivered to both parties together',
'Dispute-system design',
'Pre-dispute technical advisory',
];
---
<BaseLayout
title="One-Page Bio · Pouya Lajevardi · Mediator · Toronto"
description="A one-page record for circulation with an appointment proposal: designations, education, memberships, the processes offered, the practice areas, and the rates."
jsonLd={graph}
noindex
>
<section class="section bio-sheet">
<div class="wrap">
{
/* The download sits above the sheet and is `.no-print`, so the printed
copy does not carry a link to itself. */
}
<p class="no-print sheet-note">
This page is the source of the one-page PDF.
<a href="/pouya-lajevardi-bio.pdf" download>Download the PDF</a>, or
print this page.
</p>
<header class="sheet-head">
{
/* THE EYEBROW IS `.no-print`, AND IT IS HERE BECAUSE `og:proof` ASKED
FOR IT. That check compares each card's eyebrow against its page's
first `.eyebrow`, and this page had none — so the card said "Bio"
against nothing. The options were to weaken the check or to give the
page the element every other page has; weakening a check to match an
artefact is how a control stops controlling. On paper the sheet leads
with the name, so the eyebrow prints away. */
}
<div class="no-print">
<Eyebrow dot>Bio</Eyebrow>
</div>
<h1 class="sheet-name">{SITE.name}</h1>
<p class="sheet-desigs">{CREDENTIALS.designations.join(' · ')}</p>
<p class="sheet-strap">{SITE.tagline}</p>
</header>
<div class="sheet-grid">
<section class="block block-wide">
<h2>The practice</h2>
<p>
{
/* ⚠️ NO LEADING SCOPE. This sentence read "I act as a neutral in
commercial disputes — as a mediator, as an arbitrator in
commercial matters, and in med-arb…", and the leading clause
scoped ALL THREE, mediation included. §4's mediation row is
unscoped deliberately (Q56) and says in terms: "do not scope it
on a page." It is the `/practice/` shape exactly — the two words
never appear in the same element, so no proximity grep reaches
it — and it was found by reading the rendered PDF. The scope
belongs on the arbitration clause alone, where Q39's legal gate
puts it. */
}
I act as a neutral — as a mediator, as an arbitrator in commercial matters,
and in med-arb where the parties want one neutral across both phases.
I read the contract and the technical record underneath it rather than
either side's summary of them.
</p>
<p>
I am {ROLE.title} at {BOUTIQUE}, with {ROLE.litigationLine} across
{' '}{ROLE.litigationAreas.join(', ')} matters. I am also a practising
machine-learning and infrastructure engineer, which is what lets me work
through a technical record at first hand.
</p>
</section>
<section class="block">
<h2>Designations</h2>
<ul role="list">
{CREDENTIALS.designations.map((d) => <li>{d}</li>)}
</ul>
<h2>Education</h2>
<ul role="list">
{CREDENTIALS.education.map((d) => <li>{d}</li>)}
</ul>
<h2>Certifications</h2>
<ul role="list">
{CREDENTIALS.certifications.map((d) => <li>{d}</li>)}
</ul>
</section>
<section class="block">
<h2>Memberships</h2>
<ul role="list">
{CREDENTIALS.memberships.map((d) => <li>{d}</li>)}
</ul>
<h2>Languages</h2>
<ul role="list">
<li>
{CREDENTIALS.languages.join(' and ')}, without an interpreter
</li>
</ul>
</section>
<section class="block">
<h2>Processes</h2>
<ul role="list">
{PROCESSES.map((p) => <li>{p}</li>)}
</ul>
</section>
<section class="block">
<h2>Subject matter</h2>
<ul role="list">
{PRACTICE_AREAS.map((area) => <li>{area.name}</li>)}
</ul>
<p class="fine">
Family arbitration under the <em>Family Law Act</em> is not offered.
</p>
</section>
<section class="block block-wide">
<h2>Rates</h2>
<ul role="list" class="rates-list">
<li>
Half day, up to {halfDay.hours} hours of session — {
money(halfDay.amount)
}. Fee includes up to {halfDay.prepIncluded} hours of preparation.
</li>
<li>
Full day, up to {fullDay.hours} hours of session — {
money(fullDay.amount)
}. Fee includes up to {fullDay.prepIncluded} hours of preparation.
</li>
<li>
Each party beyond two — {money(FEES.mediation.additionalParty)}.
Overtime beyond the session hours the fee covers —
{' '}{money(FEES.mediation.overtimePerHour)} an hour.
{' '}{FEES.mediation.reservation}
</li>
<li>
Arbitration — {money(FEES.arbitration.perHour)} an hour,
{' '}{money(FEES.arbitration.hearingDay)} a hearing day, or a flat fee
for documents-only and expedited references.
</li>
<li>
{FEES.taxNote} The full card, the cancellation schedule and the terms
are published at {SITE.url}/fees/.
</li>
</ul>
</section>
<section class="block block-wide sheet-contact">
<h2>Contact</h2>
<p>
{CONTACT.email} · {CONTACT.phoneFallback} · {CONTACT.location}
<br />
{CONTACT.responseTime} · {SITE.url} · {CONTACT.linkedin}
</p>
</section>
</div>
</div>
</section>
</BaseLayout>
<style>
.sheet-note {
margin-block-end: var(--space-7);
font-size: var(--text-sm);
color: var(--text-meta);
}
.bio-sheet {
padding-block: var(--space-8);
}
.sheet-head {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding-block-end: var(--space-5);
border-block-end: 2px solid var(--rule);
}
.sheet-name {
font-family: var(--font-serif);
font-size: var(--text-4xl);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
}
.sheet-desigs {
margin-block-start: var(--space-3);
font-family: var(--font-mono);
font-size: var(--text-sm);
letter-spacing: var(--tracking-wide);
color: var(--accent);
}
.sheet-strap {
margin-block-start: var(--space-2);
font-family: var(--font-mono);
font-size: var(--text-2xs);
letter-spacing: var(--tracking-eyebrow);
text-transform: uppercase;
color: var(--text-meta);
}
.sheet-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
gap: var(--space-6);
margin-block-start: var(--space-6);
}
.block-wide {
grid-column: 1 / -1;
}
.block h2 {
font-family: var(--font-mono);
font-size: var(--text-2xs);
letter-spacing: var(--tracking-eyebrow);
text-transform: uppercase;
color: var(--text-meta);
padding-block-end: var(--space-2);
border-block-end: 1px solid var(--border);
}
.block h2:not(:first-child) {
margin-block-start: var(--space-5);
}
.block ul {
/* `global.css` strips the marker and padding from `ul[role='list']`. */
margin-block-start: var(--space-3);
display: grid;
gap: var(--space-2);
font-size: var(--text-sm);
line-height: var(--leading-snug);
}
.block p {
margin-block-start: var(--space-3);
font-size: var(--text-sm);
line-height: var(--leading-body);
color: var(--text-secondary);
max-inline-size: var(--width-prose);
}
.block p + p {
margin-block-start: var(--space-3);
}
.rates-list {
max-inline-size: none;
}
.fine {
font-size: var(--text-xs);
}
.sheet-contact p {
font-family: var(--font-mono);
font-size: var(--text-xs);
line-height: var(--leading-relaxed);
max-inline-size: none;
}
/* --- One sheet of paper ------------------------------------------------ */
/* `global.css`'s print block already hides the header, footer and skip link,
neutralises the inverse grounds and disables the reveal. This is only what
makes the content FIT, and `scripts/bio-pdf.mjs` asserts the page count
rather than this comment claiming it. */
@media print {
.bio-sheet {
padding-block: 0;
}
.wrap {
max-inline-size: none;
padding-inline: 0;
}
.sheet-grid {
/* Two fixed columns rather than auto-fit: on paper there is no viewport
to fit to, and a print UA resolves `auto-fit` against the sheet width
inconsistently. */
grid-template-columns: 1fr 1fr;
gap: 10pt 18pt;
margin-block-start: 10pt;
}
.sheet-name {
font-size: 22pt;
}
.sheet-desigs {
font-size: 9pt;
}
.sheet-strap {
font-size: 7pt;
}
.sheet-head {
padding-block-end: 8pt;
}
.block h2 {
font-size: 7pt;
padding-block-end: 3pt;
}
.block h2:not(:first-child) {
margin-block-start: 9pt;
}
.block ul,
.block p {
margin-block-start: 5pt;
font-size: 8.5pt;
line-height: 1.35;
}
.block ul {
gap: 2pt;
}
.sheet-contact p {
font-size: 8pt;
}
/* A block must not be split across a page break — on a one-sheet document
that would mean a second sheet carrying two lines. */
.block {
break-inside: avoid;
}
}
</style>