Files
adr-sml/src/pages/fees.astro
T
Pouya LajevardiandClaude Opus 5 02739adac9
Build and deploy / build-and-deploy (push) Failing after 4s
fix: refute (ar)'s intake finding; fix the D20 gloss class; add X-Robots-Tag on *.pdf
Pouya's rulings of 2026-09-03, in five parts.

1. THE INTAKE FORM IS NOT BROKEN. (ar) was wrong. docs/09 §7.1 verbatim —
POST /api/intake with an Origin header — returns 303 to /contact/could-not-send/
with access-control-allow-origin echoed; the same probe without Origin returns
403. A bare POST 403s BY DESIGN and §7.1 says so three lines below the probe it
prescribes: "403 means the Origin header did not arrive". The earlier finding
read a status code without reading the document that defines it. Second time in
two days. CLAUDE.md's instrument list goes eight to nine. D20 findings 12 and 19
fall with it; §7.2 (that both emails arrive) is still owed.

The correction is APPENDED as entry (as); (ar) stands unedited.

2. The privacy retention comment was stale, not a defect — superseded by his
decision to publish and confirm after launch, reading from 2026-09-04. Reworded;
the TODO(pouya) came off with the gate it enforced. The mechanism finding
survives: it was a JSX comment, stripped by Astro, so no build or deploy path
could see it. A publication gate that lives only in a stripped comment is not a
gate. §9 Q60 corrected.

3. The gloss class is fixed — 15 of the 20 D20 findings, 14 distinct edits across
9 files, under the rule "the gloss may say no more than the extract says; no new
claims, no new sources". Swept three unpublished insights drafts too, and
corrected the wrong CAA attribution at its source in docs/reference/, which is
where a fixed page re-seeds. /bio/ changed, so the committed PDF is regenerated
(89,549 B, 1 page asserted). Three findings outstanding: 10 needs a ruling, 11 is
ruled and owed via Q60, 13 needs him to have said it. R1 is not one of the twenty.

4. X-Robots-Tag cannot be done with S3 object metadata — --metadata writes user
metadata, returned as x-amz-meta-x-robots-tag, which no crawler reads. Built as
the CloudFront response-headers policy docs/06 has specified all along:
configure.mjs section 4. It needs a --apply run, not a deploy. The policy is
cloned from whatever is attached at run time and reconciled on every run, because
a response-headers policy replaces rather than merges.

5. Headshot deferred as an open non-defect. The master and the srcset ladder are
both fine; Astro passes no quality, so AVIF encodes at sharp's default 50 and is
served first.

Two review rounds, 29 findings, all resolved, none declined; stopped at two per
D19. NINE of round 2's fourteen were defects in round 1's own repairs — including
a fix that harmonised both /fees/ rows onto wording that was itself unregistered,
publishing an unsourced fee term twice where it had been once.

Gates, exit status read for each: check 0 (0 errors, 0 warnings, 0 hints),
build 0 (23 pages), check:claims 0, check:intake 0, og:proof 0, lint 0, minifier
grep exit 1, router.test.mjs 30/30. Lighthouse NOT run. Nothing deployed and
nothing applied to the distribution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
2026-09-03 17:23:20 -04:00

