chore: project scaffold, specs, and working record
Build and deploy / build-and-deploy (push) Failing after 5s
Build and deploy / build-and-deploy (push) Failing after 5s
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Copy to .env.local for development. Never commit .env files.
|
||||||
|
|
||||||
|
# Canonical site URL — drives canonical tags, OG tags, and the sitemap.
|
||||||
|
PUBLIC_SITE_URL=https://adr.smlcompany.ca
|
||||||
|
|
||||||
|
# Intake form endpoint — API Gateway HTTP API 'adr-intake-api', ca-central-1.
|
||||||
|
# [verified 2026-08-26]
|
||||||
|
PUBLIC_INTAKE_ENDPOINT=https://4tl0m5igkj.execute-api.ca-central-1.amazonaws.com
|
||||||
|
|
||||||
|
# Analytics — privacy-first and cookieless (AGENTS.md D15).
|
||||||
|
# Plausible or Fathom. No GA4, no consent banner. Leave blank in development.
|
||||||
|
PUBLIC_ANALYTICS_PROVIDER=plausible
|
||||||
|
PUBLIC_ANALYTICS_DOMAIN=adr.smlcompany.ca
|
||||||
|
|
||||||
|
# Booking embed — PARKED by Pouya, 2026-08-26 (AGENTS.md R6).
|
||||||
|
# /contact/ ships with the intake form and a reserved slot; leave this empty
|
||||||
|
# and the booking section does not render. Adding a URL later is a drop-in.
|
||||||
|
PUBLIC_BOOKING_URL=
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Gitea Actions — the live pipeline for this repository.
|
||||||
|
#
|
||||||
|
# Gitea Actions speaks GitHub Actions syntax, so this is a near-direct port of
|
||||||
|
# .github/workflows/deploy.yml (kept as the OIDC reference in case the repo ever
|
||||||
|
# moves to GitHub or GitLab).
|
||||||
|
#
|
||||||
|
# ONE REAL DIFFERENCE: Gitea is not an AWS OIDC provider, so there is no role to
|
||||||
|
# assume. Deploys authenticate with a SCOPED IAM USER whose key lives only in
|
||||||
|
# this repository's Gitea secrets. See docs/06-deployment.md for the exact IAM
|
||||||
|
# policy — it grants four actions on one bucket and one distribution, nothing
|
||||||
|
# more. Rotate the key quarterly; OIDC would have made that unnecessary.
|
||||||
|
#
|
||||||
|
# Requires a Gitea Actions runner registered to this repo or its organisation.
|
||||||
|
|
||||||
|
name: Build and deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }}
|
||||||
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
||||||
|
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Type and template check
|
||||||
|
run: npm run check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
||||||
|
PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
||||||
|
PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
||||||
|
|
||||||
|
# Some Gitea runner images ship without the AWS CLI. Install if missing.
|
||||||
|
- name: Ensure AWS CLI
|
||||||
|
run: |
|
||||||
|
if ! command -v aws >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
|
||||||
|
unzip -q /tmp/awscliv2.zip -d /tmp
|
||||||
|
sudo /tmp/aws/install --update
|
||||||
|
fi
|
||||||
|
aws --version
|
||||||
|
|
||||||
|
- name: Verify credentials
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
# Two passes: hashed immutable assets first, HTML last. A visitor must
|
||||||
|
# never fetch a new page whose assets have not landed yet.
|
||||||
|
- name: Sync hashed assets
|
||||||
|
run: |
|
||||||
|
aws s3 sync ./dist "s3://${S3_BUCKET}" \
|
||||||
|
--exclude "*" \
|
||||||
|
--include "_astro/*" --include "fonts/*" \
|
||||||
|
--cache-control "public, max-age=31536000, immutable" \
|
||||||
|
--no-progress
|
||||||
|
|
||||||
|
- name: Sync images
|
||||||
|
run: |
|
||||||
|
aws s3 sync ./dist "s3://${S3_BUCKET}" \
|
||||||
|
--exclude "*" \
|
||||||
|
--include "*.avif" --include "*.webp" --include "*.jpg" \
|
||||||
|
--include "*.png" --include "*.svg" \
|
||||||
|
--cache-control "public, max-age=604800" \
|
||||||
|
--no-progress
|
||||||
|
|
||||||
|
- name: Sync HTML and the rest
|
||||||
|
run: |
|
||||||
|
aws s3 sync ./dist "s3://${S3_BUCKET}" \
|
||||||
|
--exclude "_astro/*" --exclude "fonts/*" \
|
||||||
|
--cache-control "public, max-age=0, must-revalidate" \
|
||||||
|
--delete --no-progress
|
||||||
|
|
||||||
|
- name: Invalidate CloudFront
|
||||||
|
run: |
|
||||||
|
aws cloudfront create-invalidation \
|
||||||
|
--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
|
||||||
|
--paths "/*"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: echo "Deployed to https://adr.smlcompany.ca — commit ${GITHUB_SHA:0:7}"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# REFERENCE ONLY. This repository lives on self-hosted Gitea (AGENTS.md D3).
|
||||||
|
# The live pipeline is .gitea/workflows/deploy.yml.
|
||||||
|
#
|
||||||
|
# This file is kept because it is the better design: GitHub OIDC issues a
|
||||||
|
# short-lived token per run instead of a static key. If the project ever moves
|
||||||
|
# to GitHub or GitLab, use this and delete the static IAM user.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
name: Build and deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# OIDC role assumption — no long-lived AWS credentials in this repository.
|
||||||
|
# See docs/06-deployment.md for the one-time IAM setup.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Type and template check
|
||||||
|
run: npm run check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
||||||
|
PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
||||||
|
PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
||||||
|
|
||||||
|
# TODO(pouya): AGENTS.md Q9, Q10 — set these repository variables:
|
||||||
|
# AWS_DEPLOY_ROLE_ARN, AWS_REGION, S3_BUCKET, CLOUDFRONT_DISTRIBUTION_ID
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
||||||
|
aws-region: ${{ vars.AWS_REGION }}
|
||||||
|
|
||||||
|
# Two passes: hashed immutable assets first, HTML last. A visitor must
|
||||||
|
# never fetch a new page whose assets have not landed yet.
|
||||||
|
- name: Sync hashed assets
|
||||||
|
run: |
|
||||||
|
aws s3 sync ./dist "s3://${{ vars.S3_BUCKET }}" \
|
||||||
|
--exclude "*" \
|
||||||
|
--include "_astro/*" --include "fonts/*" \
|
||||||
|
--cache-control "public, max-age=31536000, immutable" \
|
||||||
|
--no-progress
|
||||||
|
|
||||||
|
- name: Sync images
|
||||||
|
run: |
|
||||||
|
aws s3 sync ./dist "s3://${{ vars.S3_BUCKET }}" \
|
||||||
|
--exclude "*" \
|
||||||
|
--include "*.avif" --include "*.webp" --include "*.jpg" \
|
||||||
|
--include "*.png" --include "*.svg" \
|
||||||
|
--cache-control "public, max-age=604800" \
|
||||||
|
--no-progress
|
||||||
|
|
||||||
|
- name: Sync HTML and the rest
|
||||||
|
run: |
|
||||||
|
aws s3 sync ./dist "s3://${{ vars.S3_BUCKET }}" \
|
||||||
|
--exclude "_astro/*" --exclude "fonts/*" \
|
||||||
|
--cache-control "public, max-age=0, must-revalidate" \
|
||||||
|
--delete --no-progress
|
||||||
|
|
||||||
|
- name: Invalidate CloudFront
|
||||||
|
run: |
|
||||||
|
aws cloudfront create-invalidation \
|
||||||
|
--distribution-id "${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}" \
|
||||||
|
--paths "/*"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: echo "Deployed to https://adr.smlcompany.ca — commit ${GITHUB_SHA::7}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
.output/
|
||||||
|
.vercel/
|
||||||
|
.netlify/
|
||||||
|
|
||||||
|
# astro
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# environment — never commit
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# credentials — never commit
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
aws-credentials*
|
||||||
|
.aws/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# editor / os
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# test / tooling caches
|
||||||
|
coverage/
|
||||||
|
.eslintcache
|
||||||
|
.cache/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|
||||||
|
# generated inventory — safe to share, but not tracked
|
||||||
|
aws-inventory.txt
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
# Agent Working File
|
||||||
|
|
||||||
|
This file is a **living document** and a **full history tree** for this project —
|
||||||
|
not a snapshot. Any agent or person working here (Claude in chat, Claude Code,
|
||||||
|
Claude Cowork, or a human) maintains it by the rules below. Everything else in
|
||||||
|
this file varies by project; these rules do not.
|
||||||
|
|
||||||
|
## How to maintain this file
|
||||||
|
|
||||||
|
1. **Document everything.** On every change, record what was discussed, decided,
|
||||||
|
changed, or planned — decisions and plans included, not just executed work.
|
||||||
|
|
||||||
|
2. **Never overwrite or delete history.** When something changes, update the
|
||||||
|
relevant Current Truth section in place **and** append a dated Change Log
|
||||||
|
entry capturing old → new and why.
|
||||||
|
|
||||||
|
3. **Two parts, kept separate:** *Current Truth* is updated in place and always
|
||||||
|
reflects the present state; the *Change Log* is append-only, newest entry
|
||||||
|
first, and is never edited retroactively.
|
||||||
|
|
||||||
|
4. **Stamp facts** as `[verified YYYY-MM-DD]` or `[assumed]`. When you re-check a
|
||||||
|
fact, re-stamp it with today's date; a stale date means it needs re-verifying.
|
||||||
|
|
||||||
|
5. **Never prune the Change Log.** If the file gets unwieldy, ask before
|
||||||
|
archiving old entries to `AGENTS-history.md`. History is not destroyed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Current Truth
|
||||||
|
|
||||||
|
## 1. What this project is
|
||||||
|
|
||||||
|
A ground-up rebuild of **adr.smlcompany.ca**, the personal-brand website of the
|
||||||
|
alternative dispute resolution practice of **Pouya Lajevardi**. `[verified 2026-08-25]`
|
||||||
|
|
||||||
|
The brand's centre of gravity is the **practitioner**, not the firm and not
|
||||||
|
SML Company Ltd. Legal background, firm affiliation, and the operating company
|
||||||
|
are credibility signals referenced in support; they are not the subject.
|
||||||
|
`[verified 2026-08-25 — source: PL_ADR_Personal_Branding_Strategy_Brief.docx]`
|
||||||
|
|
||||||
|
**Positioning in one sentence:** a credentialed neutral who is also a working
|
||||||
|
litigator and a practising machine-learning / infrastructure engineer — a
|
||||||
|
combination that is close to absent from Canadian ADR rosters, and which is the
|
||||||
|
practice's economic moat. `[verified 2026-08-25 — source: strategy brief §I, §V]`
|
||||||
|
|
||||||
|
**Time horizon:** the strategy brief plans a 24-month compounding arc. The site
|
||||||
|
is built to be added to continuously, not shipped once. `[verified 2026-08-25]`
|
||||||
|
|
||||||
|
## 2. State of the thing being replaced
|
||||||
|
|
||||||
|
The site live at `adr.smlcompany.ca` as of this entry: `[verified 2026-08-25]`
|
||||||
|
|
||||||
|
- It is a **single 2.2 MB HTML file** containing an entire React application,
|
||||||
|
compiled **in the browser at runtime** by Babel Standalone loaded from unpkg.
|
||||||
|
- React and ReactDOM are loaded as **development** builds from a public CDN.
|
||||||
|
- **Crawlers see almost nothing.** A server-side fetch of the URL returns only
|
||||||
|
`SML Company`, `DISPUTE RESOLUTION`, and the loading string `Unpacking...`.
|
||||||
|
Every word of real content is assembled client-side after three CDN round
|
||||||
|
trips. `[verified 2026-08-25 — direct fetch]`
|
||||||
|
- The deployed `<head>` has **no `<meta name="viewport">`**, no meta
|
||||||
|
description, no Open Graph or Twitter card tags, no canonical URL, and no
|
||||||
|
favicon. `[verified 2026-08-25]`
|
||||||
|
- `robots.txt` returns **403**. There is no sitemap. `[verified 2026-08-25]`
|
||||||
|
- The two logo PNGs are ~1 MB combined and are **base64-inlined** into the HTML,
|
||||||
|
so they cannot be cached separately from the page. `[verified 2026-08-25]`
|
||||||
|
- `<title>` is still `SML Company · Dispute Resolution` — the pre-rebrand
|
||||||
|
placeholder title. `[verified 2026-08-25]`
|
||||||
|
- There is **no version control, no build step, and no test suite**. The
|
||||||
|
publishing mechanism is a Python script (`rebuild-standalone.py`) that inlines
|
||||||
|
the JSX components into the single file. `[verified 2026-08-25]`
|
||||||
|
- The May 2026 content brief was **partially applied** — the JSX components
|
||||||
|
under `components-standalone/` contain the corrected Pouya-branded copy, but
|
||||||
|
the document `<head>` and some structural defaults still carry placeholder
|
||||||
|
content. `[verified 2026-08-25]`
|
||||||
|
|
||||||
|
**Consequence, stated plainly:** for a site whose entire purpose is to be found
|
||||||
|
by counsel, general counsel, and appointing bodies searching for a neutral, the
|
||||||
|
current architecture is close to a worst case. This rebuild is justified on
|
||||||
|
architecture alone, independent of any content or design opinion.
|
||||||
|
`[verified 2026-08-25]`
|
||||||
|
|
||||||
|
## 3. Locked decisions
|
||||||
|
|
||||||
|
Decided with Pouya on 2026-08-25. Each of these is settled; do not re-open one
|
||||||
|
without an explicit instruction and a Change Log entry. `[verified 2026-08-25]`
|
||||||
|
|
||||||
|
| # | Decision | Chosen | Rejected alternatives |
|
||||||
|
|---|---|---|---|
|
||||||
|
| D1 | Framework | **Astro**, static output | Next.js; patching the single-file build; hand-written HTML |
|
||||||
|
| D2 | Content scope | **Full re-architecture** — new IA, new pages, all copy rewritten | Copy rewrite on existing structure; audit only; technical layer only |
|
||||||
|
| D3 | Hosting & CI/CD | **Git repo + Gitea Actions → existing S3 + CloudFront.** *Amended 2026-08-26:* self-hosted **Gitea**, repo `adr-sml`. Gitea Actions uses GitHub Actions syntax, so the workflow ports nearly as is — but Gitea is not an AWS OIDC provider, so deploys authenticate with a **scoped IAM user**, rotated quarterly | GitHub Actions + OIDC; Terraform/CDK IaC; Amplify; manual deploys |
|
||||||
|
| D4 | Languages | **English only** | Full EN/FA bilingual; EN + one Farsi page; EN with FA scaffolding |
|
||||||
|
| D5 | Page structure | **Full multi-page**, ~20 URLs | Lean six-page; rich home + a few deep pages |
|
||||||
|
| D6 | Primary audience | **All four tiers**, served by different surfaces (see §5) | Single-audience focus |
|
||||||
|
| D7 | Visual design | **Keep palette and infinity mark; modernize the execution** | Faithful port; strip motion only; fresh design direction |
|
||||||
|
| D8 | Fees | **Full rate card published on a `/fees/` page** | Ranges only; on request; gated PDF |
|
||||||
|
| D9 | Insights | **Build it and launch with 3–5 drafted pieces**, every word reviewed by Pouya before publication | Launch empty; no Insights section |
|
||||||
|
| D10 | Intake | **Rebuilt intake form + calendar booking** for the confidential intake call | Structured questionnaire; form only; email/phone only |
|
||||||
|
| D11 | Rollout | **Build everything, one clean cutover.** The current site stays live untouched until replaced | Patch live site first; staging subdomain; take site down |
|
||||||
|
| D12 | Agent working file | **This file**, maintained under the constitution above | Ad hoc notes |
|
||||||
|
| D13 | Licensure claims | **The site asserts the JD only.** No claim of licensure, call to the bar, or entitlement to practise law. The approved phrasing for the boutique role is **"active litigation exposure"** or **"involvement in litigation and ADR matters"** — never "practice". Pouya's direction, 2026-08-26. **Explicitly interim — see Standing Reminders §12** | Stating LSO licence status; the word "practice" |
|
||||||
|
| D14 | Fee structure | **Single published rate card, all mediation matters at one rate.** Confirmed by Pouya 2026-08-26; figures in `docs/07-fees.md`. No tribunal-secretary line | Two-tier card (recommended by Claude, declined); ranges; fees on request |
|
||||||
|
| D15 | Analytics | **Privacy-first and cookieless** (Plausible or Fathom). No cookie banner, nothing to consent to, one line in the privacy policy | GA4; no analytics at all |
|
||||||
|
| D16 | Naming the boutique | **Never named.** Referred to throughout as *a Toronto litigation and ADR boutique*. Pouya's decision, 2026-08-26 | Naming the firm |
|
||||||
|
|
||||||
|
## 4. Credential and claim register
|
||||||
|
|
||||||
|
**This is the most important section in this file.** Pouya is a licensed legal
|
||||||
|
professional. Every public claim on this site is subject to Law Society of
|
||||||
|
Ontario marketing rules, and the previous version of the site carried fabricated
|
||||||
|
credentials, invented matter values, and a fictitious testimonial. That must
|
||||||
|
never recur.
|
||||||
|
|
||||||
|
### Governing rule
|
||||||
|
|
||||||
|
> **No claim reaches a public page unless it appears in the Verified column
|
||||||
|
> below.** If a page needs a fact that is not here, stop and ask Pouya. Do not
|
||||||
|
> infer it, do not soften it into something defensible, and do not carry it over
|
||||||
|
> from the old site.
|
||||||
|
|
||||||
|
### Verified — may be published
|
||||||
|
|
||||||
|
Sourced from Pouya's own strategy brief of 2026-05-26. Self-reported by the
|
||||||
|
subject, which makes them reliable as to substance, but **currency is not
|
||||||
|
confirmed** — several are on a moving credentialing track and may have advanced
|
||||||
|
since May.
|
||||||
|
|
||||||
|
| Claim | Status |
|
||||||
|
|---|---|
|
||||||
|
| Pouya Lajevardi, JD, Bond University | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| ~~Lawyer; Law Society of Ontario member~~ | **DO NOT PUBLISH.** Pouya directed on 2026-08-26 that licensure is left out of the site entirely; the JD is asserted, nothing further. See D13 and the Forbidden table below `[verified 2026-08-26]` |
|
||||||
|
| Director of Firm Operations, Toronto litigation and ADR boutique | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Active litigation practice: personal injury, construction, regulatory (POA), insurance (SABS) | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Q.Med designation through ADRIC / ADRIO | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Has completed multiple sole mediations | `[verified 2026-08-25 — strategy brief §I]` — count deliberately not published, see below |
|
||||||
|
| Q.Arb credentialing pathway — **commenced August 2026** | `[verified 2026-08-26 — Pouya]`. Describe as newly commenced, never as held or nearing completion |
|
||||||
|
| C.Med-Arb stated as long-term designation goal | `[verified 2026-08-25 — strategy brief §I, §IV]` |
|
||||||
|
| Kompass Arbitration Certificate Program — completed | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Stitt Feld Handy negotiation and ADR workshop sequence — completed | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Practising machine-learning and DevOps / infrastructure engineer | `[verified 2026-08-25 — strategy brief §I, §V]` |
|
||||||
|
| Bilingual English and Farsi | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Iranian-Canadian; cross-cultural fluency with diaspora business communities | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Operator of SML Company Ltd. alongside the practice | `[verified 2026-08-25 — strategy brief §I]` |
|
||||||
|
| Memberships: **ADRIC**, **ADRIO**, **OBA sections** | `[verified 2026-08-26 — Pouya]`. Which OBA sections is still `[assumed]` — the brief names Construction & Infrastructure, ADR, and Civil Litigation |
|
||||||
|
| ~~OCNI~~ | **Not current. Do not publish** `[verified 2026-08-26 — Pouya]` |
|
||||||
|
| ~~LSO~~ | **Do not publish.** Listing the Law Society among memberships implies licensure, which D13 bars. Excluded deliberately, not by oversight `[verified 2026-08-26]` |
|
||||||
|
| Toronto, Ontario; by appointment | `[verified 2026-08-26]` |
|
||||||
|
| Contact: `info@smlcompany.ca`; no public phone number; consultations by scheduled call | `[verified 2026-08-26 — Pouya]` |
|
||||||
|
| LinkedIn: `https://www.linkedin.com/in/pouyalajevardi/` | `[verified 2026-08-26 — Pouya]` |
|
||||||
|
| The Toronto litigation and ADR boutique **may be named on the site** | `[verified 2026-08-26 — Pouya]`; the name itself is pending, see Q7 |
|
||||||
|
|
||||||
|
### Forbidden — must not appear anywhere
|
||||||
|
|
||||||
|
| Never publish | Why |
|
||||||
|
|---|---|
|
||||||
|
| Any count of matters closed, hours mediated, or years in ADR practice | Practice is new. Small true numbers do not persuade a sophisticated GC and invite scrutiny. Save for one-to-one appointment proposals where context travels with the figure. `[verified 2026-08-25 — content brief, "Rule of thumb on numbers"]` |
|
||||||
|
| Settlement rates, resolution percentages, median time to award | No underlying data exists |
|
||||||
|
| Aggregate value resolved; any dollar figure attached to past matters | No underlying data exists |
|
||||||
|
| Named or describable past matters | Confidentiality, and none are publishable |
|
||||||
|
| Testimonials, endorsements, quotes from counterparties | None exist. The prior site's testimonial was fabricated |
|
||||||
|
| "Since 2009", "sixteen years", London / New York offices, Co. № 07452218 | Artefacts of the placeholder template. All false |
|
||||||
|
| The name "S. M. Lawrence" | Fictitious founder from the template |
|
||||||
|
| Guarantees of outcome, or superlatives ("best", "leading", "top-rated") | LSO marketing rules |
|
||||||
|
| The word **"lawyer"** used of Pouya; "called to the bar"; "licensed"; "my law practice"; "my litigation practice"; any post-nominal implying a licence | D13. The site asserts the JD and nothing further |
|
||||||
|
| Any phrasing that *implies* entitlement to practise law without saying so — "acts for clients", "represents parties", "my clients", "legal advice" | Same rule. Implication is the risk, not just the word. Describe the role factually instead: *Director of Firm Operations at a Toronto litigation and ADR boutique*, and the matter types worked on |
|
||||||
|
| Q.Arb described as held, imminent, or "nearly complete" | It commenced August 2026 |
|
||||||
|
|
||||||
|
### The substitution principle
|
||||||
|
|
||||||
|
Wherever the design wants a "how much / how many" statistic, substitute a
|
||||||
|
**longer-arc credential** — something already substantial and true at launch that
|
||||||
|
does not grow by closing files. The approved stat set is `Q.Med` /
|
||||||
|
`JD + ML` / `EN · FA`, with `Q.Arb` as a fourth slot where one exists.
|
||||||
|
`[verified 2026-08-25 — content brief]`
|
||||||
|
|
||||||
|
## 5. Audience model
|
||||||
|
|
||||||
|
All four tiers matter, but they are served by different surfaces rather than by
|
||||||
|
diluting every page into a compromise. `[verified 2026-08-25 — decision D6]`
|
||||||
|
|
||||||
|
| Tier | Reads | Wants | Primary surface |
|
||||||
|
|---|---|---|---|
|
||||||
|
| In-house / general counsel | Home, practice pages | Credentials, subject-matter fluency, procedure, predictable cost | Home + `/practice/*` |
|
||||||
|
| Referring lawyers and litigation boutiques | Practice pages, fees, process | Availability, rules familiarity, rate card, confidence you won't embarrass them | `/fees/`, `/process/`, `/mediation/` |
|
||||||
|
| Appointing bodies and ADR institutions | About, credentials | A verifiable credential record and evidence of depth | `/about/` |
|
||||||
|
| Self-represented parties and SMEs | Plain-language layer | What mediation actually is, what it costs, what happens | `/for-parties/` |
|
||||||
|
|
||||||
|
Realistically, **referring counsel are the largest source of early
|
||||||
|
appointments**. Where two audiences conflict on a page, resolve toward counsel.
|
||||||
|
`[assumed]`
|
||||||
|
|
||||||
|
## 6. Architecture
|
||||||
|
|
||||||
|
Full sitemap, URL map, and per-page content outline: **`docs/01-architecture.md`**.
|
||||||
|
|
||||||
|
Summary: ~20 static pages. Home; About; three process pages (Mediation,
|
||||||
|
Arbitration, Med-Arb); six practice-area pages; Process; Fees; For Parties;
|
||||||
|
Insights index and articles; Contact; Privacy; Terms.
|
||||||
|
|
||||||
|
**Deliberate omission:** there is no Indigenous engagement / IBA practice page at
|
||||||
|
launch, though the strategy brief rates that niche as strategically the most
|
||||||
|
valuable. Publishing a practice page for it before the multi-year relationship
|
||||||
|
work described in brief §III.4 has been done would read as overreach to exactly
|
||||||
|
the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — decision recorded here, not yet discussed with Pouya]`
|
||||||
|
|
||||||
|
## 7. Environment and stack
|
||||||
|
|
||||||
|
| Thing | Value |
|
||||||
|
|---|---|
|
||||||
|
| Framework | Astro, `output: 'static'` `[verified 2026-08-25 — D1]` |
|
||||||
|
| Node | 22 LTS, pinned in `.nvmrc` `[assumed]` |
|
||||||
|
| Styling | Plain CSS with custom properties. No Tailwind, no CSS-in-JS `[verified 2026-08-25]` |
|
||||||
|
| Client JS | Astro islands only, where genuinely needed. Target: most pages ship zero JS `[verified 2026-08-25]` |
|
||||||
|
| Content | Astro content collections, MDX for Insights `[verified 2026-08-25]` |
|
||||||
|
| Fonts | Instrument Serif + Geist + Geist Mono, **self-hosted**, `font-display: swap` `[verified 2026-08-25]` |
|
||||||
|
| AWS account | `327082975128` `[verified 2026-08-26 — inventory]` |
|
||||||
|
| Region | **`ca-central-1`** throughout — hosting, Lambda, DynamoDB `[verified 2026-08-26]` |
|
||||||
|
| S3 bucket | **`adr-smlcompany-site`** — versioning **Enabled**, so rollback works `[verified 2026-08-26]` |
|
||||||
|
| CloudFront | **`E1OK7G98KNKUTA`**, alias `adr.smlcompany.ca`, origin `adr-smlcompany-site.s3.ca-central-1.amazonaws.com`, Deployed `[verified 2026-08-26]` |
|
||||||
|
| ACM certificate | `arn:aws:acm:us-east-1:327082975128:certificate/2b6d5bdf-6790-430c-9b82-c00ab66e6d87` — ISSUED `[verified 2026-08-26]` |
|
||||||
|
| Intake API | `adr-intake-api`, HTTP API `4tl0m5igkj`, endpoint `https://4tl0m5igkj.execute-api.ca-central-1.amazonaws.com` `[verified 2026-08-26]` |
|
||||||
|
| Intake Lambda | `adr-intake-handler`, `nodejs24.x` `[verified 2026-08-26]` |
|
||||||
|
| Intake table | `adr-intake-submissions` (DynamoDB, ca-central-1) `[verified 2026-08-26]` |
|
||||||
|
| SES identities | Domain `smlcompany.ca`; addresses `info@`, `intake@`, `adr@`. **Sending status unconfirmed — Q18/Q19** `[verified 2026-08-26]` |
|
||||||
|
| TLS | ACM certificate `[verified 2026-08-25 — AWS-Hosting-Guide.md]` |
|
||||||
|
| DNS | **Namecheap**, not Route 53 `[verified 2026-08-25 — AWS-Hosting-Guide.md]` |
|
||||||
|
| Intake backend | API Gateway (HTTP API) → Lambda → DynamoDB, notifications via SES `[verified 2026-08-25 — AWS-Hosting-Guide.md]` |
|
||||||
|
| Repository | **`adr-sml`**, self-hosted **Gitea**. Local clone at `/Users/pouya/Dev/Websites/adr-sml` `[verified 2026-08-26]` |
|
||||||
|
| CI/CD | **Gitea Actions**, `.gitea/workflows/deploy.yml`. `.github/workflows/deploy.yml` kept as the OIDC reference in case the repo ever moves. Credentials: scoped IAM user in Gitea secrets — **no OIDC available** `[verified 2026-08-26 — D3 as amended]` |
|
||||||
|
| Analytics | **Plausible or Fathom** — cookieless, no personal data, no consent banner, EU-hosted `[verified 2026-08-26 — D15]` |
|
||||||
|
|
||||||
|
## 8. Design system
|
||||||
|
|
||||||
|
Full token set, type scale, motion rules, and contrast results:
|
||||||
|
**`docs/02-design-system.md`**.
|
||||||
|
|
||||||
|
Palette and infinity mark carry over unchanged. Execution is modernized:
|
||||||
|
fluid type scale, self-hosted fonts, an 8 px spacing scale, motion gated behind
|
||||||
|
`prefers-reduced-motion`, and one hard accessibility constraint discovered by
|
||||||
|
measurement:
|
||||||
|
|
||||||
|
> **Gold `#c9a876` on cream `#faf7f2` measures 2.10:1 contrast.** That fails WCAG
|
||||||
|
> AA for body text (4.5:1) and for large text (3:1). Gold is a decorative and
|
||||||
|
> on-dark colour only. On maroon it measures 5.84:1 and on ink 8.00:1, both of
|
||||||
|
> which pass. `[verified 2026-08-25 — computed]`
|
||||||
|
|
||||||
|
## 9. Open questions — blocking
|
||||||
|
|
||||||
|
Nothing below can be invented. Each needs an answer from Pouya.
|
||||||
|
|
||||||
|
| # | Question | Blocks |
|
||||||
|
|---|---|---|
|
||||||
|
| ~~Q1~~ | **ANSWERED 2026-08-26.** Licensure is left out entirely; the site asserts the JD only. See D13 | — |
|
||||||
|
| ~~Q2~~ | **ANSWERED 2026-08-26.** Q.Arb commenced August 2026 | — |
|
||||||
|
| ~~Q3~~ | **ANSWERED 2026-08-26.** Email `info@smlcompany.ca`. No public phone — "By scheduled call". Location: Toronto · Ontario · By appointment | — |
|
||||||
|
| ~~Q4 / Q14~~ | **ANSWERED 2026-08-26.** Rate card confirmed by Pouya — see D14 and `docs/07-fees.md` | — |
|
||||||
|
| ~~Q13~~ | **ANSWERED 2026-08-26.** Self-hosted Gitea with Gitea Actions | — |
|
||||||
|
| ~~Q15 / Q16 / Q17~~ | **ANSWERED 2026-08-26.** Non-mediation hourly $500. Prep bundled: 2 h in the half day, 3 h in the full day, **stated on the page**. Overtime $500/h | — |
|
||||||
|
| Q5 | Booking tool — **parked 2026-08-26 at Pouya's request.** Build `/contact/` with the form only and a clean slot for the embed. Now tracked as standing reminder R6 | `/contact/` — non-blocking |
|
||||||
|
| ~~Q6~~ | **ANSWERED 2026-08-26.** Supplied and committed: `src/assets/pouya-lajevardi.jpg` (1600×1600 master) and `src/assets/og-portrait.jpg` (1200×630 link-preview crop) | — |
|
||||||
|
| ~~Q7~~ | **ANSWERED 2026-08-26.** Pouya reverted to generic. The boutique is **never named**; refer to it as *a Toronto litigation and ADR boutique* throughout | — |
|
||||||
|
| ~~Q8~~ | **ANSWERED 2026-08-26.** ADRIC, ADRIO, OBA sections. Not OCNI. Not LSO (see §4) | — |
|
||||||
|
| ~~Q9~~ | **ANSWERED 2026-08-26.** Gitea, repo `adr-sml`, clone at `/Users/pouya/Dev/Websites/adr-sml` | — |
|
||||||
|
| ~~Q10~~ | **ANSWERED 2026-08-26.** Full inventory captured; values in §7 | — |
|
||||||
|
| Q18 | **Are the SES identities actually verified for sending?** `list-email-identities` returned `VerifiedForSendingStatus: None` for all six. If they are unverified, no intake email sends | Intake confirmation and notification email |
|
||||||
|
| Q19 | **Is the SES account still in the sandbox?** In sandbox, mail only reaches pre-verified addresses — the confirmation email to an inquirer would silently fail | `/contact/` going live |
|
||||||
|
| ~~Q11~~ | **ANSWERED 2026-08-26.** Privacy-first, cookieless — Plausible or Fathom. No GA4, no consent banner | — |
|
||||||
|
| ~~Q12~~ | **ANSWERED 2026-08-26.** `https://www.linkedin.com/in/pouyalajevardi/` | — |
|
||||||
|
|
||||||
|
## 10. Risks
|
||||||
|
|
||||||
|
| Risk | Severity | Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| A fabricated or unverifiable claim reaches a public page | **High** — professional-conduct exposure for a licensed practitioner | §4 register; every claim traced to a source before it ships |
|
||||||
|
| Copy silently carried over from the placeholder template | High | Nothing is ported verbatim. All copy written fresh against `docs/03-content-spec.md` |
|
||||||
|
| Insights section launches and then goes stale | Medium | D9 commits to 3–5 pieces at launch and a monthly cadence. A dead blog is worse than no blog |
|
||||||
|
| Personal data in the intake pipeline without a retention policy | Medium — PIPEDA | `docs/05-backend-spec.md` sets retention, and `/legal/privacy/` states it |
|
||||||
|
| Cutover breaks the live site (D11 is a single-shot deploy) | Medium | Full pre-cutover checklist in `docs/06-deployment.md`; CloudFront can be rolled back to the prior origin path |
|
||||||
|
| Twenty pages of thin copy rank worse than six good ones | Medium | Each page must justify itself with substantive content. Ship fewer pages rather than padded ones |
|
||||||
|
| **Deploy-credential blast radius.** AWS account `327082975128` is not a single-project account. It also holds `meshkinilaw.ca` and its preview site, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and **`mlp-clientdb-prod-backups`** — which by its name is a law firm's production client-database backups | **High** | A static deploy key for a personal website must never be able to reach a client database. The scoped IAM policy in `docs/06-deployment.md` grants four actions on one bucket and one distribution and nothing else — that narrowness is now load-bearing, not hygiene. Never widen it. Never reuse the `user/pouya` credentials in CI `[verified 2026-08-26 — inventory]` |
|
||||||
|
| SES not verified or still sandboxed | Medium | Q18/Q19. A silently undelivered confirmation email looks to the inquirer like being ignored |
|
||||||
|
|
||||||
|
## 11. Glossary
|
||||||
|
|
||||||
|
| Term | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| ADR | Alternative dispute resolution |
|
||||||
|
| ADRIC | ADR Institute of Canada — national credentialing body |
|
||||||
|
| ADRIO | ADR Institute of Ontario — provincial affiliate |
|
||||||
|
| Q.Med | Qualified Mediator — ADRIC/ADRIO designation |
|
||||||
|
| Q.Arb | Qualified Arbitrator — ADRIC/ADRIO designation |
|
||||||
|
| C.Med-Arb | Chartered Mediator-Arbitrator — senior hybrid designation; the long-term goal |
|
||||||
|
| Med-Arb | Hybrid process: mediation that converts to binding arbitration if unresolved |
|
||||||
|
| SABS | Statutory Accident Benefits Schedule — Ontario auto insurance benefits |
|
||||||
|
| LAT | Licence Appeal Tribunal — hears Ontario SABS disputes |
|
||||||
|
| POA | Provincial Offences Act |
|
||||||
|
| ENE | Early neutral evaluation |
|
||||||
|
| LSO | Law Society of Ontario |
|
||||||
|
| OBA | Ontario Bar Association |
|
||||||
|
| IESO | Independent Electricity System Operator |
|
||||||
|
| OEB | Ontario Energy Board |
|
||||||
|
| SMR | Small modular reactor |
|
||||||
|
| PIPEDA | Personal Information Protection and Electronic Documents Act |
|
||||||
|
|
||||||
|
## 12. Standing reminders
|
||||||
|
|
||||||
|
**Surface these to Pouya at the start of any substantial session.** They are
|
||||||
|
open questions he has deliberately parked, not settled matters. The point of
|
||||||
|
this section is that a parked decision does not quietly become permanent by
|
||||||
|
never being raised again.
|
||||||
|
|
||||||
|
| # | Reminder | Raised | Why it must keep coming back |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R1 | **Licensure.** The site currently asserts the JD only and describes the boutique role as *active litigation exposure*, never *practice*. Pouya flagged this as an interim position and asked to be reminded to change it | 2026-08-26 | If he is licensed and in good standing, it is the first credential appointing bodies and opposing counsel look for, and its absence from a detailed credentials page is conspicuous. If licensure is in progress, the copy should be rewritten the moment it completes. Either way this is a **temporary** framing that will otherwise ossify |
|
||||||
|
| R2 | **Matter counts stay off the site until they are independently credible.** Revisit once there is a number a sophisticated GC would find persuasive on its own | 2026-08-26 | §4 forbids them now. That rule has an expiry date nobody has set |
|
||||||
|
| R3 | **Indigenous engagement practice page.** Deliberately omitted at launch (§6). Revisit at month 12–18 | 2026-08-26 | The strategy brief rates it the most valuable single niche. Omission is a timing call, not a permanent one |
|
||||||
|
| R4 | **Insights cadence.** D9 commits to monthly. A blog that stops is worse than one that never started | 2026-08-26 | The section's whole value is compounding |
|
||||||
|
| R5 | **Fee review at 12 months.** Published rates are sticky; the right moment to move them is deliberate, not reactive | 2026-08-26 | D14 is priced for where the practice is going, not where it is |
|
||||||
|
| R6 | **Booking tool.** Parked by Pouya on 2026-08-26; `/contact/` ships with the intake form and a reserved slot for an embed | 2026-08-26 | He asked to be reminded. D10 committed to booking because it removes the back-and-forth that loses appointments — the form alone is a partial answer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
## 2026-08-26 (e) — AWS inventory captured; Q10 closed; blast-radius risk logged
|
||||||
|
|
||||||
|
**Who:** Pouya ran `scripts/aws-discover.sh`. Claude recording.
|
||||||
|
|
||||||
|
**Q10 closed.** All identifiers in §7. Bucket `adr-smlcompany-site`,
|
||||||
|
distribution `E1OK7G98KNKUTA`, region `ca-central-1`, intake API
|
||||||
|
`4tl0m5igkj`. Certificate ISSUED. **S3 versioning is already Enabled**, so the
|
||||||
|
rollback path in `docs/06-deployment.md` works as written — no change needed.
|
||||||
|
|
||||||
|
**Data residency confirmed.** Hosting, Lambda, and DynamoDB are all in
|
||||||
|
`ca-central-1`. `docs/05-backend-spec.md` treated Canadian residency as a
|
||||||
|
selling point conditional on the table's region; it is now a verified fact and
|
||||||
|
`/legal/privacy/` can state it plainly.
|
||||||
|
|
||||||
|
**New risk logged — deploy-credential blast radius.** The inventory shows this is
|
||||||
|
a shared account across at least five unrelated properties, including
|
||||||
|
`mlp-clientdb-prod-backups-327082975128`. A Gitea runner holding a static AWS key
|
||||||
|
for this site sits in the same account as a law firm's client-database backups.
|
||||||
|
Nothing about the plan changes — the scoped IAM policy was already correct — but
|
||||||
|
its narrowness is now load-bearing rather than good practice, and that is
|
||||||
|
recorded so nobody widens it later for convenience.
|
||||||
|
|
||||||
|
**Q18 and Q19 opened — SES.** `list-email-identities` returned
|
||||||
|
`VerifiedForSendingStatus: None` for all six identities, and sandbox status was
|
||||||
|
not checked. Either condition means the intake confirmation email silently fails.
|
||||||
|
This is the failure mode where the site appears to work and inquirers think they
|
||||||
|
have been ignored, so it is on the cutover checklist, not the nice-to-have list.
|
||||||
|
|
||||||
|
**Closed:** Q10. **Opened:** Q18, Q19.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-26 (d) — Memberships, analytics, prep time, firm naming; one blocker left
|
||||||
|
|
||||||
|
**Who:** Pouya. Claude recording.
|
||||||
|
|
||||||
|
**Q16 closed — preparation time is bundled and stated.** 2 h in the half day,
|
||||||
|
3 h in the full day, printed on `/fees/`. The carried-forward assumption is now a
|
||||||
|
decision. At $4,000 a day counsel will assume preparation happened; saying so
|
||||||
|
converts the assumption into a selling point and forecloses an argument about
|
||||||
|
what the fee covered.
|
||||||
|
|
||||||
|
**Q15 and Q17 closed.** Non-mediation hourly (ENE, settlement counsel,
|
||||||
|
dispute-system design, technical advisory) and mediation overtime both **$500/h**,
|
||||||
|
matching the arbitration rate.
|
||||||
|
|
||||||
|
**Q8 closed — memberships.** Old → new: five `[assumed]` bodies from the strategy
|
||||||
|
brief → **ADRIC, ADRIO, and OBA sections, verified.** OCNI is **not current** and
|
||||||
|
must not be published; the brief listed it as aspirational positioning for the
|
||||||
|
nuclear niche, which is a different thing from a membership.
|
||||||
|
|
||||||
|
- **LSO was deliberately not offered as an option and is not published.** Listing
|
||||||
|
the Law Society in a credentials block implies licensure, which D13 bars. Worth
|
||||||
|
recording explicitly so a later reader does not "helpfully" add it back.
|
||||||
|
- Which OBA sections remains `[assumed]` — the brief names Construction &
|
||||||
|
Infrastructure, ADR, and Civil Litigation. Low stakes; confirm in passing.
|
||||||
|
|
||||||
|
**D15 — analytics (new decision).** Old → new: unchosen → **privacy-first and
|
||||||
|
cookieless**, Plausible or Fathom. No GA4, no consent banner, no personal data
|
||||||
|
leaving the site, one honest line in the privacy policy. Consistent with a
|
||||||
|
practice whose privacy posture is part of its offer, on a site where visitors are
|
||||||
|
often describing live disputes.
|
||||||
|
|
||||||
|
**D16 — the boutique is never named (new decision).** Old → new: "may be named,
|
||||||
|
name pending" → **generic throughout**: *a Toronto litigation and ADR boutique*.
|
||||||
|
Pouya reversed his earlier answer. Trade-off, recorded for the record: naming a
|
||||||
|
recognisable firm is a meaningful credibility signal to appointing bodies, and
|
||||||
|
the generic phrasing gives that up in exchange for zero coordination cost and no
|
||||||
|
dependency on anyone else's sign-off. Do not infer the name from his email domain
|
||||||
|
or from anything else.
|
||||||
|
|
||||||
|
**Closed:** Q7, Q8, Q11, Q15, Q16, Q17. **Opened:** none.
|
||||||
|
|
||||||
|
**Q10 is now the only blocker.** Everything else needed to build is answered.
|
||||||
|
R6 (booking) stays parked by choice and does not block `/contact/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-26 (c) — Contact, headshot, LinkedIn; booking parked
|
||||||
|
|
||||||
|
**Who:** Pouya. Claude recording.
|
||||||
|
|
||||||
|
**Closed.** Q3 — contact is `info@smlcompany.ca`, no public phone, consultations
|
||||||
|
by scheduled call. Q6 — headshot supplied; committed as
|
||||||
|
`src/assets/pouya-lajevardi.jpg` (1600×1600 master, for Astro to derive AVIF and
|
||||||
|
WebP from at build) and `src/assets/og-portrait.jpg` (1200×630, cropped high so
|
||||||
|
the face is not centred under the fold of a link preview). Q12 — LinkedIn URL,
|
||||||
|
which now feeds `sameAs` in the `Person` JSON-LD.
|
||||||
|
|
||||||
|
**Q5 parked, not closed.** Old → new: "choose a booking tool" → **deferred at
|
||||||
|
Pouya's request**; `/contact/` ships with the intake form and a reserved slot for
|
||||||
|
an embed, so adding one later is a drop-in rather than a rebuild. He asked to be
|
||||||
|
reminded, so it is now **R6** in §12 rather than a question that quietly expires.
|
||||||
|
Worth restating when it comes back up: D10 chose booking because it removes the
|
||||||
|
scheduling back-and-forth that loses appointments, and the form alone only
|
||||||
|
half-solves that.
|
||||||
|
|
||||||
|
**Q7 partially answered.** The boutique may be named. The name itself was not
|
||||||
|
given. His email domain (`meshkinilaw.ca`) points to **Meshkini Law**, but an
|
||||||
|
inferred firm name is exactly the kind of thing §4 exists to stop — asked for
|
||||||
|
confirmation rather than published.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-26 (b) — Fees confirmed; Gitea; licensure wording; standing reminders
|
||||||
|
|
||||||
|
**Who:** Pouya, answering the second round. Claude recording.
|
||||||
|
|
||||||
|
**D14 closed — rate card confirmed, and it is not what was recommended.**
|
||||||
|
- Old → new: two-tier card (Tier A insurance/SABS at $1,800/day, Tier B
|
||||||
|
commercial at $3,200/day) → **one rate for all mediation matters: $2,000 half
|
||||||
|
day, $4,000 full day, $500 per party beyond two.** Arbitration $500/h, hearing
|
||||||
|
day $4,000, documents-only flat $6,500 simple / $9,500 complex. Tribunal-
|
||||||
|
secretary line removed. Cancellation terms adopted as recommended.
|
||||||
|
- Why: Pouya's decision.
|
||||||
|
- **Recorded dissent, for the 12-month review (R5).** A single premium rate puts
|
||||||
|
the insurance / SABS / LAT segment out of reach — the published market there
|
||||||
|
runs roughly $800–$1,200 a day, and $4,000 is three to five times it. That is
|
||||||
|
the segment the strategy brief (§IV.7) identifies as the highest realistic
|
||||||
|
near-term volume, flowing directly from the firm's existing accident-benefits
|
||||||
|
work. The trade is deliberate if it is deliberate: a premium specialist
|
||||||
|
position that forgoes volume. It is a mistake if the volume was being counted
|
||||||
|
on. Flagged once, implemented as directed.
|
||||||
|
- Three parameters were not specified and are carried forward as assumptions,
|
||||||
|
logged as Q15–Q17: non-mediation hourly ($500), bundled prep hours (2 h half
|
||||||
|
day / 3 h full day), mediation overtime ($500/h).
|
||||||
|
|
||||||
|
**D13 refined — approved wording for the boutique role.**
|
||||||
|
- Old → new: "describe the role factually" → the specific approved phrases are
|
||||||
|
**"active litigation exposure"** and **"involvement in litigation and ADR
|
||||||
|
matters"**. The word **"practice"** is barred in that context.
|
||||||
|
- Pouya asked to be **continuously reminded** that this is interim. That request
|
||||||
|
is now structural rather than a note — see the new §12.
|
||||||
|
|
||||||
|
**New §12, Standing Reminders.** Five items (R1–R5) that any agent must surface
|
||||||
|
at the start of a substantial session. Added because a parked decision otherwise
|
||||||
|
becomes permanent by never being raised again — and R1 is Pouya's explicit
|
||||||
|
instruction to keep raising it.
|
||||||
|
|
||||||
|
**D3 amended again — Gitea.**
|
||||||
|
- Old → new: "private git server, software unknown" → **self-hosted Gitea with
|
||||||
|
Gitea Actions**, clone at `/Users/pouya/Dev/Websites/adr-sml`.
|
||||||
|
- Consequence: Gitea Actions uses GitHub Actions syntax, so the workflow ports
|
||||||
|
almost unchanged into `.gitea/workflows/deploy.yml`. But **Gitea is not an AWS
|
||||||
|
OIDC provider**, so the OIDC role assumption is replaced by a scoped IAM user
|
||||||
|
whose key lives only in Gitea's secret store. The IAM policy stays exactly as
|
||||||
|
narrow. Quarterly rotation is now an operational obligation that OIDC would
|
||||||
|
have made unnecessary — noted in `docs/06-deployment.md`.
|
||||||
|
|
||||||
|
**Closed:** Q4, Q9, Q13, Q14. **Opened:** Q15, Q16, Q17.
|
||||||
|
**Still open:** Q3, Q5, Q6, Q7, Q8, Q10, Q11, Q12, Q15, Q16, Q17.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-26 (a) — Licensure decision; Q.Arb dated; git host changed; fees researched
|
||||||
|
|
||||||
|
**Who:** Pouya, answering the blocking questions from §9. Claude recording.
|
||||||
|
|
||||||
|
**Date correction.** The entry below is stamped 2026-08-25. The correct date of
|
||||||
|
that work was **2026-08-26** — the session clock was a day behind. Per rule 3 the
|
||||||
|
entry is left as written rather than edited; this note is the correction. Facts
|
||||||
|
touched today are re-stamped `[verified 2026-08-26]`.
|
||||||
|
|
||||||
|
**D13 — licensure left out of the site (new decision).**
|
||||||
|
- Old → new: "confirm LSO licence status before launch" → **the site asserts the
|
||||||
|
JD and makes no licensure claim at all.**
|
||||||
|
- Why: Pouya's direction.
|
||||||
|
- **Consequence, and it is not small.** The strategy brief describes an *"active
|
||||||
|
litigation practice in personal injury, construction, regulatory (POA), and
|
||||||
|
insurance (SABS) matters."* Copy written that way would imply entitlement to
|
||||||
|
practise law without stating it — which is the risk the decision is meant to
|
||||||
|
avoid, restated in different words. So the Forbidden table now bars implication
|
||||||
|
as well as assertion, and the About page describes the role factually:
|
||||||
|
*Director of Firm Operations at a Toronto litigation and ADR boutique*, plus
|
||||||
|
the matter types worked on. **Flagged back to Pouya**: if he is in fact
|
||||||
|
licensed and in good standing, that is a material credential for exactly the
|
||||||
|
audience this site targets, and omitting it costs more than it protects.
|
||||||
|
|
||||||
|
**Q.Arb dated.** Old → new: `[assumed]`, stage unknown → **commenced August
|
||||||
|
2026** `[verified 2026-08-26]`. Describe as newly commenced. Not as held, not as
|
||||||
|
nearing completion.
|
||||||
|
|
||||||
|
**D3 amended — the repository is not on GitHub.**
|
||||||
|
- Old → new: "git repo + GitHub Actions" → **repo `adr-sml` on a private git
|
||||||
|
server**; the GitHub Actions workflow in `.github/workflows/` is retained as a
|
||||||
|
working reference implementation, to be translated once the server software is
|
||||||
|
known (**Q13**).
|
||||||
|
- Why: Pouya is running his own git server.
|
||||||
|
- Knock-on: GitHub's OIDC provider is unavailable. GitLab CE can federate to AWS
|
||||||
|
by OIDC; Gitea/Forgejo and bare git cannot, so those need a scoped IAM user
|
||||||
|
with rotated keys, or deploys run from Pouya's machine. The choice is real and
|
||||||
|
waits on Q13.
|
||||||
|
|
||||||
|
**D14 — fee structure recommended (new decision, pending sign-off).** Market
|
||||||
|
research completed against the Ontario mandatory-mediation tariff, ADR Chambers
|
||||||
|
published ranges, and four published Ontario practitioner rate cards. A two-tier
|
||||||
|
structure is recommended so the price-sensitive SABS/LAT volume work does not
|
||||||
|
anchor the commercial rate. Figures are in the response to Pouya and are **not
|
||||||
|
written into any page until he signs off (Q14).**
|
||||||
|
|
||||||
|
**Still open:** Q3, Q5, Q6, Q7, Q8, Q10, Q11, Q12, Q13, Q14.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-25 — Project initiated; architecture decided; repo bundle authored
|
||||||
|
|
||||||
|
**Who:** Pouya Lajevardi with Claude (Cowork session), acting as architects. Claude Code to implement.
|
||||||
|
|
||||||
|
**What was discussed.** Pouya asked for a modernization of `adr.smlcompany.ca`
|
||||||
|
going beyond appearance — content and codebase included — with Claude Code doing
|
||||||
|
the implementation and deploying to AWS.
|
||||||
|
|
||||||
|
**What was found.** An audit of the live site and the local sources established
|
||||||
|
the facts in Current Truth §2. The headline finding: the site is a browser-
|
||||||
|
compiled single-file React bundle that serves crawlers three words of content.
|
||||||
|
For a personal-brand site whose function is discoverability, this is the
|
||||||
|
dominant problem, ahead of any question of looks or copy.
|
||||||
|
|
||||||
|
**What was decided.** Twelve decisions, D1–D12, recorded in §3. In summary: an
|
||||||
|
Astro static rebuild, full content re-architecture, ~20 pages, English only,
|
||||||
|
git + GitHub Actions onto the existing S3/CloudFront, palette and infinity mark
|
||||||
|
retained with a modernized execution, a published rate card, an Insights section
|
||||||
|
launching with drafted content, a rebuilt intake form plus booking, and a single
|
||||||
|
clean cutover.
|
||||||
|
|
||||||
|
- Old → new (framework): browser-compiled React single file → Astro static site
|
||||||
|
- Old → new (publishing): manual `rebuild-standalone.py` + console upload → git + GitHub Actions with OIDC
|
||||||
|
- Old → new (structure): one scrolling page → ~20 pre-rendered pages
|
||||||
|
- Old → new (content): partially-corrected placeholder template → written fresh against a claim register
|
||||||
|
- Old → new (record-keeping): none → this file, under the AGENTS constitution
|
||||||
|
|
||||||
|
**Why.** Discoverability is the practice's growth constraint and the current
|
||||||
|
architecture forecloses it. Every other decision follows from fixing that, with
|
||||||
|
the content decisions following from the strategy brief of 2026-05-26.
|
||||||
|
|
||||||
|
**Also decided, not yet discussed with Pouya:** no Indigenous engagement practice
|
||||||
|
page at launch (§6), on the reasoning recorded there. Flag this to him.
|
||||||
|
|
||||||
|
**Planned, not yet done.** Astro implementation; all page copy; 3–5 Insights
|
||||||
|
drafts; intake backend rework; CI/CD wiring; pre-cutover audit. Twelve blocking
|
||||||
|
questions are open in §9 — several pages cannot be written until they are
|
||||||
|
answered.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# CLAUDE.md — operating instructions for Claude Code
|
||||||
|
|
||||||
|
## Read this first
|
||||||
|
|
||||||
|
1. **`AGENTS.md` is the source of truth for this project.** Read it in full
|
||||||
|
before your first edit in any session. It carries the locked decisions, the
|
||||||
|
credential register, the open questions, and the full history.
|
||||||
|
2. **You are required to maintain `AGENTS.md`** under the constitution written at
|
||||||
|
the top of it. Update *Current Truth* in place; append to the *Change Log*,
|
||||||
|
newest first; never edit a past entry; never delete history. Record decisions
|
||||||
|
and plans, not only executed work. Stamp facts `[verified YYYY-MM-DD]` or
|
||||||
|
`[assumed]`.
|
||||||
|
3. Update it **at the end of every working session**, not only when something
|
||||||
|
ships. A session that produced a decision and no code still produces a Change
|
||||||
|
Log entry.
|
||||||
|
4. **Read `AGENTS.md` §12 Standing Reminders at the start of every substantial
|
||||||
|
session and surface anything live to Pouya.** These are decisions he parked
|
||||||
|
deliberately, not settled matters — R1 in particular is his explicit
|
||||||
|
instruction to keep raising the licensure wording. A parked decision that
|
||||||
|
stops being raised has quietly become permanent, which is the failure mode
|
||||||
|
§12 exists to prevent.
|
||||||
|
|
||||||
|
## The one rule that matters more than the code
|
||||||
|
|
||||||
|
Pouya is a licensed legal professional. **No factual claim about him, his
|
||||||
|
credentials, his experience, or his practice may appear on a public page unless
|
||||||
|
it is in the Verified table in `AGENTS.md` §4.**
|
||||||
|
|
||||||
|
If a page needs a fact you do not have:
|
||||||
|
|
||||||
|
- Do not infer it from context.
|
||||||
|
- Do not soften it into something defensible ("extensive experience", "years of").
|
||||||
|
- Do not carry it over from the old site — the old site contained a fictitious
|
||||||
|
founder, invented matter values, and a fabricated testimonial.
|
||||||
|
- **Leave `TODO(pouya): <the exact question>` in the source, and add the question
|
||||||
|
to `AGENTS.md` §9.** A build that fails on an unanswered question is a correct
|
||||||
|
build.
|
||||||
|
|
||||||
|
Read the Forbidden table in §4 before writing any statistic, number, or
|
||||||
|
superlative.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # local dev server
|
||||||
|
npm run build # static build to ./dist
|
||||||
|
npm run preview # serve ./dist locally
|
||||||
|
npm run check # astro check — type and template errors
|
||||||
|
npm run lint # eslint + prettier check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where things live
|
||||||
|
|
||||||
|
```
|
||||||
|
AGENTS.md living project record — read first, maintain always
|
||||||
|
docs/ the specs you build from
|
||||||
|
01-architecture.md sitemap, URL map, per-page content outline
|
||||||
|
02-design-system.md tokens, type scale, motion, contrast constraints
|
||||||
|
03-content-spec.md voice, copy rules, per-page copy deck
|
||||||
|
04-seo-spec.md metadata, structured data, sitemap, crawlability
|
||||||
|
05-backend-spec.md intake form, Lambda/DynamoDB/SES, booking, PIPEDA
|
||||||
|
06-deployment.md S3/CloudFront, GitHub Actions OIDC, cutover checklist
|
||||||
|
src/
|
||||||
|
styles/tokens.css design tokens — the single source of colour and scale
|
||||||
|
styles/global.css reset, base type, utilities
|
||||||
|
layouts/ page shells
|
||||||
|
components/ UI components
|
||||||
|
pages/ routes (file-based)
|
||||||
|
content/ content collections; Insights MDX lives here
|
||||||
|
data/site.ts site-wide constants, nav, contact details
|
||||||
|
public/ static assets served as-is
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
**Framework.** Astro, `output: 'static'`. Never introduce a server runtime
|
||||||
|
without a Change Log entry recording why.
|
||||||
|
|
||||||
|
**JavaScript.** Default to zero. Reach for an Astro island only when a feature
|
||||||
|
genuinely cannot be CSS or progressive HTML. If you add a `client:*` directive,
|
||||||
|
say why in the Change Log. A `<details>` element beats a JS accordion.
|
||||||
|
|
||||||
|
**Styling.** Plain CSS with custom properties. No Tailwind, no CSS-in-JS, no
|
||||||
|
utility framework. Every colour, space, and font size comes from a token in
|
||||||
|
`tokens.css` — no raw hex values and no magic numbers in component styles.
|
||||||
|
|
||||||
|
**Accessibility is a build requirement, not a polish pass.** Semantic landmarks,
|
||||||
|
one `<h1>` per page, heading levels never skipped, visible focus states, all
|
||||||
|
interactive elements reachable by keyboard, `prefers-reduced-motion` honoured on
|
||||||
|
every animation. Gold `#c9a876` never sits on cream — it fails contrast at
|
||||||
|
2.10:1. See `docs/02-design-system.md`.
|
||||||
|
|
||||||
|
**Images.** Astro `<Image>` with explicit width and height. AVIF/WebP with
|
||||||
|
fallback. Never base64-inline an image into HTML — the old site did this with
|
||||||
|
~1 MB of logo PNGs.
|
||||||
|
|
||||||
|
**Fonts.** Self-hosted, subset, `font-display: swap`, preloaded. No Google Fonts
|
||||||
|
request at runtime — it costs a round trip and adds a third-party call to a
|
||||||
|
page that collects legal inquiries.
|
||||||
|
|
||||||
|
**Every page ships with:** a unique `<title>` and meta description, a canonical
|
||||||
|
URL, Open Graph and Twitter card tags, and appropriate JSON-LD. See
|
||||||
|
`docs/04-seo-spec.md`. A page without these is not finished.
|
||||||
|
|
||||||
|
**Commits.** Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`).
|
||||||
|
One logical change per commit. Never commit secrets, `.env` files, or AWS
|
||||||
|
credentials — deploys use OIDC role assumption.
|
||||||
|
|
||||||
|
**Performance budget.** Lighthouse ≥ 95 on all four categories, on mobile, for
|
||||||
|
every page. Under 100 KB of JS on any route. LCP under 2.0 s on a simulated
|
||||||
|
Slow 4G connection. Treat a budget breach as a failing build.
|
||||||
|
|
||||||
|
## What "done" means for a page
|
||||||
|
|
||||||
|
- [ ] Copy written from `docs/03-content-spec.md`, every claim traceable to `AGENTS.md` §4
|
||||||
|
- [ ] No `TODO(pouya)` left unlogged in §9
|
||||||
|
- [ ] Unique title, meta description, canonical, OG/Twitter tags, JSON-LD
|
||||||
|
- [ ] Semantic HTML; keyboard navigable; reduced-motion honoured
|
||||||
|
- [ ] Lighthouse ≥ 95 mobile, all four categories
|
||||||
|
- [ ] Renders correctly with JavaScript disabled
|
||||||
|
- [ ] `AGENTS.md` Change Log entry appended
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# adr.smlcompany.ca
|
||||||
|
|
||||||
|
The dispute resolution practice of Pouya Lajevardi — Toronto.
|
||||||
|
|
||||||
|
A static site built with [Astro](https://astro.build), deployed to Amazon S3
|
||||||
|
behind CloudFront by GitHub Actions.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nvm use # Node 22
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:4321
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Command | Does |
|
||||||
|
|---|---|
|
||||||
|
| `npm run dev` | Development server with hot reload |
|
||||||
|
| `npm run build` | Static build to `./dist` |
|
||||||
|
| `npm run preview` | Serve the built site locally |
|
||||||
|
| `npm run check` | `astro check` — type and template errors |
|
||||||
|
| `npm run lint` | ESLint + Prettier |
|
||||||
|
|
||||||
|
## Before you contribute
|
||||||
|
|
||||||
|
Read **`AGENTS.md`** first, and maintain it as you work — it is the living
|
||||||
|
record of what this project is, what was decided, and why. Then read
|
||||||
|
**`CLAUDE.md`** for the working rules, and the specs in `docs/`.
|
||||||
|
|
||||||
|
The single hardest rule: **no factual claim about the practice ships unless it
|
||||||
|
appears in the verified register in `AGENTS.md` §4.** This is a licensed
|
||||||
|
professional's public marketing surface, and the site this replaces contained
|
||||||
|
fabricated credentials.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Pushes to `main` build and deploy automatically via
|
||||||
|
`.github/workflows/deploy.yml`, using OIDC role assumption — there are no
|
||||||
|
long-lived AWS credentials in this repository. See `docs/06-deployment.md`.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
|
||||||
|
// Canonical origin. Drives canonical URLs, OG tags, and the sitemap.
|
||||||
|
const SITE = process.env.PUBLIC_SITE_URL ?? 'https://adr.smlcompany.ca';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: SITE,
|
||||||
|
|
||||||
|
// Static output is the point of this rebuild — see docs/04-seo-spec.md.
|
||||||
|
// Do not switch to a server adapter without an AGENTS.md Change Log entry.
|
||||||
|
output: 'static',
|
||||||
|
|
||||||
|
// Directory-style URLs with a trailing slash, matched by the CloudFront
|
||||||
|
// trailing-slash function. See docs/06-deployment.md.
|
||||||
|
trailingSlash: 'always',
|
||||||
|
build: { format: 'directory', inlineStylesheets: 'auto' },
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
mdx(),
|
||||||
|
sitemap({
|
||||||
|
filter: (page) => !page.includes('/legal/'),
|
||||||
|
changefreq: 'monthly',
|
||||||
|
lastmod: new Date(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
image: {
|
||||||
|
// Explicit dimensions everywhere; never base64-inline an image.
|
||||||
|
service: { entrypoint: 'astro/assets/services/sharp' },
|
||||||
|
},
|
||||||
|
|
||||||
|
prefetch: { prefetchAll: true, defaultStrategy: 'viewport' },
|
||||||
|
});
|
||||||
@@ -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 12–18, 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**, 400–600 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, proponent–municipality
|
||||||
|
disputes, IESO market participation, data-centre connection allocation. Brief
|
||||||
|
§III.2 frames this as a 24–36 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 (1–7) · pre-session exchange (7–21) · the session (21–30) · 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 mediator–party
|
||||||
|
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`)
|
||||||
@@ -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, 11–12 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.
|
||||||
@@ -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
|
||||||
|
400–600 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 §III–IV — 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,200–1,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
|
||||||
|
mediator–party relationship
|
||||||
|
- [ ] Any comparative claim is factual and verifiable
|
||||||
@@ -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 50–60 chars, unique. Pattern: "<Page> · Pouya Lajevardi"
|
||||||
|
Home: "Pouya Lajevardi · Mediation & Arbitration · Toronto"
|
||||||
|
description 140–160 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
|
||||||
@@ -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 8–10.
|
||||||
|
**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 mediator–party 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 30–45 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 mediator–party
|
||||||
|
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
|
||||||
@@ -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
@@ -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)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "adr-smlcompany-ca",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "The dispute resolution practice of Pouya Lajevardi — Toronto",
|
||||||
|
"type": "module",
|
||||||
|
"engines": { "node": ">=22" },
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"check": "astro check",
|
||||||
|
"lint": "eslint . && prettier --check .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lighthouse": "lhci autorun"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^5.0.0",
|
||||||
|
"@astrojs/mdx": "^4.0.0",
|
||||||
|
"@astrojs/sitemap": "^3.2.0",
|
||||||
|
"sharp": "^0.33.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0.9.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"prettier": "^3.4.0",
|
||||||
|
"prettier-plugin-astro": "^0.14.0",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"eslint-plugin-astro": "^1.3.0",
|
||||||
|
"@lhci/cli": "^0.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# adr.smlcompany.ca
|
||||||
|
# AI crawlers are deliberately allowed. Being read by an assistant that counsel
|
||||||
|
# is using to shortlist a neutral is the point. See docs/04-seo-spec.md.
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Disallow: /legal/
|
||||||
|
|
||||||
|
Sitemap: https://adr.smlcompany.ca/sitemap-index.xml
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Collects the AWS resource identifiers this project needs (AGENTS.md Q10).
|
||||||
|
# Read-only: every call is a list/describe. Nothing is created or changed.
|
||||||
|
#
|
||||||
|
# chmod +x scripts/aws-discover.sh
|
||||||
|
# ./scripts/aws-discover.sh > aws-inventory.txt
|
||||||
|
#
|
||||||
|
# aws-inventory.txt contains NO secrets — only resource names and IDs — so it
|
||||||
|
# is safe to share. It is gitignored anyway.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
hr() { printf '\n== %s %s\n' "$1" "$(printf '=%.0s' $(seq 1 $((60 - ${#1}))))"; }
|
||||||
|
try() { "$@" 2>&1 || echo " (failed — check permissions or region)"; }
|
||||||
|
|
||||||
|
command -v aws >/dev/null || { echo "AWS CLI not installed. See AWS-Hosting-Guide.md Part 0.5"; exit 1; }
|
||||||
|
|
||||||
|
hr "Identity and default region"
|
||||||
|
try aws sts get-caller-identity --output table
|
||||||
|
echo "default region: $(aws configure get region || echo '(unset)')"
|
||||||
|
|
||||||
|
hr "S3 buckets -> which one holds the site?"
|
||||||
|
try aws s3 ls
|
||||||
|
|
||||||
|
hr "CloudFront distributions -> Id, Aliases, Origin"
|
||||||
|
try aws cloudfront list-distributions \
|
||||||
|
--query "DistributionList.Items[].{Id:Id,Status:Status,Domain:DomainName,Aliases:join(',',Aliases.Items||[\`none\`]),Origin:Origins.Items[0].DomainName}" \
|
||||||
|
--output table
|
||||||
|
|
||||||
|
hr "ACM certificates (us-east-1 — CloudFront certs live there)"
|
||||||
|
try aws acm list-certificates --region us-east-1 \
|
||||||
|
--query "CertificateSummaryList[].{Domain:DomainName,Status:Status,Arn:CertificateArn}" --output table
|
||||||
|
|
||||||
|
hr "API Gateway HTTP APIs -> the intake endpoint"
|
||||||
|
try aws apigatewayv2 get-apis \
|
||||||
|
--query "Items[].{Name:Name,ApiId:ApiId,Endpoint:ApiEndpoint,Protocol:ProtocolType}" --output table
|
||||||
|
|
||||||
|
hr "Lambda functions"
|
||||||
|
try aws lambda list-functions \
|
||||||
|
--query "Functions[].{Name:FunctionName,Runtime:Runtime,Modified:LastModified}" --output table
|
||||||
|
|
||||||
|
hr "DynamoDB tables"
|
||||||
|
try aws dynamodb list-tables --output table
|
||||||
|
|
||||||
|
hr "SES verified identities"
|
||||||
|
try aws sesv2 list-email-identities \
|
||||||
|
--query "EmailIdentities[].{Identity:IdentityName,Type:IdentityType,Verified:VerifiedForSendingStatus}" --output table
|
||||||
|
|
||||||
|
hr "S3 versioning on each bucket (rollback depends on this)"
|
||||||
|
for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text 2>/dev/null); do
|
||||||
|
printf ' %-45s %s\n' "$b" "$(aws s3api get-bucket-versioning --bucket "$b" --query 'Status' --output text 2>/dev/null || echo '?')"
|
||||||
|
done
|
||||||
|
|
||||||
|
hr "Done"
|
||||||
|
cat <<'NOTE'
|
||||||
|
Send back:
|
||||||
|
* the S3 bucket that holds the site
|
||||||
|
* the CloudFront distribution Id whose Aliases include adr.smlcompany.ca
|
||||||
|
* the default region, and the region of the DynamoDB table
|
||||||
|
* the API Gateway Endpoint for the intake API
|
||||||
|
NOTE
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
import { PRACTICE_AREAS } from '../data/site';
|
||||||
|
|
||||||
|
const practiceSlugs = PRACTICE_AREAS.map((a) => a.slug) as [string, ...string[]];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insights. Content territories are set by the strategy brief §VII and
|
||||||
|
* restated in docs/03-content-spec.md.
|
||||||
|
*
|
||||||
|
* Every piece must link to at least one practice-area page — that is what
|
||||||
|
* turns the blog into ranking power for the pages that convert.
|
||||||
|
*/
|
||||||
|
const insights = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: ({ image }) =>
|
||||||
|
z.object({
|
||||||
|
title: z.string().max(70),
|
||||||
|
description: z.string().min(70).max(160), // doubles as the meta description
|
||||||
|
publishDate: z.date(),
|
||||||
|
updatedDate: z.date().optional(),
|
||||||
|
topic: z.enum([
|
||||||
|
'process-explainer',
|
||||||
|
'regulatory-commentary',
|
||||||
|
'industry-commentary',
|
||||||
|
'reflection',
|
||||||
|
'technical-explainer',
|
||||||
|
'credentialing',
|
||||||
|
]),
|
||||||
|
practiceAreas: z.array(z.enum(practiceSlugs)).min(1),
|
||||||
|
image: image().optional(),
|
||||||
|
imageAlt: z.string().optional(),
|
||||||
|
/** Drafts are excluded from the build, the index, and the sitemap. */
|
||||||
|
draft: z.boolean().default(true),
|
||||||
|
/**
|
||||||
|
* Every article is reviewed by Pouya before publication (AGENTS.md D9).
|
||||||
|
* An article with draft:false and reviewedByPouya:false is a bug.
|
||||||
|
*/
|
||||||
|
reviewedByPouya: z.boolean().default(false),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { insights };
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* Site-wide constants.
|
||||||
|
*
|
||||||
|
* Anything marked TODO(pouya) is an unanswered question in AGENTS.md §9.
|
||||||
|
* Do not guess a value to make the build pass — an unanswered question is
|
||||||
|
* supposed to be visible. See CLAUDE.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SITE = {
|
||||||
|
name: 'Pouya Lajevardi',
|
||||||
|
tagline: 'Mediation · Arbitration · Toronto',
|
||||||
|
url: 'https://adr.smlcompany.ca',
|
||||||
|
locale: 'en_CA',
|
||||||
|
entity: 'SML Company Ltd. · Ontario, Canada',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verified credentials only — mirrors AGENTS.md §4.
|
||||||
|
* Adding a line here without adding it there is a bug.
|
||||||
|
*
|
||||||
|
* D13: the site asserts the JD and makes NO licensure claim. Do not add
|
||||||
|
* 'lawyer', 'called to the bar', 'licensed', or any post-nominal implying a
|
||||||
|
* licence — and do not imply it either. See AGENTS.md §4 Forbidden.
|
||||||
|
*/
|
||||||
|
export const CREDENTIALS = {
|
||||||
|
designations: ['Q.Med (ADRIC / ADRIO)'],
|
||||||
|
inProgress: ['Q.Arb — commenced August 2026'], // [verified 2026-08-26]
|
||||||
|
goal: 'C.Med-Arb (Chartered Mediator-Arbitrator)',
|
||||||
|
education: ['JD, Bond University'],
|
||||||
|
certifications: [
|
||||||
|
'Kompass Arbitration Certificate Program',
|
||||||
|
'Stitt Feld Handy — negotiation and ADR workshop series',
|
||||||
|
],
|
||||||
|
languages: ['English', 'Farsi'],
|
||||||
|
/** [verified 2026-08-26]. NOT OCNI (lapsed) and NOT the Law Society —
|
||||||
|
* listing the LSO implies licensure, which D13 bars. Do not add either. */
|
||||||
|
memberships: [
|
||||||
|
'ADR Institute of Canada (ADRIC)',
|
||||||
|
'ADR Institute of Ontario (ADRIO)',
|
||||||
|
'Ontario Bar Association — Construction & Infrastructure, ADR, and Civil Litigation sections',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** The three credential slots. Never matter counts — AGENTS.md §4. */
|
||||||
|
export const CREDENTIAL_ROW = [
|
||||||
|
{ value: 'Q.Med', label: 'ADRIC / ADRIO designation' },
|
||||||
|
{ value: 'JD + ML', label: 'Law and engineering' },
|
||||||
|
{ value: 'EN · FA', label: 'Bilingual practice' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Toronto boutique is NEVER named — AGENTS.md D16. Use this string.
|
||||||
|
* Do not infer a name from an email domain or anywhere else.
|
||||||
|
*/
|
||||||
|
export const BOUTIQUE = 'a Toronto litigation and ADR boutique' as const;
|
||||||
|
|
||||||
|
/** Analytics: privacy-first and cookieless (D15). No GA4, no consent banner. */
|
||||||
|
export const ANALYTICS = {
|
||||||
|
provider: 'plausible' as 'plausible' | 'fathom' | null,
|
||||||
|
domain: 'adr.smlcompany.ca',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CONTACT = {
|
||||||
|
email: 'info@smlcompany.ca', // [verified 2026-08-26]
|
||||||
|
/** No public phone by choice. Render 'By scheduled call' wherever a number
|
||||||
|
* would go — do not leave the field visually empty. */
|
||||||
|
phone: null as string | null, // [verified 2026-08-26]
|
||||||
|
phoneFallback: 'By scheduled call',
|
||||||
|
location: 'Toronto · Ontario · By appointment',
|
||||||
|
responseTime: 'Inquiries are answered within one business day.',
|
||||||
|
linkedin: 'https://www.linkedin.com/in/pouyalajevardi/', // [verified 2026-08-26]
|
||||||
|
/** Booking parked 2026-08-26 (AGENTS.md R6). Build /contact/ with the intake
|
||||||
|
* form and a reserved slot so an embed drops in later without a rebuild. */
|
||||||
|
bookingUrl: null as string | null,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Portrait assets. Astro derives AVIF/WebP variants from the master at build. */
|
||||||
|
export const PORTRAIT = {
|
||||||
|
master: 'src/assets/pouya-lajevardi.jpg', // 1600x1600
|
||||||
|
og: 'src/assets/og-portrait.jpg', // 1200x630, cropped high
|
||||||
|
alt: 'Pouya Lajevardi',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Shown on /contact/ and with the booking embed. Do not reword casually. */
|
||||||
|
export const NO_RETAINER_NOTICE =
|
||||||
|
'Submitting this form does not create a retainer, does not appoint a neutral, ' +
|
||||||
|
'and does not itself establish a mediator–party relationship.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate card — AGENTS.md D14, confirmed by Pouya 2026-08-26.
|
||||||
|
* One rate for all mediation matters. All figures PLUS HST.
|
||||||
|
* Full research and reasoning: docs/07-fees.md
|
||||||
|
*/
|
||||||
|
export const FEES = {
|
||||||
|
currency: 'CAD',
|
||||||
|
taxNote: 'All fees are plus HST.',
|
||||||
|
mediation: {
|
||||||
|
/** Prep is bundled AND stated on the page — [verified 2026-08-26].
|
||||||
|
* Do not hide it: at these rates, saying preparation is included is the
|
||||||
|
* point, not a detail. */
|
||||||
|
halfDay: { amount: 2000, hours: 3.5, prepIncluded: 2 },
|
||||||
|
fullDay: { amount: 4000, hours: 7, prepIncluded: 3 },
|
||||||
|
additionalParty: 500, // each party beyond two
|
||||||
|
overtimePerHour: 500, // [verified 2026-08-26]
|
||||||
|
},
|
||||||
|
arbitration: {
|
||||||
|
perHour: 500,
|
||||||
|
hearingDay: 4000,
|
||||||
|
documentsOnlySimple: 6500, // flat
|
||||||
|
documentsOnlyComplex: 9500, // flat
|
||||||
|
// No tribunal-secretary rate — removed by Pouya 2026-08-26.
|
||||||
|
},
|
||||||
|
// ENE, settlement counsel, dispute-system design, pre-dispute technical advisory
|
||||||
|
hourly: 500, // [verified 2026-08-26]
|
||||||
|
cancellation: [
|
||||||
|
{ window: 'More than 30 days before', fee: 'No fee. Disbursements only.' },
|
||||||
|
{ window: '15 to 30 days before', fee: '50% of the booked fee.' },
|
||||||
|
{ window: 'Fewer than 15 days before', fee: '100% of the booked fee.' },
|
||||||
|
],
|
||||||
|
cancellationNotes: [
|
||||||
|
'Rescheduling at the time of cancellation, with a new date fixed, is not charged.',
|
||||||
|
'The cancellation fee is waived if the reserved time is filled by another matter of equal or greater value.',
|
||||||
|
],
|
||||||
|
terms: [
|
||||||
|
'Fees are shared equally between the parties unless they agree otherwise in writing.',
|
||||||
|
'Accounts are payable on rendering. Interest accrues on overdue accounts at 5% per annum.',
|
||||||
|
'Video and in-person sessions are charged at the same rate.',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PRACTICE_AREAS = [
|
||||||
|
{ slug: 'construction', name: 'Construction & Infrastructure', chip: 'Construction' },
|
||||||
|
{ slug: 'technology', name: 'Technology, AI & Data', chip: 'Technology' },
|
||||||
|
{ slug: 'energy', name: 'Energy, Grid & Regulatory', chip: 'Energy' },
|
||||||
|
{ slug: 'insurance', name: 'Insurance, SABS & LAT', chip: 'Insurance' },
|
||||||
|
{ slug: 'shareholder', name: 'Shareholder & Family Business', chip: 'Shareholder' },
|
||||||
|
{ slug: 'cross-cultural', name: 'Cross-Border & Diaspora', chip: 'Cross-cultural' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Seven items is the ceiling before a nav stops being scannable. */
|
||||||
|
export const PRIMARY_NAV = [
|
||||||
|
{ href: '/about/', label: 'About' },
|
||||||
|
{ href: '/mediation/', label: 'Mediation' },
|
||||||
|
{ href: '/arbitration/', label: 'Arbitration' },
|
||||||
|
{ href: '/practice/', label: 'Practice', children: PRACTICE_AREAS },
|
||||||
|
{ href: '/fees/', label: 'Fees' },
|
||||||
|
{ href: '/insights/', label: 'Insights' },
|
||||||
|
{ href: '/contact/', label: 'Contact' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Linked contextually rather than from the primary nav. */
|
||||||
|
export const SECONDARY_NAV = [
|
||||||
|
{ href: '/process/', label: 'How I work' },
|
||||||
|
{ href: '/med-arb/', label: 'Med-Arb' },
|
||||||
|
{ href: '/for-parties/', label: 'For parties' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const LEGAL_NAV = [
|
||||||
|
{ href: '/legal/privacy/', label: 'Privacy' },
|
||||||
|
{ href: '/legal/terms/', label: 'Terms' },
|
||||||
|
] as const;
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
Global base. Spec: docs/02-design-system.md
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
@import './tokens.css';
|
||||||
|
|
||||||
|
/* --- Fonts: self-hosted, subset, swap. No runtime Google Fonts request. ----
|
||||||
|
TODO(claude-code): place subset woff2 files in /public/fonts/ and preload
|
||||||
|
Instrument Serif 400 and Geist 400 in BaseLayout — they are the only two
|
||||||
|
faces used above the fold. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Instrument Serif';
|
||||||
|
src: url('/fonts/instrument-serif-400.woff2') format('woff2');
|
||||||
|
font-weight: 400; font-style: normal; font-display: swap;
|
||||||
|
unicode-range: U+0000-00FF, U+0100-017F, U+2000-206F, U+2190-21BB;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Instrument Serif';
|
||||||
|
src: url('/fonts/instrument-serif-400-italic.woff2') format('woff2');
|
||||||
|
font-weight: 400; font-style: italic; font-display: swap;
|
||||||
|
unicode-range: U+0000-00FF, U+0100-017F, U+2000-206F;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Geist';
|
||||||
|
src: url('/fonts/geist-variable.woff2') format('woff2-variations');
|
||||||
|
font-weight: 300 600; font-style: normal; font-display: swap;
|
||||||
|
unicode-range: U+0000-00FF, U+0100-017F, U+2000-206F, U+2190-21BB;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
src: url('/fonts/geist-mono-variable.woff2') format('woff2-variations');
|
||||||
|
font-weight: 400 500; font-style: normal; font-display: swap;
|
||||||
|
unicode-range: U+0000-00FF, U+2000-206F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Reset ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
* { margin: 0; }
|
||||||
|
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scroll-padding-top: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--weight-normal);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
overflow-x: hidden;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
img, picture, video, canvas, svg { display: block; max-width: 100%; }
|
||||||
|
img { height: auto; }
|
||||||
|
input, button, textarea, select { font: inherit; color: inherit; }
|
||||||
|
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
|
||||||
|
ul[role='list'], ol[role='list'] { list-style: none; padding: 0; }
|
||||||
|
|
||||||
|
/* --- Type ----------------------------------------------------------------- */
|
||||||
|
|
||||||
|
h1, h2, h3, h4 { font-weight: var(--weight-normal); text-wrap: balance; }
|
||||||
|
|
||||||
|
.display {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-weight: var(--weight-normal);
|
||||||
|
line-height: var(--leading-display);
|
||||||
|
letter-spacing: var(--tracking-display);
|
||||||
|
}
|
||||||
|
/* The one flourish the design allows. One italic phrase per headline, max. */
|
||||||
|
.display .it { font-style: italic; }
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
letter-spacing: var(--tracking-eyebrow);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-meta);
|
||||||
|
}
|
||||||
|
/* An eyebrow is a label, never the page's heading element. */
|
||||||
|
.eyebrow .dot {
|
||||||
|
display: inline-block;
|
||||||
|
inline-size: 6px; block-size: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
margin-inline-end: var(--space-3);
|
||||||
|
vertical-align: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p { max-inline-size: var(--width-prose); }
|
||||||
|
|
||||||
|
a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 0.2em; }
|
||||||
|
a:hover { color: var(--accent-hover); }
|
||||||
|
|
||||||
|
/* --- Focus: visible, always. The previous build removed it globally. ------- */
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 3px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
:focus:not(:focus-visible) { outline: none; }
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: var(--space-2);
|
||||||
|
inset-inline-start: var(--space-2);
|
||||||
|
z-index: var(--z-skip);
|
||||||
|
padding: var(--space-3) var(--space-5);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transform: translateY(-200%);
|
||||||
|
transition: transform var(--dur-fast) var(--ease);
|
||||||
|
}
|
||||||
|
.skip-link:focus { transform: translateY(0); }
|
||||||
|
|
||||||
|
::selection { background: var(--accent); color: var(--text-inverse); }
|
||||||
|
|
||||||
|
/* --- Layout --------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.wrap { inline-size: 100%; max-inline-size: var(--width-content); margin-inline: auto; padding-inline: var(--gutter); }
|
||||||
|
.wrap-wide { max-inline-size: var(--width-wide); }
|
||||||
|
.prose { max-inline-size: var(--width-prose); }
|
||||||
|
.section { padding-block: var(--section-y); }
|
||||||
|
.section-alt { background: var(--bg-alt); }
|
||||||
|
.section-inverse { background: var(--bg-inverse); color: var(--text-inverse); }
|
||||||
|
.section-inverse .eyebrow,
|
||||||
|
.section-inverse .text-meta { color: var(--text-inverse-2); }
|
||||||
|
|
||||||
|
hr { border: none; border-block-start: 1px solid var(--border); }
|
||||||
|
.rule-gold { border: none; border-block-start: 1px solid var(--rule); }
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute; inline-size: 1px; block-size: 1px;
|
||||||
|
padding: 0; margin: -1px; overflow: hidden;
|
||||||
|
clip-path: inset(50%); white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Reveal ---------------------------------------------------------------
|
||||||
|
Progressive enhancement, not a dependency. Content is rendered and visible
|
||||||
|
in the HTML; `.reveal` only takes effect once JS adds `js-reveal` to <html>.
|
||||||
|
If the observer never runs, every page reads normally. The previous build
|
||||||
|
had this backwards and shipped a blank page to anything without JS. */
|
||||||
|
|
||||||
|
.js-reveal .reveal { opacity: 0; transform: translateY(20px); }
|
||||||
|
.js-reveal .reveal.is-in {
|
||||||
|
opacity: 1; transform: none;
|
||||||
|
transition: opacity var(--dur-reveal) var(--ease), transform var(--dur-reveal) var(--ease);
|
||||||
|
}
|
||||||
|
.js-reveal .reveal-stagger > * { opacity: 0; transform: translateY(16px); }
|
||||||
|
.js-reveal .reveal-stagger.is-in > * {
|
||||||
|
opacity: 1; transform: none;
|
||||||
|
transition: opacity var(--dur-reveal) var(--ease), transform var(--dur-reveal) var(--ease);
|
||||||
|
}
|
||||||
|
.js-reveal .reveal-stagger.is-in > *:nth-child(1) { transition-delay: 0ms; }
|
||||||
|
.js-reveal .reveal-stagger.is-in > *:nth-child(2) { transition-delay: 70ms; }
|
||||||
|
.js-reveal .reveal-stagger.is-in > *:nth-child(3) { transition-delay: 140ms; }
|
||||||
|
.js-reveal .reveal-stagger.is-in > *:nth-child(4) { transition-delay: 210ms; }
|
||||||
|
.js-reveal .reveal-stagger.is-in > *:nth-child(5) { transition-delay: 280ms; }
|
||||||
|
.js-reveal .reveal-stagger.is-in > *:nth-child(6) { transition-delay: 350ms; }
|
||||||
|
/* Stagger caps at six children by design. */
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html { scroll-behavior: auto; }
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
.js-reveal .reveal,
|
||||||
|
.js-reveal .reveal-stagger > * { opacity: 1 !important; transform: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Print: the About page will be printed by people evaluating an appointment */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body { background: #fff; color: #000; font-size: 11pt; }
|
||||||
|
.site-header, .site-footer, .skip-link, .no-print { display: none !important; }
|
||||||
|
a[href^='http']::after { content: ' (' attr(href) ')'; font-size: 9pt; }
|
||||||
|
.section { padding-block: var(--space-5); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
Design tokens — the single source of colour, type, space, and motion.
|
||||||
|
Spec: docs/02-design-system.md
|
||||||
|
|
||||||
|
Never write a raw hex value or a magic number in a component. If a value is
|
||||||
|
missing here, add it here.
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* --- Colour ------------------------------------------------------------
|
||||||
|
Palette carried over unchanged from the previous build (AGENTS.md D7).
|
||||||
|
Contrast ratios measured 2026-08-25 and recorded in docs/02. */
|
||||||
|
|
||||||
|
--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 16.81:1 */
|
||||||
|
--ink-soft: #3a322c; /* secondary text 11.75:1 */
|
||||||
|
--muted: #6e6359; /* metadata — ON CREAM ONLY (3.07:1 on ink) */
|
||||||
|
|
||||||
|
--maroon: #5a1a1c; /* primary action, dark panels 12.29:1 */
|
||||||
|
--maroon-d: #3d1112; /* hover on maroon */
|
||||||
|
--maroon-l: #7a2a2c; /* links on cream 8.95:1 */
|
||||||
|
|
||||||
|
/* GOLD IS NOT A TEXT COLOUR ON CREAM.
|
||||||
|
--gold on cream measures 2.10:1 and fails AA for body AND large text.
|
||||||
|
--gold-d measures 3.11:1 — large decorative text only, 24px+.
|
||||||
|
Both are fine on --ink (8.00) and --maroon (5.84). See docs/02. */
|
||||||
|
--gold: #c9a876; /* rules, dividers, icon strokes, on-dark text */
|
||||||
|
--gold-d: #a88858; /* large decorative display text on cream only */
|
||||||
|
--gold-l: #e2c89a; /* text on ink or maroon 11.09:1 */
|
||||||
|
|
||||||
|
--line: rgb(26 22 20 / 0.10);
|
||||||
|
--line-2: rgb(26 22 20 / 0.06);
|
||||||
|
--line-dark: rgb(250 247 242 / 0.14);
|
||||||
|
|
||||||
|
/* Semantic aliases — prefer these in components over raw palette names. */
|
||||||
|
--bg: var(--cream);
|
||||||
|
--bg-alt: var(--cream-2);
|
||||||
|
--bg-raised: var(--cream-3);
|
||||||
|
--bg-inverse: var(--ink);
|
||||||
|
--text: var(--ink);
|
||||||
|
--text-secondary: var(--ink-soft);
|
||||||
|
--text-meta: var(--muted);
|
||||||
|
--text-inverse: var(--cream);
|
||||||
|
--text-inverse-2: var(--gold-l);
|
||||||
|
--accent: var(--maroon);
|
||||||
|
--accent-hover: var(--maroon-d);
|
||||||
|
--link: var(--maroon-l);
|
||||||
|
--rule: var(--gold);
|
||||||
|
--border: var(--line);
|
||||||
|
--focus-ring: var(--maroon);
|
||||||
|
|
||||||
|
/* --- Type -------------------------------------------------------------- */
|
||||||
|
|
||||||
|
--font-serif: 'Instrument Serif', 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
|
||||||
|
--font-sans: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
|
||||||
|
/* Fluid scale, 360px → 1600px viewport. Ratio widens toward the display
|
||||||
|
end (1.25 → 1.333) so headlines scale harder than body copy. */
|
||||||
|
--text-xs: 0.75rem; /* 12 — legal */
|
||||||
|
--text-sm: 0.875rem; /* 14 — meta */
|
||||||
|
--text-base: 1rem; /* 16 — body */
|
||||||
|
--text-lg: clamp(1.0625rem, 0.99rem + 0.32vw, 1.1875rem); /* 17→19 */
|
||||||
|
--text-xl: clamp(1.25rem, 1.14rem + 0.48vw, 1.5rem); /* 20→24 */
|
||||||
|
--text-2xl: clamp(1.5rem, 1.31rem + 0.81vw, 1.875rem); /* 24→30 */
|
||||||
|
--text-3xl: clamp(1.875rem, 1.55rem + 1.37vw, 2.5rem); /* 30→40 */
|
||||||
|
--text-4xl: clamp(2.25rem, 1.66rem + 2.50vw, 3.5rem); /* 36→56 */
|
||||||
|
--text-5xl: clamp(2.75rem, 1.75rem + 4.25vw, 4.75rem); /* 44→76 */
|
||||||
|
--text-6xl: clamp(3.25rem, 1.75rem + 6.35vw, 6rem); /* 52→96 */
|
||||||
|
|
||||||
|
--leading-display: 0.98;
|
||||||
|
--leading-tight: 1.15;
|
||||||
|
--leading-snug: 1.35;
|
||||||
|
--leading-body: 1.6;
|
||||||
|
--leading-relaxed: 1.75;
|
||||||
|
|
||||||
|
--tracking-display: -0.02em;
|
||||||
|
--tracking-tight: -0.01em;
|
||||||
|
--tracking-normal: 0;
|
||||||
|
--tracking-wide: 0.06em;
|
||||||
|
--tracking-eyebrow: 0.18em;
|
||||||
|
|
||||||
|
--weight-light: 300;
|
||||||
|
--weight-normal: 400;
|
||||||
|
--weight-medium: 500;
|
||||||
|
--weight-semi: 600;
|
||||||
|
|
||||||
|
/* --- Space — 8px base -------------------------------------------------- */
|
||||||
|
|
||||||
|
--space-1: 0.25rem; /* 4 */
|
||||||
|
--space-2: 0.5rem; /* 8 */
|
||||||
|
--space-3: 0.75rem; /* 12 */
|
||||||
|
--space-4: 1rem; /* 16 */
|
||||||
|
--space-5: 1.5rem; /* 24 */
|
||||||
|
--space-6: 2rem; /* 32 */
|
||||||
|
--space-7: 3rem; /* 48 */
|
||||||
|
--space-8: 4rem; /* 64 */
|
||||||
|
--space-9: 6rem; /* 96 */
|
||||||
|
--space-10: 8rem; /* 128 */
|
||||||
|
--space-11: 10rem; /* 160 */
|
||||||
|
|
||||||
|
--section-y: clamp(var(--space-9), 6vw + 2rem, var(--space-11));
|
||||||
|
|
||||||
|
/* --- Layout ------------------------------------------------------------ */
|
||||||
|
|
||||||
|
--width-content: 80rem; /* 1280 */
|
||||||
|
--width-wide: 90rem; /* 1440 */
|
||||||
|
--width-prose: 68ch; /* reading measure — never exceed for body copy */
|
||||||
|
--gutter: var(--space-5);
|
||||||
|
--gutter-lg: var(--space-7);
|
||||||
|
|
||||||
|
/* --- Radius, elevation, motion ----------------------------------------- */
|
||||||
|
|
||||||
|
--radius-sm: 4px;
|
||||||
|
--radius-md: 8px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-full: 999px;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(26 22 20 / 0.04);
|
||||||
|
--shadow-md: 0 4px 16px rgb(26 22 20 / 0.06);
|
||||||
|
--shadow-lg: 0 12px 40px rgb(26 22 20 / 0.10);
|
||||||
|
|
||||||
|
--ease: cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||||
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
--dur-fast: 150ms;
|
||||||
|
--dur-hover: 250ms;
|
||||||
|
--dur-reveal: 600ms;
|
||||||
|
|
||||||
|
--z-base: 0;
|
||||||
|
--z-sticky: 100;
|
||||||
|
--z-header: 200;
|
||||||
|
--z-overlay: 300;
|
||||||
|
--z-skip: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 48rem) {
|
||||||
|
:root { --gutter: var(--gutter-lg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No dark mode by design (docs/02). A single committed light identity is the
|
||||||
|
right call for a legal practice and halves the ways contrast can break. */
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@components/*": ["src/components/*"],
|
||||||
|
"@layouts/*": ["src/layouts/*"],
|
||||||
|
"@styles/*": ["src/styles/*"],
|
||||||
|
"@data/*": ["src/data/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user