chore: project scaffold, specs, and working record
Build and deploy / build-and-deploy (push) Failing after 5s

This commit is contained in:
Pouya Lajevardi
2026-08-26 08:51:16 -04:00
commit 19f7226661
26 changed files with 3206 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
# 01 — Information architecture
Authority: `AGENTS.md` §3 D5 (full multi-page), §6, §5 (audience model).
Every claim in the copy outlines below must clear `AGENTS.md` §4.
---
## Why multi-page at all
The site being replaced is one scrolling page. One page can hold one title, one
meta description, one canonical URL, and one primary topic. It therefore gets
one shot at a search result.
The practice's target searches are not "Toronto mediator" — that term is owned by
retired judges with twenty years of name recognition, and the strategy brief is
explicit that competing there is the wrong game (§II). The winnable searches are
specific: *construction lien mediation Ontario*, *SaaS contract arbitration
Canada*, *SABS mediation Toronto*, *technology dispute neutral*, *Farsi-speaking
mediator*. Each of those wants its own page, its own title, its own copy, and its
own structured data.
That is the entire argument for the structure below. It is a discoverability
decision, not an aesthetic one.
---
## Sitemap
```
/ Home
/about/ Biography, credentials, the professional record
/mediation/ Mediation — the process, formats, rules
/arbitration/ Arbitration — the process, tracks, rules
/med-arb/ Med-Arb and hybrid processes
/practice/ Practice areas index
/practice/construction/ Construction and infrastructure disputes
/practice/technology/ Technology, AI, and data disputes
/practice/energy/ Energy, grid, and regulatory disputes
/practice/insurance/ Insurance, SABS, and accident benefits
/practice/shareholder/ Shareholder, partnership, and family business
/practice/cross-cultural/ Cross-border and diaspora disputes
/process/ What an engagement looks like, step by step
/fees/ Fee schedule and engagement terms
/for-parties/ Plain language: what mediation actually is
/insights/ Article index
/insights/[slug]/ Individual articles
/contact/ Intake form and booking
/legal/privacy/ Privacy policy — PIPEDA
/legal/terms/ Terms of use
```
Nineteen fixed URLs plus one per article.
### URL rules
- Lowercase, hyphenated, trailing slash, no file extensions.
- `/practice/<area>/` is a stable namespace — new practice areas slot in without
touching anything else.
- `/insights/<slug>/` — no dates in the path. A dated URL makes a piece look
stale at 18 months, and this content is mostly evergreen.
- Never change a published URL. If one must move, ship a CloudFront Function
301 and record it in the Change Log.
### Navigation
**Primary (header).** About · Mediation · Arbitration · Practice · Fees ·
Insights · Contact
"Practice" is a dropdown to the six areas, with `/practice/` itself reachable.
Build it as a `<details>` element or a CSS-only disclosure — no JavaScript.
**Footer.** Full sitemap in three columns, plus contact block, professional
designations, LinkedIn, privacy, terms, and the SML Company Ltd. entity line.
**Deliberately not in primary nav:** `/process/`, `/for-parties/`, `/med-arb/`.
These are linked contextually from the pages that lead to them. Seven items is
the ceiling before a nav stops being scannable.
---
## Deliberate omission: Indigenous engagement
The strategy brief (§III.4) rates Indigenous engagement, IBA, and consultation-
breakdown mediation as *"strategically the most valuable single niche"* for a
Q.Med on the C.Med-Arb pathway.
There is no page for it at launch, on the following reasoning:
The brief itself says the niche *"requires deliberate relationship work with
First Nations advisors, federal and provincial engagement staff, and corporate
proponents over a multi-year horizon."* A practice page is a claim of present
capability. Publishing one before that relationship work exists would be read as
exactly what it is by the audience best positioned to notice — and that audience
is small, well-connected, and unforgiving of practitioners who arrive claiming a
seat. The cost of getting this wrong is much higher than the cost of waiting.
Revisit at month 1218, once there is relationship history to point to.
**This reasoning is Claude's, recorded for Pouya's decision — not yet his call.**
---
## Page specifications
Each page below gives its job, its primary audience, its target search intent,
and its section outline. Copy itself is in `03-content-spec.md`.
### `/` — Home
**Job:** establish the unusual stack in under ten seconds, and route each of the
four audiences to its surface.
**Audience:** all four; leans in-house counsel.
**Search intent:** brand and name searches; "Toronto ADR practice".
1. **Hero.** Eyebrow (`Mediation · Arbitration · Toronto`), display headline,
two-sentence positioning paragraph, two CTAs (*Request a consultation* /
*How I work*), portrait.
2. **Credential row.** Three slots: `Q.Med` · `JD + ML` · `EN · FA`. Never
matter counts — `AGENTS.md` §4.
3. **The approach.** The "two directions at once" argument — law and engineering
converging on the same dispute. Infinity mark as the visual anchor.
4. **Two practices.** Mediation and Arbitration cards → `/mediation/`, `/arbitration/`.
Med-Arb named here as the long-term arc, linking to `/med-arb/`.
5. **Practice areas.** Six-card grid → `/practice/*`. This is the most important
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/`.
8. **Contact band.** Intake CTA and booking link.
### `/about/` — Biography and credentials
**Job:** be the page an appointing body or opposing counsel reads before agreeing
to an appointment. This page carries the verifiable record.
**Audience:** appointing bodies, ADR institutions, opposing counsel.
**Search intent:** `"Pouya Lajevardi"`, `Pouya Lajevardi mediator`.
1. Portrait, name, designation line.
2. **Narrative biography**, 400600 words. The three-track story — law,
engineering, operating a company — told as one arc rather than three lists.
3. **Credentials**, structured and scannable: designations, education,
certifications, memberships. Every line from `AGENTS.md` §4 Verified.
4. **The credentialing arc.** Q.Med held → Q.Arb in progress → C.Med-Arb as the
endpoint. The brief (§V) treats the arc itself as part of the story; say so
openly rather than implying a finished state.
5. **Languages and cross-cultural practice.**
6. **Speaking and publications.** Omit the section entirely until there is
something in it. An empty "Speaking" heading is worse than no heading.
7. `Person` JSON-LD. Downloadable one-page PDF bio — brief §VIII lists this as
an asset for circulation with appointment proposals.
### `/mediation/`
**Job:** convert counsel who have already decided on mediation and are choosing a
neutral.
**Search intent:** `commercial mediator Toronto`, `ADRIC mediation rules`,
`what happens at mediation Ontario`.
1. What the service is; the neutral's role stated plainly.
2. **Formats:** full-day, half-day, shuttle, remote, hybrid.
3. **Rules:** ADRIC Model Mediation Rules, or a bespoke protocol agreed by the
parties.
4. **What parties should bring** — briefs, documents, authority to settle.
5. **Confidentiality and without-prejudice framing.**
6. Practice areas → `/practice/*`.
7. Fees → `/fees/`. Booking → `/contact/`.
### `/arbitration/`
**Job:** the same, for arbitration — and to state the Q.Arb position honestly.
**Search intent:** `sole arbitrator Ontario`, `expedited arbitration Canada`,
`documents-only arbitration`.
1. What the service is; sole-arbitrator, party-appointed, and tribunal-secretary
appointments.
2. **Tracks:** documents-only, expedited, full hearing.
3. **Rules:** ADRIC, ADR Chambers, ad hoc.
4. Awards — form, reasoning, timing.
5. **Credentialing status, stated plainly.** The Q.Arb pathway is in progress;
the page says so and describes what is available now (co-arbitration,
tribunal secretary) versus what follows designation. Honesty here is a
differentiator, not a weakness — and misstating it is a conduct problem.
6. Fees, booking.
### `/med-arb/`
**Job:** own a term few Canadian neutrals explain well, and frame the C.Med-Arb
endpoint.
**Search intent:** `med-arb Canada`, `what is med-arb`, `arb-med`.
1. What Med-Arb is; how it differs from Arb-Med.
2. The procedural fairness objection, addressed head-on rather than elided.
3. When it fits and when it does not.
4. The C.Med-Arb designation and why it is the practice's stated endpoint.
This page is a strong candidate for the best-performing page on the site.
Search demand exists, competition is thin, and it maps exactly to the brand's
long-term narrative.
### `/practice/` — index
Six cards, one paragraph each, linking onward. Also the natural home for the
"also offered" strip: early neutral evaluation, settlement counsel, dispute-
system design, and pre-dispute technical advisory.
### `/practice/construction/`
**Search intent:** `construction lien mediation Ontario`, `delay claim mediation`,
`subcontract dispute arbitration Toronto`.
Dispute types (lien, delay, change orders, scheduling, subcontract, deficiency);
what an active litigation practice in the same matters brings to the room; the
Ontario megaproject pipeline as context — Darlington SMR, Bruce C, data centres,
transit; typical process shape. Strongest immediate fit per brief §III.1.
### `/practice/technology/`
**Search intent:** `SaaS dispute arbitration Canada`, `AI vendor dispute`,
`data residency dispute resolution`, `software contract mediator`.
The differentiator page. Dispute types: software contracts, SLA and MSA
breakdowns, data residency and processing, AI vendor diligence, cloud
sub-processor disputes, IP and licensing.
**Write this page in the register the brief demands:** a neutral who can read an
API trace, a model card, or a System Impact Assessment on the same page as the
contract. The brief warns explicitly against softening this to "technologically
literate" — the claim is engineering practice, so the copy says engineering
practice.
### `/practice/energy/`
**Search intent:** `Bill 40 dispute`, `IESO dispute resolution`,
`OEB leave to construct dispute`, `grid connection dispute Ontario`.
Grid connection and allocation, leave-to-construct, proponentmunicipality
disputes, IESO market participation, data-centre connection allocation. Brief
§III.2 frames this as a 2436 month build. **Write it as a genuine position, not
a claim of existing volume.**
### `/practice/insurance/`
**Search intent:** `SABS mediation`, `LAT pre-hearing mediation`,
`accident benefits mediator Ontario`, `MIG dispute`.
Highest realistic near-term volume — it flows directly from the existing
personal-injury and SABS practice, and brief §IV.7 notes the segment is
underserved by senior mediators. Unglamorous and worth doing well.
### `/practice/shareholder/`
**Search intent:** `shareholder dispute mediation Ontario`,
`partnership dissolution mediator`, `family business succession dispute`.
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.
### `/practice/cross-cultural/`
**Search intent:** `Farsi speaking mediator Toronto`,
`Iranian Canadian business dispute`, `diaspora shareholder dispute`.
Note that D4 makes the site English-only. This page describes Farsi-language
capability in English; it is not a Farsi page. Diaspora family-business
succession, dual-jurisdiction shareholder disputes, partnership disputes among
diaspora entrepreneurs, cross-cultural commercial matters.
### `/process/`
Five steps, from intake to conclusion: confidential intake (day 0) · engagement
and framing (17) · pre-session exchange (721) · the session (2130) · binding
conclusion (30+). Also: conflicts checking, confidentiality, and what happens if
a matter does not settle.
### `/fees/`
**Blocked on `AGENTS.md` Q4 — do not invent numbers.**
Hourly rate; half-day and full-day mediation; preparation time policy;
cancellation terms; administrative fee; HST treatment; who pays and how costs
are shared between parties; payment terms. A real page with real numbers, or a
`TODO(pouya)` — nothing in between.
### `/for-parties/`
**Job:** serve the self-represented tier without diluting the counsel-facing
pages. Plain language, short sentences, no jargon.
What mediation is · what it is not · who the mediator is and is not (not your
lawyer, not a judge) · what happens on the day · what it costs · what happens if
you do not settle · how to prepare.
### `/insights/` and `/insights/[slug]/`
Astro content collection, MDX. Index reverse-chronological with topic filtering
by practice area.
Article frontmatter: `title`, `description`, `publishDate`, `updatedDate`,
`topics[]`, `practiceAreas[]`, `readingTime`, `draft`.
Content territories, from brief §VII: process explainers · regulatory commentary ·
industry-specific dispute commentary · anonymised reflections · technical
explainers for lawyers · credentialing and career-arc content.
`Article` JSON-LD with `author` pointing at the `Person` entity. Each article
links to the relevant practice-area page — this is what turns the blog into
ranking power for the pages that convert.
**The section stays out of primary navigation until at least two pieces are
live.** An empty blog signals abandonment more loudly than no blog signals
anything.
### `/contact/`
Intake form (`05-backend-spec.md`), booking embed, direct email and phone
(Q3), Toronto by-appointment line, response-time expectation, and an explicit
note that submitting the form does not create a retainer or a mediatorparty
relationship and does not itself create a conflict check.
### `/legal/privacy/` and `/legal/terms/`
Required, not optional — the intake form collects personal information about
identifiable third parties in live legal disputes. What is collected, why, where
it is stored (DynamoDB, region), retention period, who can access it, how to
request deletion, and the contact for privacy inquiries. Must match what the
backend actually does.
---
## Build order
Dependency-ordered, so nothing is blocked mid-stream:
1. Scaffold, tokens, base layout, header, footer, SEO component
2. `/` — proves the design system end to end
3. `/about/` — the credential spine everything else references
4. `/mediation/`, `/arbitration/`, `/med-arb/`
5. `/practice/` and the six area pages
6. `/process/`, `/for-parties/`
7. `/insights/` plumbing, then the drafted articles
8. `/contact/` and the intake backend
9. `/fees/` — last, since it is blocked on Q4
10. `/legal/*` — written to match the backend as actually built
11. Audit and cutover (`06-deployment.md`)
+195
View File
@@ -0,0 +1,195 @@
# 02 — Design system
Authority: `AGENTS.md` §3 D7 — *keep the palette and the infinity mark;
modernize the execution.* The look is not up for redesign. What follows is the
system that preserves it while fixing what the old build got wrong.
---
## What carries over unchanged
- The **palette**: cream, ink, maroon, gold.
- The **infinity mark** — SML Company Ltd.'s actual logo, and the metaphor holds:
a dispute is a loop, and the work is redrawing the loop into a line.
- The **type pairing**: Instrument Serif for display, Geist for text, Geist Mono
for eyebrows and labels.
- The **editorial register** — generous whitespace, restrained colour, serif
display type at large sizes.
## What changes
| Was | Is | Why |
|---|---|---|
| Google Fonts at runtime | Self-hosted, subset, preloaded | Removes a render-blocking third-party round trip from a page that collects legal inquiries |
| Fixed px type sizes | Fluid `clamp()` scale | One scale from 360 px to 1600 px with no breakpoint jumps |
| Ad hoc spacing values | 8 px base scale | Consistent vertical rhythm; no magic numbers |
| Gold used as a text colour on cream | Gold restricted to decorative and on-dark | **It fails WCAG AA at 2.10:1.** Measured, not assumed |
| Scroll-reveal on every element, always on | Reveal on major sections only, gated behind `prefers-reduced-motion` | Motion that reads as confident rather than decorative; accessible by default |
| 2.2 MB single file, ~1 MB of base64 logos | Optimized SVG mark, AVIF/WebP photography | The mark is geometry; it should be vector, not a 470 KB PNG |
| React 18 dev build + Babel Standalone in the browser | Static HTML, near-zero JS | The reason the site is invisible to crawlers |
---
## Colour
Tokens live in `src/styles/tokens.css`. Never write a raw hex value in a
component.
| Token | Value | Use |
|---|---|---|
| `--cream` | `#faf7f2` | Page background |
| `--cream-2` | `#f3ede0` | Alternating section background |
| `--cream-3` | `#ebe3d1` | Cards and insets on cream |
| `--ink` | `#1a1614` | Body text; dark section backgrounds |
| `--ink-soft` | `#3a322c` | Secondary text |
| `--muted` | `#6e6359` | Metadata, captions — **on cream only** |
| `--maroon` | `#5a1a1c` | Primary action, accents, dark panels |
| `--maroon-d` | `#3d1112` | Hover on maroon |
| `--maroon-l` | `#7a2a2c` | Links on cream |
| `--gold` | `#c9a876` | Rules, dividers, on-dark accent — **never text on cream** |
| `--gold-d` | `#a88858` | Large decorative text on cream only |
| `--gold-l` | `#e2c89a` | Text on ink or maroon |
### Contrast — measured, 2026-08-25
Against `--cream` `#faf7f2`:
| Colour | Ratio | AA body (4.5) | AA large (3.0) |
|---|---|---|---|
| `--ink` `#1a1614` | **16.81** | pass | pass |
| `--ink-soft` `#3a322c` | **11.75** | pass | pass |
| `--maroon` `#5a1a1c` | **12.29** | pass | pass |
| `--maroon-l` `#7a2a2c` | **8.95** | pass | pass |
| `--muted` `#6e6359` | **5.47** | pass | pass |
| `--gold-d` `#a88858` | **3.11** | **FAIL** | pass |
| `--gold` `#c9a876` | **2.10** | **FAIL** | **FAIL** |
Against `--maroon` `#5a1a1c`: cream 12.29 · gold-l 8.11 · gold 5.84 — all pass.
Against `--ink` `#1a1614`: cream 16.81 · gold-l 11.09 · gold 8.00 — all pass.
`--muted` on ink measures **3.07 and fails**; use `--gold-l` or cream at reduced
opacity for secondary text on dark.
**Hard rules.**
1. `--gold` is never a text colour on cream. Rules, borders, dividers, icon
strokes, and on-dark text only.
2. `--gold-d` on cream only at 24 px+ / 19 px bold, and only for decorative
display text — never for anything a reader must parse.
3. Body text on cream is `--ink` or `--ink-soft`. Metadata may use `--muted`.
4. Secondary text on dark is `--gold-l` or `--cream` at ≥ 70% opacity.
5. **No dark mode.** A single committed light identity is the right call for a
legal practice, and halving the surface area halves the ways contrast breaks.
---
## Typography
**Faces.** Instrument Serif (display) · Geist 300/400/500/600 (text) ·
Geist Mono 400/500 (eyebrows, labels, data).
Self-host all three. Subset to Latin + Latin Extended-A. `font-display: swap`.
Preload only the two faces used above the fold — Instrument Serif regular and
Geist 400.
**Scale.** Fluid, `clamp()`, `1.25` ratio at the small end widening to `1.333` at
the display end. Tokens `--text-xs` through `--text-6xl` in `tokens.css`.
**Rules.**
- Display type (`--font-serif`) at `--text-4xl` and above only. It has almost no
hinting at small sizes and looks weak below 32 px.
- Display line-height `0.95``1.05`; letter-spacing `-0.02em`.
- Body line-height `1.6`. Measure capped at `68ch` — the old site ran full-bleed
paragraphs at 1400 px, which is unreadable.
- Eyebrows: mono, 1112 px, `0.18em` tracking, uppercase, always paired with a
real heading. An eyebrow is not a heading and never carries the `<h*>`.
- Italic display (`.it`) is the one flourish the design allows. One italic phrase
per headline, at most.
- Never skip a heading level. `<h1>` once per page.
---
## Spacing and layout
8 px base: `--space-1` `4px` · `-2` `8` · `-3` `12` · `-4` `16` · `-5` `24` ·
`-6` `32` · `-7` `48` · `-8` `64` · `-9` `96` · `-10` `128` · `-11` `160`.
Content width `1280px`; prose measure `68ch`; wide media `1440px`.
Gutters: `24px` mobile, `48px` desktop.
Section rhythm: `--space-9` (96px) mobile, `--space-11` (160px) desktop.
Grid: 12 columns desktop, 6 tablet, 4 mobile, `--space-5` gutter.
---
## Motion
The old build animated nearly everything on scroll. The replacement is
deliberate and quiet.
- **Reveal on section entry only** — not on every child element. Sub-element
stagger is limited to card grids, and capped at six children.
- Duration `600ms`, easing `cubic-bezier(.2,.7,.2,1)`. Transform and opacity
only — never layout properties.
- Implement with `IntersectionObserver` in one tiny inline script, or
`animation-timeline: view()` where supported. Not a framework, not a library.
- **Content is visible without JavaScript.** The reveal is an enhancement layered
on top of already-rendered HTML. If the observer never runs, the page reads
normally. The old build had this exactly backwards.
- Hover transitions `250ms`.
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
Nothing animates infinitely. Nothing autoplays. Nothing moves on page load
except the reveal of the hero.
---
## Components
| Component | Notes |
|---|---|
| `InfinityMark` | Inline SVG, `currentColor`, `aria-hidden` when decorative. Never a PNG |
| `SiteHeader` | Sticky, condenses on scroll. Practice dropdown as CSS-only `<details>` |
| `SiteFooter` | Three-column sitemap, contact block, designations, entity line |
| `Eyebrow` | Mono label with optional maroon dot |
| `SectionHeading` | Eyebrow + display heading + optional lede, one measure |
| `Button` | Variants `primary` (maroon) · `ghost` (outlined) · `gold` (ink bg, gold-l text). Renders `<a>` or `<button>` correctly |
| `Pill` | Small bordered label for designations and sector chips |
| `CredentialRow` | Three or four credential slots. **Never matter counts**`AGENTS.md` §4 |
| `PracticeCard` | Sector chip, heading, one paragraph, arrow link |
| `ProcessStep` | Numbered step, timing, body |
| `ArticleCard` | Title, description, date, topic pills, reading time |
| `Prose` | Long-form wrapper. Owns all typographic defaults for MDX |
| `SEO` | Title, description, canonical, OG, Twitter, JSON-LD — see `04-seo-spec.md` |
**Focus states.** Every interactive element gets a visible focus ring:
`outline: 2px solid var(--maroon); outline-offset: 3px`. Use `:focus-visible`.
Never `outline: none` without a replacement — the old build removed it globally.
---
## Accessibility floor
Not a polish pass. A build requirement.
- One `<h1>` per page; heading levels never skipped.
- Landmarks: `<header>`, `<nav>`, `<main>`, `<footer>`. Skip-to-content link first
in tab order.
- Every image has `alt`. Decorative images get `alt=""`.
- Colour never carries meaning alone.
- All functionality reachable by keyboard; focus order matches visual order.
- 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.
- Every page must be readable and navigable with JavaScript disabled.
+198
View File
@@ -0,0 +1,198 @@
# 03 — Content and voice
Authority: `AGENTS.md` §4 (claim register) and §5 (audience model).
Source material: `PL_ADR_Personal_Branding_Strategy_Brief.docx` (2026-05-26) and
`ADR_Site_Content_Brief_for_Claude_Design.md` (2026-05-26).
---
## The one rule
**Every factual claim traces to `AGENTS.md` §4 Verified.** Read the Forbidden
table before writing any number, statistic, or superlative. If you need a fact
you do not have, write `TODO(pouya): <exact question>` and log it in §9. Do not
infer, do not soften, do not carry anything over from the old site.
---
## Voice
**Restrained, precise, and unhedged.** The reader is usually a lawyer. They
detect padding instantly and discount everything after it.
**Do:**
- Short declaratives. "I read the contract and the code." Not "clients benefit
from a uniquely multidisciplinary perspective."
- First person singular. This is a practitioner brand — "I", not "we", not "the
practice". The old site's "we" implied a firm that does not exist.
- Concrete nouns. *Lien claim. Change order. System Impact Assessment. Model
card. Minutes of settlement.* Specificity is the credential.
- Name the limits. "Sole-arbitrator appointments follow the Q.Arb designation;
co-arbitration and tribunal-secretary work is available now." Precision about
what you cannot yet do makes the rest believable.
- Plain words over Latin. "Without prejudice" survives because it is a term of
art; *inter alia* does not.
**Do not:**
- **Any claim or implication of legal licensure.** D13: the site asserts the JD
and nothing more. Never "lawyer", "called to the bar", "licensed", "my law
practice", "my litigation practice", "my clients", "acts for", "represents".
Implication counts as much as assertion.
**The approved phrasing is "active litigation exposure" or "involvement in
litigation and ADR matters" — never "practice" in that context.** Pouya's
wording, 2026-08-26. So: *Director of Firm Operations at a Toronto litigation
and ADR boutique, with active exposure to construction, personal injury, POA,
and SABS matters.* Accurate, specific, and it claims nothing it should not.
This framing is **interim** — see `AGENTS.md` §12 R1. Raise it with Pouya
rather than letting it settle in by default.
- Superlatives. No "leading", "premier", "top-rated", "best". LSO marketing rules,
and they read as insecure.
- Outcome language that could be read as a guarantee.
- "Passionate", "dedicated", "committed", "proven track record", "results-driven",
"leverage", "synergy", "solutions".
- Hedges that erase the claim. The strategy brief warns specifically against
softening the technical claim to "technologically literate" — **the claim is
engineering practice, so the copy says engineering practice.**
- Em-dash-heavy rhythm and tricolon padding. One idea per sentence.
- Second-person sales copy on counsel-facing pages. `/for-parties/` is the one
page written to "you".
---
## The core positioning statement
Reused, adapted, across the hero, the About page, and the PDF bio:
> The dispute resolution practice of Pouya Lajevardi — a credentialed neutral
> who is also a working litigator and a practising machine-learning and
> infrastructure engineer. Built for commercial, construction, energy,
> technology, and cross-cultural disputes that turn on facts most neutrals take
> on faith: the contract, the code, the engineering documents, and the
> regulatory overlay around them.
Every version of this must survive the §4 check. It does: each element is
verified.
## Approved headline options
From the content brief; all three sit honestly with the practice.
1. *A mediator who reads the contract, the code, and the room.***recommended.**
The cleanest one-sentence statement of the moat, and rare because it is rare.
2. *Engineered for the cases that don't fit a courtroom.*
3. *Disputes resolved by someone who has been on every side of one.*
## The credential row
Three slots, never counts:
| Slot | Value | Label |
|---|---|---|
| 1 | **Q.Med** | ADRIC / ADRIO designation |
| 2 | **JD + ML** | Law and engineering |
| 3 | **EN · FA** | Bilingual practice |
Fourth slot where the layout has one: **Q.Arb** — in progress.
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.
---
## Per-page copy notes
### Home
Hero headline from the approved list. Positioning paragraph above. CTAs:
*Request a consultation →* and *How I work*. The approach section makes the
"two directions at once" argument — law and engineering converging on the same
dispute — and keeps the infinity metaphor: *disputes are loops; the work is
redrawing the loop into a line.* First person: "my mark", not "our mark".
### About
400600 words of narrative, then structured credentials. Tell the three tracks
as one arc, not three lists: a JD and an active litigation practice; a parallel
career in machine learning and infrastructure engineering; a company run
alongside both. The arc is the point — the credentialing pathway from Q.Med
through Q.Arb to C.Med-Arb is stated openly as in progress. The brief treats
that arc as part of the story rather than something to obscure.
Omit any section that would be empty. No "Speaking" heading until there is a
talk to list.
### Mediation / Arbitration / Med-Arb
Procedural, specific, unembellished. Name the rules. Describe the formats. State
what a party should expect to do and when. On `/arbitration/`, state the Q.Arb
position in plain terms — what is available now versus what follows designation.
`/med-arb/` addresses the procedural-fairness objection directly: the same
neutral who heard a party's confidential caucus later decides the matter. Do not
elide it. Explain the consent mechanics and when the process is inappropriate.
Meeting the strongest objection is what makes the page worth reading.
### Practice areas
Each page: dispute types, why this practice fits, what the process looks like,
and the market context that makes the area live. Context comes from the strategy
brief §IIIIV — Ontario's megaproject pipeline, Bill 40 and grid connection, the
2026 privacy statute, LAT volumes.
**Frame as positioning, not as history.** "Built to facilitate procurement and
subcontract disputes on Ontario's megaproject pipeline" — not "extensive
experience resolving". The first is true and forward-looking. The second is
neither.
### Process
Five steps with real timing. Say what happens if the matter does not settle —
counsel want to know the downside shape before they commit a client's day.
### Fees
**Blocked on Q4.** Real numbers or `TODO(pouya)`. Plain table, no "starting from"
evasions, no "contact for pricing" after promising a rate card.
### For parties
The one page in second person. Grade-9 reading level. Short sentences. Says
explicitly: the mediator is not your lawyer and cannot give you legal advice; the
mediator does not decide who is right. Answers what it costs and what happens if
you do not settle.
### Insights
1,2001,800 words, monthly cadence (brief §VIII). Territories from §VII:
process explainers · regulatory commentary · industry dispute commentary ·
anonymised reflections · technical explainers for lawyers · credentialing content.
Every piece links to at least one practice-area page. Anonymised reflections must
be genuinely unidentifiable — not merely name-stripped. If a matter could be
recognised by the parties to it, it does not run.
### Launch article slate (D9)
Drafted by Claude, **every word reviewed by Pouya before publication**:
1. *What the Ontario data-centre build-out means for dispute resolution*
technology + construction; the strongest single differentiator piece.
2. *When Med-Arb is the right answer, and when it is not* — process explainer;
feeds `/med-arb/`; high search intent, thin competition.
3. *Bill 40 and grid connection: a dispute-resolution read* — regulatory
commentary; establishes the energy niche.
4. *What a System Impact Assessment actually evaluates* — technical explainer for
lawyers; the clearest demonstration of the claim the whole brand rests on.
5. *Choosing a neutral: what counsel should actually ask* — evergreen, useful,
and it makes the case for this practice without arguing for it.
---
## Compliance checklist — before any page ships
- [ ] Every factual claim appears in `AGENTS.md` §4 Verified
- [ ] No matter counts, settlement rates, dollar figures, or time-to-award stats
- [ ] No testimonials, endorsements, or third-party quotes
- [ ] No superlatives and no guarantee language
- [ ] No claim or implication of legal licensure anywhere (D13)
- [ ] Q.Arb described as commenced August 2026, never as held or nearly complete
- [ ] Nothing implies a firm, a team, or offices that do not exist
- [ ] Contact page states that an inquiry creates no retainer and no
mediatorparty relationship
- [ ] Any comparative claim is factual and verifiable
+132
View File
@@ -0,0 +1,132 @@
# 04 — Discoverability
The problem this project exists to fix. `AGENTS.md` §2 has the measurements: a
server-side fetch of the live site returns three words.
---
## The baseline being replaced
| | Now `[verified 2026-08-25]` | Target |
|---|---|---|
| Content in server HTML | `SML Company · DISPUTE RESOLUTION · Unpacking...` | Every word |
| Indexable pages | 1 | 19 + articles |
| `<title>` | `SML Company · Dispute Resolution` — pre-rebrand placeholder | Unique per page |
| Meta description | none | Unique per page |
| `<meta viewport>` | **absent** | Present |
| Canonical URL | none | Every page |
| OG / Twitter tags | none | Every page |
| Structured data | none | Person, LegalService, Article, FAQ, Breadcrumb |
| `robots.txt` | 403 | Served |
| Sitemap | none | Generated at build |
| Favicon | none | Full set |
Astro's static output solves most of this by existing. The rest is the spec below.
## Why static output matters more than usual here
Google can sometimes render client-side JavaScript. Bing largely does not.
LinkedIn's preview crawler does not. Slack's unfurler does not. **And the
crawlers behind AI assistants — increasingly how counsel and in-house teams
find a neutral — generally do not.**
A site that requires three CDN round trips and an in-browser Babel compile before
producing a sentence is invisible to all of them. That is the whole argument for
D1.
---
## Metadata
Every page passes through one `SEO` component. A page without it is not finished.
```
title 5060 chars, unique. Pattern: "<Page> · Pouya Lajevardi"
Home: "Pouya Lajevardi · Mediation & Arbitration · Toronto"
description 140160 chars, unique, written for a human, not stuffed
canonical absolute, https, trailing slash
og:title/description/image/url/type/site_name/locale (en_CA)
twitter:card summary_large_image
robots index,follow — except /legal/* which is noindex,follow
```
**OG images:** 1200 × 630. Generate at build with `satori` or `astro-og-canvas`
using the site's own type and palette. One template: display headline on cream,
infinity mark, designation line. Never a screenshot.
## Structured data
JSON-LD only. Validate against Google's Rich Results Test before cutover.
| Type | Where | Notes |
|---|---|---|
| `Person` | `/about/`, referenced site-wide | `name`, `jobTitle`, `description`, `alumniOf` (Bond University), `knowsLanguage` (en, fa), `hasCredential` (Q.Med), `sameAs` (LinkedIn — **Q12**), `image`, `worksFor` |
| `LegalService` | Home | `areaServed` Toronto/Ontario, `serviceType` Mediation/Arbitration, `provider` → Person, `priceRange` once `/fees/` is real |
| `Service` | Each practice page | `serviceType`, `provider` → Person, `areaServed` |
| `Article` | Each article | `headline`, `description`, `datePublished`, `dateModified`, `author` → Person, `image` |
| `BreadcrumbList` | All nested pages | Matches visible breadcrumbs |
| `FAQPage` | `/for-parties/`, `/med-arb/` | Only where the visible page genuinely is Q&A. Never fabricate questions to farm a rich result |
**`hasCredential` must reflect reality.** Q.Med is held. Q.Arb is not. Marking an
unheld credential as held in structured data is a misrepresentation that happens
to be machine-readable.
## Crawlability
**`public/robots.txt`:**
```
User-agent: *
Allow: /
Disallow: /legal/
Sitemap: https://adr.smlcompany.ca/sitemap-index.xml
```
Do not block AI crawlers. Being read by an assistant that a general counsel is
using to shortlist neutrals is the point.
**Sitemap:** `@astrojs/sitemap`, excluding `/legal/*` and any `draft: true`
article. Submit to Google Search Console and Bing Webmaster Tools at cutover.
**Internal linking.** Every practice page links to `/mediation/` and
`/arbitration/`; those link back to the practice areas; every article links to
at least one practice page. This is what turns Insights into ranking power for
the pages that convert. Breadcrumbs on every nested page.
**404 page.** Real, styled, with search-intent links out. CloudFront must return
it with a genuine 404 status — not a 200, which the S3 website-endpoint pattern
gets wrong by default.
## Performance
Core Web Vitals are a ranking input, and the current build fails all of them.
| Metric | Budget |
|---|---|
| LCP | < 2.0 s, Slow 4G |
| CLS | < 0.05 |
| INP | < 150 ms |
| JS per route | < 100 KB |
| Lighthouse (mobile) | ≥ 95 all four categories |
How: static HTML, self-hosted preloaded subset fonts, AVIF/WebP with explicit
dimensions, critical CSS inlined, no third-party scripts on any page except the
booking embed on `/contact/` — and that one is lazy-loaded behind a click.
## Local and professional presence
Not code, but it belongs in the launch checklist: Google Business Profile for the
practice; ADRIC and ADRIO directory listings pointing at the site; a LinkedIn
profile whose headline and Featured section match the brand (brief §VIII);
consistent name, address, and phone across all of them.
## Post-launch verification
- [ ] `curl -s https://adr.smlcompany.ca/ | grep -c "<h1"` returns ≥ 1
- [ ] Every page renders its full text with JavaScript disabled
- [ ] Rich Results Test passes on Person, LegalService, Article
- [ ] OG preview renders correctly in LinkedIn Post Inspector and Slack
- [ ] Sitemap submitted to Google Search Console and Bing
- [ ] No page returns 200 for a URL that should 404
- [ ] Lighthouse ≥ 95 mobile on `/`, `/about/`, one practice page, one article
+175
View File
@@ -0,0 +1,175 @@
# 05 — Intake, booking, and data handling
Authority: `AGENTS.md` §3 D10 — rebuilt intake form plus calendar booking.
Existing infrastructure is documented in `AWS-Hosting-Guide.md` Parts 810.
**Read that guide before changing anything**; the resources already exist and
were built by hand in the console.
---
## What exists today
API Gateway (HTTP API) → Lambda → DynamoDB, with SES for notification email and
a verified sender on `smlcompany.ca`. `[verified 2026-08-25 — AWS-Hosting-Guide.md]`
The shape is right. This is a hardening and rework pass, not a replacement.
## What this data actually is
The form collects, in a live legal dispute: the inquirer's identity and contact
details, the names of opposing parties and their counsel, the nature of the
dispute, and often the amounts at issue.
That is **personal information about identifiable third parties who have not
consented and do not know the submission happened.** It is more sensitive than a
typical contact form by a wide margin, and it is potentially conflict-relevant.
Design accordingly. Nothing in this section is optional.
---
## Form fields
| Field | Type | Required | Notes |
|---|---|---|---|
| Name | text | yes | |
| Email | email | yes | Validated server-side, not only in the browser |
| Phone | tel | no | |
| Role | select | yes | Counsel · In-house · Party · Institution · Other |
| Firm / organisation | text | no | |
| Process sought | select | yes | Mediation · Arbitration · Med-Arb · ENE · Not sure |
| Practice area | select | yes | The six areas plus Other |
| Other parties | text | no | Surfaced for conflicts screening |
| Opposing counsel | text | no | Same |
| Matter summary | textarea | yes | 2000 char cap. Hint: *no privileged or confidential detail* |
| Timing | select | no | Urgent · 30 days · 90 days · Exploring |
| Preferred contact | radio | no | Email · Phone |
| Consent | checkbox | **yes** | Explicit, unchecked by default, links to `/legal/privacy/` |
**Do not collect** dollar amounts, document uploads, or anything the inquirer
might reasonably treat as privileged. The intake call is for that.
### Consent text
> I consent to Pouya Lajevardi storing and using the information in this form to
> respond to my inquiry and to run a conflicts check. I understand that
> submitting this form does not create a retainer, does not appoint a neutral,
> and does not itself establish a mediatorparty relationship.
## Validation and abuse control
Client-side validation is a convenience. **The Lambda re-validates everything.**
- Required fields present; email well-formed; lengths within bounds
- Reject any field over its cap rather than truncating silently
- **Honeypot** field, hidden from sighted and screen-reader users, must be empty
- **Timestamp check** — reject submissions completed in under 3 seconds
- **Rate limit** by source IP at API Gateway: 5 requests / 5 minutes
- No CAPTCHA. It is a third-party script on a page collecting legal information,
and the two controls above stop the traffic that matters
- CORS restricted to `https://adr.smlcompany.ca` — no wildcard
- Strip HTML from every field before storage and before it enters an email body
## Storage
DynamoDB, `ca-central-1` — **Canadian data residency is a real selling point for
a Canadian legal practice, and the privacy policy will say so.** Confirm the
existing table's region and migrate if it is elsewhere (**Q10**).
| Attribute | |
|---|---|
| `pk` | `INTAKE#<uuid>` |
| `sk` | `<ISO-8601 timestamp>` |
| fields | as above |
| `sourceIp`, `userAgent` | abuse investigation only |
| `ttl` | epoch seconds — **automatic deletion** |
**Encryption at rest** with a customer-managed KMS key. **Point-in-time recovery
on.** Table access limited to the Lambda role and one named administrative
principal.
### Retention
**24 months, enforced by DynamoDB TTL.** Not a policy someone remembers — a
mechanism that runs whether anyone remembers or not.
Rationale: long enough to serve conflicts screening across a normal matter
lifecycle; short enough to be defensible under PIPEDA's requirement to retain
personal information only as long as necessary. Whatever number ships must match
`/legal/privacy/` exactly.
## Notification
SES on submission:
- **To Pouya:** the full submission, plainly formatted, replyable to the inquirer.
- **To the inquirer:** confirmation of receipt, expected response time, a repeat
of the no-retainer language, and a link to the privacy policy. This email is
the reason the form beats a `mailto:` link.
SES must have SPF, DKIM, and DMARC aligned on `smlcompany.ca` or these land in
spam. The guide covers domain verification; **DMARC needs confirming (Q3)**.
Failure handling: SES failure must never lose the submission. Write to DynamoDB
first, then send. A dead-letter queue on the Lambda, and a CloudWatch alarm on
DLQ depth ≥ 1.
## Booking
An embedded scheduler for the 3045 minute confidential intake call
(**Q5** — tool not yet chosen).
- Prefer a provider with Canadian or EU data residency and no advertising
business. Cal.com self-hosted is the strongest privacy posture; Cal.com cloud
or Calendly are acceptable.
- **Lazy-load behind a click.** No third-party iframe on first paint, and no
third-party script on any other page.
- Provide a plain link fallback that works with JavaScript disabled.
- The booking page must carry the same no-retainer language.
- Disclose the provider by name in `/legal/privacy/`.
## Security headers
Set at CloudFront via a response-headers policy:
```
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()
Content-Security-Policy: default-src 'self'; img-src 'self' data:;
style-src 'self' 'unsafe-inline'; script-src 'self';
frame-src <booking-provider>; form-action 'self' <api-endpoint>;
base-uri 'self'; frame-ancestors 'none'
```
Tighten CSP once the booking provider is chosen. `unsafe-inline` on styles is
tolerable for critical CSS; `unsafe-inline` on scripts is not — use a hash or
nonce for the reveal script.
## Privacy policy must state
Written to match what is actually built, not what is typical:
What is collected · why · lawful basis (consent) · where it is stored (DynamoDB,
region, encrypted at rest) · **the retention period and that deletion is
automatic** · who can access it · third parties involved (AWS, SES, the booking
provider, analytics if any) · how to request access or deletion and the address
to use · that submitting the form creates no retainer and no mediatorparty
relationship · cookie and analytics disclosure · last-updated date.
If analytics ship, prefer a cookieless privacy-preserving tool (Plausible,
Fathom). GA4 on a page collecting legal-dispute information is a poor fit for a
practice whose privacy posture is part of its offer (**Q11**).
## Definition of done
- [ ] Server-side validation independent of the client
- [ ] Honeypot and timing checks live; rate limit configured
- [ ] CORS restricted to the production origin
- [ ] TTL set and verified by test record
- [ ] KMS encryption and PITR enabled
- [ ] Both emails send; SPF/DKIM/DMARC aligned; inbox-tested, not spam-tested
- [ ] DLQ and CloudWatch alarm configured
- [ ] Form usable by keyboard only; errors announced with `role="alert"`
- [ ] Form degrades to a `mailto:` fallback with JavaScript disabled
- [ ] Privacy policy matches the implementation line for line
+274
View File
@@ -0,0 +1,274 @@
# 06 — Deployment and cutover
Authority: `AGENTS.md` §3 D3 (git + GitHub Actions → existing S3/CloudFront) and
D11 (build everything, one clean cutover).
Existing infrastructure: `AWS-Hosting-Guide.md`.
---
## Topology
```
GitHub push to main
└─ GitHub Actions
├─ npm ci && npm run build → ./dist
├─ assume AWS role via OIDC (no stored keys)
├─ aws s3 sync ./dist s3://<bucket>
└─ cloudfront create-invalidation
Namecheap DNS → CloudFront → S3 (OAC)
API Gateway → Lambda → DynamoDB / SES (intake, unchanged path)
```
DNS is at **Namecheap, not Route 53** `[verified 2026-08-25]`. Nothing in the
pipeline touches DNS. Certificate renewal is ACM-automatic as long as the
validation CNAME stays in place at Namecheap — **do not delete it.**
## CI runs on Gitea, not GitHub
`AGENTS.md` D3 as amended, 2026-08-26: self-hosted **Gitea**, repo `adr-sml`,
local clone at `/Users/pouya/Dev/Websites/adr-sml`.
**The live pipeline is `.gitea/workflows/deploy.yml`.** Gitea Actions speaks
GitHub Actions syntax, so it is a near-direct port — the build steps, the
two-pass sync, and the cache headers are unchanged. `.github/workflows/deploy.yml`
stays in the repo as the OIDC reference in case the project ever moves.
### The one real difference: no OIDC
Gitea is not an AWS OIDC provider. There is no role to assume, so deploys
authenticate with a **scoped IAM user** whose access key lives only in the
repository's Gitea secrets.
This is a genuine step down in security from the GitHub setup, and it should be
treated as one. The mitigations are the policy scope and the rotation schedule.
**Create the user:**
1. IAM → Users → `adr-sml-deploy`. **Programmatic access only** — no console
password, no MFA device, no group membership.
2. Attach this inline policy and nothing else. Substitute the real bucket name,
account ID, and distribution ID from `scripts/aws-discover.sh`:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListSiteBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::BUCKET_NAME"
},
{
"Sid": "WriteSiteObjects",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:PutObjectAcl", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::BUCKET_NAME/*"
},
{
"Sid": "InvalidateOneDistribution",
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
]
}
```
No `s3:*`. No `cloudfront:*`. No wildcard resources. If a deploy step needs a
permission this policy lacks, the correct response is to question the step, not
to widen the policy.
3. Create an access key. **Copy it once** — AWS will not show the secret again.
### Gitea configuration
**Repository → Settings → Actions → Secrets:**
| Name | Value |
|---|---|
| `AWS_ACCESS_KEY_ID` | from the IAM user |
| `AWS_SECRET_ACCESS_KEY` | from the IAM user |
**The real values** (captured 2026-08-26, `aws-inventory.txt`):
| Variable | Value |
|---|---|
| `AWS_REGION` | `ca-central-1` |
| `S3_BUCKET` | `adr-smlcompany-site` |
| `CLOUDFRONT_DISTRIBUTION_ID` | `E1OK7G98KNKUTA` |
| `INTAKE_ENDPOINT` | `https://4tl0m5igkj.execute-api.ca-central-1.amazonaws.com` |
| `BOOKING_URL` | *(empty — parked, R6)* |
IAM policy substitutions: `BUCKET_NAME` = `adr-smlcompany-site`,
`ACCOUNT_ID` = `327082975128`, `DISTRIBUTION_ID` = `E1OK7G98KNKUTA`.
> **Read this before creating the key.** Account `327082975128` is shared across
> `meshkinilaw.ca`, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and
> `mlp-clientdb-prod-backups` — a law firm's client-database backups. A static
> deploy key for a marketing site lives in the same account. The scoped policy is
> what keeps a compromised Gitea runner from reaching any of that. Do not widen
> it, and never put the `user/pouya` credentials in CI.
**Repository → Settings → Actions → Variables** (not secrets — these are not
sensitive, and keeping them as variables means they appear in run logs where they
are useful for debugging):
| Name | Value |
|---|---|
| `AWS_REGION` | e.g. `ca-central-1` |
| `S3_BUCKET` | the site bucket |
| `CLOUDFRONT_DISTRIBUTION_ID` | the `E...` ID |
| `INTAKE_ENDPOINT` | API Gateway invoke URL |
| `BOOKING_URL` | once chosen (Q5) |
### A runner must exist
Gitea Actions needs `act_runner` registered to this repository or its
organisation, and Actions enabled both site-wide in `app.ini`
(`[actions] ENABLED = true`) and per-repository. Without a runner the workflow
queues silently and never runs — which looks exactly like a broken pipeline.
The workflow installs the AWS CLI if the runner image lacks it, and runs
`aws sts get-caller-identity` before touching anything, so a credential problem
fails loudly and early rather than halfway through a sync.
### Key rotation — an operational obligation
**Rotate `adr-sml-deploy` quarterly.** OIDC would have made this unnecessary;
with a static key it is a standing task:
1. Create a second access key on the same user.
2. Update the Gitea secrets.
3. Run the workflow and confirm it succeeds.
4. **Delete the old key.** Rotation that leaves the old key active is not
rotation.
Set a calendar reminder. A key that is never rotated is the failure mode this
whole section exists to bound.
## Finding the AWS identifiers
`scripts/aws-discover.sh` collects everything Q10 needs — bucket, distribution
ID, regions, API endpoint, certificate, SES identities, and whether S3 versioning
is on. Read-only; every call is a list or describe.
```bash
chmod +x scripts/aws-discover.sh
./scripts/aws-discover.sh > aws-inventory.txt
```
The output contains resource names and IDs but no secrets.
## Why OIDC and not access keys
The alternative is a long-lived `AWS_ACCESS_KEY_ID` in GitHub secrets: a
credential that never expires, is invisible once set, and grants its permissions
to anyone who can reach the repository. OIDC issues a short-lived token per run,
scoped to this repository and this branch.
One-time setup:
1. IAM → Identity providers → add OIDC provider `token.actions.githubusercontent.com`,
audience `sts.amazonaws.com`.
2. Create role `adr-site-deploy` trusting that provider, with a condition on
`token.actions.githubusercontent.com:sub` equal to
`repo:<org>/<repo>:ref:refs/heads/main` (**Q9**).
3. Attach a policy granting **only**: `s3:PutObject`, `s3:DeleteObject`,
`s3:ListBucket` on the site bucket, and `cloudfront:CreateInvalidation` on the
one distribution. Nothing else. No `s3:*`, no `cloudfront:*`.
4. Store the role ARN, bucket name, and distribution ID as repository
**variables** (they are not secrets), and reference them in the workflow.
## Cache policy
The mistake to avoid is caching HTML aggressively — a stale index page is a site
that does not update.
| Pattern | `Cache-Control` |
|---|---|
| `*.html` | `public, max-age=0, must-revalidate` |
| `/_astro/*` (hashed) | `public, max-age=31536000, immutable` |
| Fonts | `public, max-age=31536000, immutable` |
| Images | `public, max-age=604800` |
| `robots.txt`, `sitemap*.xml` | `public, max-age=3600` |
Sync in two passes: hashed assets first with the long TTL, then HTML with the
short one. Uploading HTML last means a user never fetches a new page whose assets
have not landed yet.
Invalidate `/*` on deploy. At this traffic volume the cost is nil, and partial
invalidation paths are a reliable source of confusing bugs.
## CloudFront configuration
- Origin: S3 with **Origin Access Control**, bucket not public. The guide's
Part 2.2 bucket policy already does this — verify it was not loosened.
- Redirect HTTP → HTTPS. TLS 1.2 minimum.
- Default root object `index.html`.
- **Custom error response:** 404 → `/404.html` with **response code 404**, not
200. Returning 200 for a missing page tells crawlers every bad URL is real
content, and it is the single most common misconfiguration in this stack.
- Compression on. Response-headers policy from `05-backend-spec.md`.
- A CloudFront Function for trailing-slash normalisation, so `/about` and
`/about/` do not both resolve as separate indexable URLs.
## Branch model
`main` is production; every push deploys. Work on short-lived branches, open a
PR, let CI build and run Lighthouse, merge.
**Pull request checks (blocking):** `npm run build` · `astro check` · lint ·
Lighthouse CI against the budgets in `04-seo-spec.md` · link check.
Tag every production deploy `v<year>.<n>` so a rollback has something to name.
## Rollback
1. Re-run the workflow at the last good tag, or
2. `git revert` and push, or
3. Restore from S3 object versioning — **enable versioning on the bucket if it is
off**; it is the difference between a rollback and a rebuild.
Then invalidate `/*`.
## Cutover checklist — D11 is a single shot, so run all of it
**Content and compliance**
- [ ] Every claim traced to `AGENTS.md` §4 Verified
- [ ] No `TODO(pouya)` remains in any shipped page
- [ ] No matter counts, rates, dollar figures, or testimonials anywhere
- [ ] Q.Arb described as in progress everywhere it appears
- [ ] `/fees/` carries real numbers (Q4) or the page does not ship
- [ ] Privacy policy matches the backend as actually built
**Technical**
- [ ] Lighthouse ≥ 95 mobile on `/`, `/about/`, a practice page, an article
- [ ] Every page renders fully with JavaScript disabled
- [ ] `curl` of each URL returns real content, not a shell
- [ ] All internal links resolve; no orphan pages
- [ ] Sitemap generated and correct; `robots.txt` served, not 403
- [ ] Rich Results Test passes; OG previews render in LinkedIn and Slack
- [ ] 404 returns a 404 status
- [ ] Security headers present (`securityheaders.com` A or better)
- [ ] **SES identities verified for sending** (Q18) — `aws sesv2 get-email-identity --email-identity smlcompany.ca` and confirm `VerifiedForSendingStatus: true`
- [ ] **SES out of the sandbox** (Q19) — `aws sesv2 get-account --query 'ProductionAccessEnabled'`. In sandbox, mail reaches only pre-verified addresses and the inquirer's confirmation silently fails
- [ ] Intake form tested end to end: DynamoDB record written to `adr-intake-submissions`, both emails delivered to a real inbox, TTL set
- [ ] Booking link works, including the no-JavaScript fallback
- [ ] Favicon set complete
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
- [ ] Tested at 320 px and at 200% zoom
**Infrastructure**
- [ ] S3 versioning enabled
- [ ] Bucket not publicly readable; OAC in force
- [ ] ACM certificate valid; Namecheap validation CNAME still present
- [ ] CloudWatch alarms: Lambda errors, DLQ depth, 5xx rate
- [ ] Billing alarm still active (guide Part 0.3)
**Post-cutover, same day**
- [ ] Sitemap submitted to Google Search Console and Bing Webmaster Tools
- [ ] Live site fetched as an anonymous crawler to confirm indexable content
- [ ] LinkedIn profile and ADRIC/ADRIO listings updated to point here
- [ ] Archive the old single-file build to `_archive/` — do not delete it
- [ ] `AGENTS.md` Change Log entry recording the cutover
+202
View File
@@ -0,0 +1,202 @@
# 07 — Fee research and recommended rate card
Authority: `AGENTS.md` §3 D8 (publish a full rate card) and D14 (two-tier
structure, **pending Pouya's sign-off — Q14**).
**Nothing in this document publishes until Pouya confirms the figures.** These
are researched recommendations, not decisions. This is business pricing
information, not legal or financial advice.
Research date: 2026-08-26. All figures below are **plus HST** unless stated.
---
## The Ontario market, measured
### The regulated floor
Ontario's mandatory mediation tariff (Rule 24.1) sets the roster rate. ADR
Chambers publishes it as **$600 to $825 depending on the number of parties**,
covering *"one half hour of preparation time per party and up to three hours of
mediation."*
This is the floor of the market, and it is a floor with a signal attached:
pricing at or near it reads as roster-filler work.
### Published hourly bands
ADR Chambers, across its full roster:
| | Range |
|---|---|
| Mediators | **$150 $750 / hour** *"depending on the experience of the mediator"* |
| Arbitrators | **$250 $800 / hour** |
Plus, on the arbitration side: an **$800 filing fee**, a **$800 venue fee** for a
hearing room and one breakout room, and **$400** per additional room.
### Published practitioner rate cards
Four Ontario practitioners publishing real numbers:
| Practice | Half-day | Full day | Overtime | Notes |
|---|---|---|---|---|
| **Patey** — Tier 1, PI / insurance | $800 (3 h) | $1,200 (6 h) | $250 / h | Multi-party 3 h $1,200; multi-party full day $2,400; pro forma to 1.5 h $500 |
| **Patey** — Tier 2, estate / employment / civil | $1,200 (3 h) | $2,400 (6 h) | $375 / h | Pre-mediation caucus $175 flat |
| **Zuber** — video | $1,800 (3 h) | $2,800 (6 h) | $500 / h | +$500 per additional party |
| **Zuber** — in person, GTA | — | $4,000 (6 h) | $500 / h | Eastern Ontario $3,500. Prep and travel included |
| **Carroll** — Ottawa | $1,750 (incl. 1.5 h prep) | $3,000 (incl. 2 h prep) | $400 / h | Arbitration day rate $3,000 |
### What the shape of that data says
Three observations that drive the recommendation.
1. **The market is already segmented by matter type, not only by seniority.**
Patey runs two published tiers off the same neutral. Insurance and PI work
clears around $800$1,200 a day; estate, employment, and civil work clears
$2,400 for the same hours. This is the single most useful structural fact in
the research.
2. **Prep time is a pricing lever, disclosed differently by everyone.** Carroll
bundles named hours (1.5 h and 2 h). Zuber bundles prep *and* travel. Patey
bundles neither and sells a caucus separately. Bundling explicitly reads as
more confident and removes an argument later.
3. **Additional parties are always priced, never absorbed.** $300$500 per party
beyond two is the norm, and a four-party construction mediation is materially
more work than a two-party one.
---
## Where this practice should sit
**Not at the floor.** Pouya's stack — JD, an operating role inside a litigation
and ADR boutique, Q.Med held, Q.Arb commenced, and a working engineering career —
is not a junior generalist profile. Entering at roster rates would anchor him
into SABS volume work and make the commercial rate very hard to raise later.
Published rates are close to unrecoverable once set: raising them looks
opportunistic, discounting privately never becomes public knowledge.
**Not at the top either.** $4,000-a-day in-person GTA rates belong to neutrals
with twenty years of name recognition. Asking that without an independent track
record invites a comparison he loses.
**The position is the upper-middle: at or just above Patey Tier 2, just below
Zuber and Carroll.** That reads as *credentialed and serious, priced to be taken
seriously, not yet a marquee name* — which is exactly true.
---
## The confirmed rate card
**Set by Pouya on 2026-08-26 (D14). This is the card. Build `/fees/` from it.**
He declined the two-tier structure and set one rate for all mediation matters.
All figures **plus HST**.
### Mediation — all matters, one rate
| Item | Fee |
|---|---|
| Half day — up to 3.5 h, including 2 h preparation | **$2,000** |
| Full day — up to 7 h, including 3 h preparation | **$4,000** |
| Each party beyond two | **$500** |
| Overtime, per hour | **$500** |
### Arbitration
Available now as co-arbitrator; sole appointments follow the Q.Arb designation,
commenced August 2026. The page must say so — see `03-content-spec.md`.
| Item | Fee |
|---|---|
| Hourly | **$500** |
| Hearing day | **$4,000** |
| Documents-only / expedited, flat — simple | **$6,500** |
| Documents-only / expedited, flat — complex | **$9,500** |
**No tribunal-secretary rate.** Removed by Pouya. Do not reinstate it, and do not
offer tribunal-secretary work on the site.
### Other services — hourly
Early neutral evaluation, settlement counsel, dispute-system design, and
pre-dispute technical advisory: **$500 / hour**.
### Cancellation — adopted as recommended
| When | Fee |
|---|---|
| More than 30 days before | No fee. Disbursements only |
| 15 30 days before | 50% of the booked fee |
| Fewer than 15 days before | 100% of the booked fee |
| Rescheduled with a new date fixed at the same time | No charge |
| Reserved time filled by another matter of equal or greater value | Waived |
### Terms to state on the page
- All fees plus HST.
- Shared equally between the parties unless they agree otherwise in writing.
- Payable on rendering; interest on overdue accounts at 5% per annum.
- **Video and in-person at the same rate.** Do not discount remote sessions —
the preparation is identical, and discounting teaches the market that the
session is the product.
- Travel outside the GTA billed separately or bundled at a stated day rate.
### All parameters confirmed
Q15, Q16, and Q17 were closed on 2026-08-26. **Preparation time is bundled and
must be stated on the page** — "including 2 hours of preparation", "including
3 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.
---
## Recorded dissent — for the 12-month review (R5)
Claude recommended a two-tier card; Pouya set a single rate. The reasoning is
recorded here so the 12-month review has something to test against, not to
re-open a settled decision.
**Where the single rate lands relative to the measured market:**
| Segment | Published market, full day | This card |
|---|---|---|
| Insurance / SABS / LAT | ~$1,200 $2,400 | **$4,000** |
| Commercial / civil / estate | ~$2,400 $3,000 | **$4,000** |
| Established GTA in person | ~$3,500 $4,000 | **$4,000** |
$4,000 is at the ceiling of the published Ontario market — level with Zuber's
in-person GTA rate, and roughly **three times** the going rate for the insurance
and SABS segment.
**The consequence worth watching.** The strategy brief (§IV.7) identifies
accident-benefits and LAT mediation as the highest realistic near-term volume,
flowing directly from the firm's existing practice. At $4,000 a day that segment
is priced out. This is a coherent choice — a premium specialist position that
forgoes volume — **provided the volume was not being counted on.** If early
appointment flow is slower than expected, the SABS tier is the first place to
look, and reintroducing a second tier is a cleaner fix than cutting the headline
rate.
**What makes the rate defensible.** $4,000 for a neutral who reads the contract,
the code, and the System Impact Assessment is a fair price. $4,000 for a
generalist is not. The rate and `/practice/technology/` are load-bearing for each
other, which is an argument for shipping them in the same release — and for the
Insights section carrying real technical depth rather than process explainers
alone.
**One thing the single rate gets right.** Published rates are close to
unrecoverable, and it is far easier to add a lower tier later than to raise a
headline rate. Setting the ceiling first and discounting privately preserves
more optionality than the reverse.
---
## Sources
- [ADR Chambers — Mediation Fees](https://adrchambers.com/mediation/fees/)
- [ADR Chambers — Roster Rate / Mandatory Mediations](https://adrchambers.com/roster-rate-mediation/)
- [ADR Chambers — Arbitration Fees](https://adrchambers.com/arbitration/fees/)
- [Patey Mediations — Rates & Cancellation](https://pateymediations.com/rates/)
- [Zuber Mediation — Fees](https://www.zubermediation.com/fees.html)
- [Carroll Mediation — Rates & Cancellation](https://www.carrollmediation.ca/?page_id=16)
- [O. Reg. 451/98 — Mediators' Fees (Rule 24.1)](https://www.canlii.org/en/on/laws/regu/o-reg-451-98/latest/o-reg-451-98.html)