Build and deploy / build-and-deploy (push) Failing after 4s
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
477 lines
18 KiB
Plaintext
477 lines
18 KiB
Plaintext
---
|
||
/**
|
||
* `/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 →</Button
|
||
>
|
||
<Button href="/process/" variant="ghost"
|
||
>How an engagement runs →</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>
|