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