477 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
/**
* `/fees/` — build step 9. Spec: docs/01 §`/fees/`, docs/03 §Fees,
* **docs/07-fees.md is the authority on every number here.**
*
* ⚠️ **EVERY FIGURE IS INTERPOLATED FROM `FEES`. NOT ONE IS TYPED.** This is the
* page D8 commits to and the one where a hand-typed number would be an incorrect
* price rather than an untidy fact. `docs/07` §All parameters confirmed sets the
* publication rules the interpolation has to satisfy, and two of them are
* wording rather than value:
*
* 1. **The preparation allowance is CAPPED and must READ as capped** —
* *"including **up to** 2 hours of preparation"*. Never "including 2 hours",
* which sells an entitlement, and never "preparation included", which sells
* an uncapped one. `/for-parties/` shipped the flat form for one pass, on the
* one page written for a reader with no counsel to catch it.
* 2. **The session cap and the preparation allowance are DIFFERENT THINGS with
* different nouns** — `hours` is time in the session, `prepIncluded` is
* preparation bundled into the fee. Folding them into one figure is the
* ambiguity Q58 was opened to fix, and it was an ambiguity in `docs/07`
* itself rather than in any copy.
*
* ✅ **Q59 IS RULED AND THIS PAGE IS WHY IT MATTERED — Pouya, 2026-08-31.**
* Overtime runs from the **session cap**: the fourth hour of a half day, the
* seventh of a full day. Until that ruling this page could not publish the
* $500 rate at all, because a rate printed beside "up to 3 hours" defines its
* own trigger by adjacency and there was no other quantity for it to attach to.
*
* ⚠️ **AND THE RULING'S SECOND HALF IS NOT DECORATION — IT IS WHAT KEEPS THE
* PAGE FROM READING AS AN ARITHMETIC MISTAKE.** `FEES.mediation.reservation`:
* *a full day reserves the day; half-day overtime is subject to availability.*
* Without it a reader adds up `2000 + 500 × 3 = 3500` against `4000` and
* concludes the full-day rate is simply worse — which is a real feature of D14's
* figures (§12 R5 carries it, with the table in `docs/07` §Recorded dissent) and
* is answered by what the full-day fee actually buys. So the reservation
* sentence ships **adjacent to the overtime row**, not in a footnote. Same
* structural rule as `PROCESS_FRAMING` beside the five timings under Q43.
*
* ⚠️ **NO TRIBUNAL-SECRETARY RATE AND NO SETTLEMENT COUNSEL.** Both are struck
* rows in §4 Offerings — the first removed by Pouya from D14, the second by him
* as his own error in `docs/01`. **A rate on a fee page is an offer**, which is
* exactly why they are struck here rather than merely unpriced.
*
* ⚠️ **ARBITRATION IS SCOPED COMMERCIAL, MEDIATION IS NOT.** The asymmetry is
* designed (Q39, Q56): family arbitration in Ontario carries prescribed training
* and is separately NOT OFFERED, so the scope on the arbitration rows is a legal
* gate. Mediation has no equivalent gate and is unscoped on purpose. A later
* editor tidying these into a matching pair would reintroduce the defect.
*/
import BaseLayout from '../layouts/BaseLayout.astro';
import Button from '../components/Button.astro';
import ContactBand from '../components/ContactBand.astro';
import Eyebrow from '../components/Eyebrow.astro';
import SectionHeading from '../components/SectionHeading.astro';
import { getImage } from 'astro:assets';
import ogDefault from '../assets/og-portrait.jpg';
import { pageGraph } from '../data/schema';
import { FEES } from '../data/site';
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
/* No `Service` node, and no `Offer` node either. The Person alone — the
`/practice/` and `/process/` shape. `/mediation/` and `/arbitration/` already
carry the `Service` nodes for what is priced here, and a second one on this
path would put a duplicate `@id` in the graph. An `Offer` with `price` would
be the obvious addition and is deliberately not made: schema.org's `Offer`
models a single price for a single item, and every row below is conditional on
session length, party count and format — a machine-readable $2,000 with none
of those conditions attached is a worse claim than no claim.
⚠️ AND `/`'s NODE CARRIES NO PRICE EITHER — this comment said
`ProfessionalService.priceRange` "carries the range instead", and that field
was removed the same day for mixing units and understating its own floor.
Nothing on this site states a price in machine-readable form, deliberately:
every figure here is conditional on session length, party count or format,
and a number without those conditions is a worse claim than no number.
Found by `adversarial-reviewer` round 2 — a justification resting on a field
that no longer exists is how an `Offer` node gets added by the next reader. */
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 two mediation rows, built from the constants so the noun and the "up to"
travel with the number rather than being retyped beside it. */
const MEDIATION_ROWS = [
{
item: `Half day — up to ${halfDay.hours} hours of session`,
detail: `Fee includes up to ${halfDay.prepIncluded} hours of preparation.`,
fee: money(halfDay.amount),
},
{
item: `Full day — up to ${fullDay.hours} hours of session`,
detail: `Fee includes up to ${fullDay.prepIncluded} hours of preparation.`,
fee: money(fullDay.amount),
},
{
item: 'Each party beyond two',
detail: 'Per party, added to the session fee.',
fee: money(FEES.mediation.additionalParty),
},
{
item: 'Overtime, per hour',
/* Q59: the trigger is the SESSION cap, and the reservation point ships in
the same cell as the rate. See the header for why it is not a footnote. */
detail: `Charged beyond the ${halfDay.hours} or ${fullDay.hours} session hours the fee covers. ${FEES.mediation.reservation}`,
fee: money(FEES.mediation.overtimePerHour),
},
];
const ARBITRATION_ROWS = [
{
item: 'Hourly',
detail: 'Procedural work, document review, award writing.',
fee: money(FEES.arbitration.perHour),
},
{
item: 'Hearing day',
detail: 'In person or by video, at the same rate.',
fee: money(FEES.arbitration.hearingDay),
},
{
item: 'Documents-only or expedited — simple',
detail: 'Flat fee.',
fee: money(FEES.arbitration.documentsOnlySimple),
},
{
item: 'Documents-only or expedited — complex',
detail: 'Flat fee.',
fee: money(FEES.arbitration.documentsOnlyComplex),
},
];
---
<BaseLayout
title="Fees · Mediation and Arbitration Rates · Pouya Lajevardi"
description="The full rate card: half-day and full-day mediation, arbitration, cancellation terms and what an overrun costs. Published in full, with no ranges."
jsonLd={graph}
>
{/* ---- 1. Hero -------------------------------------------------------- */}
<section class="section hero">
<div class="wrap">
<Eyebrow dot>Fees</Eyebrow>
<h1 class="display hero-h">
Published in full, including what overruns cost.
</h1>
{
/* ⚠️ THIS SENTENCE QUOTED A PHRASE THE SPEC BARS, AND THE QUOTATION WAS
THE PROBLEM. It read: *No ranges, no "starting from", and nothing that
has to be asked for.* Two defects in one clause. (1) It defines the
page against an unnamed practice — an implied comparative, which Q41(b)
answers: assert his capability, never the field's. (2) It plants the
literal string `starting from` in `dist/`, where a future sweep for
`docs/03`'s "no 'starting from' evasions" would hit it and read a
negation as a breach — the `I aLSO practise` / `the pLEADINGs` shape,
manufactured on purpose by the copy. Stating what the page DOES needs
no comparison and leaves nothing to trip over. */
}
<p class="hero-lede">
One rate for all mediation matters, whatever the subject. Every figure
is on this page, and none of it has to be asked for. {FEES.taxNote}
</p>
</div>
</section>
{/* ---- 2. Mediation --------------------------------------------------- */}
<section class="section section-alt reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="Mediation"
level={2}
lede="One rate for every matter. Preparation is bundled into the fee and is capped."
>
<span slot="heading">Half day or full day.</span>
</SectionHeading>
</div>
<dl class="rates">
{
MEDIATION_ROWS.map((row) => (
<div class="rate">
<dt>
<span class="rate-item">{row.item}</span>
<span class="rate-detail">{row.detail}</span>
</dt>
<dd>{row.fee}</dd>
</div>
))
}
</dl>
</div>
</section>
{/* ---- 3. Arbitration ------------------------------------------------- */}
<section class="section reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="Arbitration"
level={2}
lede="Sole, party-appointed and co-arbitration appointments, in commercial matters."
>
<span slot="heading">Hourly, by hearing day, or flat.</span>
</SectionHeading>
</div>
<dl class="rates">
{
ARBITRATION_ROWS.map((row) => (
<div class="rate">
<dt>
<span class="rate-item">{row.item}</span>
<span class="rate-detail">{row.detail}</span>
</dt>
<dd>{row.fee}</dd>
</div>
))
}
</dl>
{
/* The Q39 scope, stated on the page rather than left to the lede. §4
Offerings carries a NOT OFFERED row for family arbitration, and
`/practice/shareholder/` makes the same exclusion in one sentence on
Pouya's instruction — "one sentence, not a section", because a
disclaimer that grows reads as defensive. */
}
<p class="scope-note">
Family arbitration under the <em>Family Law Act</em> is not offered, and family
law matters are not accepted.
</p>
</div>
</section>
{/* ---- 4. Other services ---------------------------------------------- */}
<section class="section section-alt reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="Also offered"
level={2}
lede="Charged hourly, with an estimate agreed in the terms of appointment."
>
<span slot="heading">Three things beside the two processes.</span>
</SectionHeading>
</div>
<dl class="rates">
<div class="rate">
<dt>
<span class="rate-item">Early neutral evaluation</span>
{
/* §4's ENE row and `docs/01` §`/practice/` both require this
framing, and `docs/07` repeats it for this page in terms:
"nothing on `/fees/` may read as a rate for advising one of
them." ENE is the offering nearest §4's NOT-NEGOTIABLE boundary,
because a neutral assessment of the merits sits closest to
providing legal services. */
}
<span class="rate-detail">
A reasoned assessment of the merits, delivered to both parties
together. Never advice to one of them.
</span>
</dt>
<dd>{money(FEES.hourly)}<span class="per"> / hour</span></dd>
</div>
<div class="rate">
<dt>
<span class="rate-item">Dispute-system design</span>
<span class="rate-detail">
Advising an organisation on how its future disputes should be
handled, before there are any.
</span>
</dt>
<dd>{money(FEES.hourly)}<span class="per"> / hour</span></dd>
</div>
<div class="rate">
<dt>
<span class="rate-item">Pre-dispute technical advisory</span>
{
/* THE CONFLICT CAUTION IS NOT OPTIONAL. §4's row: "no copy may
imply the offering is free of that tension", and it names
`/practice/`'s strip as where the temptation would arise. A fee
page is the second such place, because a priced line reads as a
product. Advisory work for one organisation can conflict against
a later appointment in the same matter. */
}
<span class="rate-detail">
Technical review before a dispute exists. Taking it on can rule me
out of a later appointment in the same matter, and that is settled
in writing before the work starts.
</span>
</dt>
<dd>{money(FEES.hourly)}<span class="per"> / hour</span></dd>
</div>
</dl>
</div>
</section>
{/* ---- 5. Cancellation ------------------------------------------------ */}
<section class="section reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading
eyebrow="Cancellation"
level={2}
lede="A reserved date is time that cannot be given to another matter. The schedule is published so it is never a surprise."
>
<span slot="heading">If a date is cancelled.</span>
</SectionHeading>
</div>
<dl class="rates">
{
FEES.cancellation.map((row) => (
<div class="rate">
<dt>
<span class="rate-item">{row.window}</span>
</dt>
<dd class="dd-text">{row.fee}</dd>
</div>
))
}
</dl>
<ul class="notes" role="list">
{FEES.cancellationNotes.map((note) => <li>{note}</li>)}
</ul>
</div>
</section>
{/* ---- 6. Terms -------------------------------------------------------- */}
<section class="section section-inverse reveal">
<div class="wrap">
<div class="section-head">
<SectionHeading eyebrow="Terms" level={2}>
<span slot="heading">How the account works.</span>
</SectionHeading>
</div>
<ul class="notes" role="list">
<li>{FEES.taxNote}</li>
{FEES.terms.map((term) => <li>{term}</li>)}
<li>
Travel outside the Greater Toronto Area is billed separately, or
bundled at a day rate stated in the terms of appointment.
</li>
<li>
Everything above is confirmed in the terms of appointment before an
engagement begins. Nothing on this page is an appointment.
</li>
</ul>
<div class="cta">
<Button href="/contact/" variant="gold"
>Request a consultation &rarr;</Button
>
<Button href="/process/" variant="ghost"
>How an engagement runs &rarr;</Button
>
</div>
</div>
</section>
<ContactBand />
</BaseLayout>
<style>
.hero {
padding-block-start: var(--space-9);
}
.hero-h {
margin-block: var(--space-4) var(--space-5);
font-size: var(--text-6xl);
}
.hero-lede {
max-inline-size: 58ch;
font-size: var(--text-lg);
line-height: var(--leading-body);
color: var(--text-secondary);
}
/* --- The rate rows ---------------------------------------------------- */
/* A `<dl>`, not a `<table>`. Each row is one item and its price — a
term-and-value pair — and a two-column table of eight rows reflows badly on
a phone, where the price ends up under a wrapped item name with no
alignment left to carry the association. The `<dt>`/`<dd>` pair keeps that
association semantically whatever the layout does. */
.rates {
display: grid;
gap: 0;
max-inline-size: 56rem;
border-block-start: 1px solid var(--border);
}
.rate {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
gap: var(--space-3) var(--space-5);
padding-block: var(--space-5);
border-block-end: 1px solid var(--border);
}
.rate dt {
display: flex;
flex-direction: column;
gap: var(--space-2);
/* Leaves room for the fee on one line at tablet width and up, and wraps
under it below that. */
flex: 1 1 22rem;
}
.rate-item {
font-size: var(--text-lg);
line-height: var(--leading-snug);
}
.rate-detail {
font-size: var(--text-sm);
line-height: var(--leading-body);
color: var(--text-meta);
max-inline-size: 52ch;
}
.rate dd {
font-family: var(--font-serif);
font-size: var(--text-2xl);
line-height: var(--leading-tight);
white-space: nowrap;
}
/* The cancellation column is a sentence, not a figure, so it takes body type
and is allowed to wrap. */
.rate .dd-text {
flex: 1 1 16rem;
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--leading-body);
color: var(--text-secondary);
white-space: normal;
}
.per {
font-family: var(--font-sans);
font-size: var(--text-sm);
color: var(--text-meta);
}
.scope-note {
margin-block-start: var(--space-6);
max-inline-size: var(--width-prose);
font-size: var(--text-base);
line-height: var(--leading-body);
color: var(--text-secondary);
}
.notes {
/* No `list-style: none` or `padding: 0` — `global.css` applies both to
`ul[role='list']`, and a second copy is a second thing to keep true. */
display: grid;
gap: var(--space-4);
margin-block-start: var(--space-6);
max-inline-size: var(--width-prose);
}
.notes li {
padding-inline-start: var(--space-5);
border-inline-start: 1px solid var(--rule);
line-height: var(--leading-body);
}
.cta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3) var(--space-4);
margin-block-start: var(--space-8);
}
</style>