diff --git a/.claude/agents/adversarial-reviewer.md b/.claude/agents/adversarial-reviewer.md
new file mode 100644
index 0000000..2332a8f
--- /dev/null
+++ b/.claude/agents/adversarial-reviewer.md
@@ -0,0 +1,82 @@
+---
+name: adversarial-reviewer
+description: Adversarial code reviewer for this repository. Invoked after every implementation pass. Its brief is to find defects, not to approve work. Use for correctness, accessibility, performance, crawlability, and security review of a diff.
+tools: Read, Grep, Glob, Bash
+model: opus
+---
+
+You are reviewing a change to `adr.smlcompany.ca` — the public marketing site of
+a legal professional's dispute resolution practice.
+
+**Your job is to find what is wrong with it.** You are not here to confirm that
+the work is good. An approving review that misses a real defect is a failure; a
+review that raises a concern later judged minor is not.
+
+## Standing bias
+
+**When you are uncertain whether something is a defect, treat it as a defect and
+say so.** State your confidence. It is cheaper for the implementer to explain why
+you are wrong than for a defect to reach a page that counsel will read.
+
+Do not accept the implementer's reasoning as evidence. Read the code. Run it if
+you can. A claim in a commit message is not a verified behaviour.
+
+## What you are given
+
+A diff or a set of files, and the specs in `docs/`. You are deliberately **not**
+given the implementer's account of why the work is correct — form your own view
+from the artefact.
+
+## Lenses — work all of them
+
+**1. Correctness.** Does it do what `docs/01-architecture.md` and
+`docs/03-content-spec.md` actually specify, or something adjacent? Trace edge
+cases: empty collections, missing frontmatter, a draft article, a practice area
+with no articles, an absent image, a null contact field. `src/data/site.ts` has
+fields that are deliberately `null` — does the code render sensibly, or print
+"null"?
+
+**2. Accessibility.** `docs/02-design-system.md` §Accessibility floor is a build
+requirement, not a preference. Check: one `
` per page, no skipped heading
+levels, landmarks present, skip link first in tab order, visible `:focus-visible`
+states, `alt` on every image, 44px touch targets, keyboard reachability, form
+labels and `role="alert"` error announcement.
+
+**Check the one measured constraint every time:** gold `#c9a876` on cream
+`#faf7f2` is 2.10:1 and fails AA for body *and* large text. `--gold-d` is 3.11:1
+— large decorative text only. If gold is used as a text colour on a cream
+background anywhere, that is a defect, full stop.
+
+**3. Crawlability.** The entire project exists because the previous site served
+three words to crawlers. Verify: unique title and meta description, canonical,
+OG/Twitter tags, correct JSON-LD, and — critically — **that the page renders its
+full content with JavaScript disabled.** Any `client:*` directive is a finding
+unless the change explains why CSS or progressive HTML could not do the job.
+
+**4. Performance.** Budgets in `docs/04-seo-spec.md`: Lighthouse ≥ 95 mobile on
+all four categories, under 100 KB JS per route, LCP under 2.0 s. Check for
+base64-inlined images, images without explicit dimensions, runtime font requests,
+and third-party scripts. The old build inlined ~1 MB of logo PNGs — watch for
+regressions of that shape.
+
+**5. Security and data handling.** Any hardcoded endpoint, key, or credential is
+a finding. Check CSP compatibility, that form input is validated server-side and
+not only in the browser, and that nothing logs personal information.
+
+**6. Simplicity.** Is there a materially simpler correct version? Unnecessary
+abstraction is a defect in a site this size. So is a component with one use.
+
+## Output
+
+For each finding:
+
+- **Severity** — blocking / should-fix / consider
+- **Location** — file and line
+- **The defect**, in one sentence
+- **How it fails** — concrete inputs or conditions producing the wrong result.
+ If you cannot describe a concrete failure, say so and lower the severity
+ rather than dressing up a preference as a bug.
+- **The fix**, specifically
+
+If you genuinely find nothing at a given severity, say which lenses you applied
+and what you checked, so the gap is auditable. **"Looks good" is not a review.**
diff --git a/.claude/agents/claims-auditor.md b/.claude/agents/claims-auditor.md
new file mode 100644
index 0000000..d680d70
--- /dev/null
+++ b/.claude/agents/claims-auditor.md
@@ -0,0 +1,77 @@
+---
+name: claims-auditor
+description: Audits every factual assertion in site copy against the verified claim register in AGENTS.md section 4. Invoked before any page or article is considered complete. This is the professional-conduct guard, not a proofreading pass.
+tools: Read, Grep, Glob
+model: opus
+---
+
+You audit public copy for a **licensed legal professional's** marketing site.
+
+The site this replaces contained a fictitious founder, invented matter values
+("420+ matters", "$3.8B resolved", "93% settled"), fabricated office locations,
+and a testimonial attributed to a person who does not exist. Your existence is
+the control that stops that recurring.
+
+## Method
+
+1. Read `AGENTS.md` §4 in full — the Verified table, the Forbidden table, and
+ the substitution principle. Read `AGENTS.md` §3 D13 and D16.
+2. Extract **every factual assertion** from the copy under review. A factual
+ assertion is anything a reader could check: a credential, a designation, a
+ role, an institution, a language, a number, a date, a location, a capability,
+ a comparison.
+3. For each one, find its line in the Verified table.
+
+## The rule
+
+**A claim not in the Verified table does not ship.** There is no "close enough",
+no "defensible", no "everyone says this". Report it and require it be removed or
+replaced with something verified.
+
+## Specific things to catch
+
+**Licensure (D13).** The site asserts the JD and nothing further. Flag: "lawyer",
+"called to the bar", "licensed", "my law practice", "my litigation practice",
+"my clients", "acts for", "represents", "legal advice", or any post-nominal
+implying a licence. **Flag implication as hard as assertion** — "my litigation
+practice" claims licensure without the word.
+
+The approved phrasing for the boutique role is **"active litigation exposure"**
+or **"involvement in litigation and ADR matters"**. The word **"practice"** in
+that context is a defect.
+
+**The boutique is never named (D16).** Flag any firm name. Flag any detail
+specific enough to identify it.
+
+**Numbers.** Any matter count, settlement rate, dollar figure, hours mediated,
+years in ADR practice, or time-to-award statistic is forbidden outright. The
+approved stat set is `Q.Med` / `JD + ML` / `EN · FA`, plus `Q.Arb` in a fourth
+slot.
+
+**Q.Arb.** Commenced August 2026. Flag anything reading as held, imminent, or
+nearly complete. The Arbitration page must state plainly what is available now
+versus what follows designation.
+
+**Memberships.** ADRIC, ADRIO, OBA sections only. **OCNI is not current** — flag
+it. **The Law Society must not be listed** — listing it implies licensure, which
+D13 bars. Flag any addition of either, however well-intentioned.
+
+**Testimonials, endorsements, third-party quotes.** None exist. Any is a
+fabrication.
+
+**Superlatives and guarantees.** "Leading", "premier", "top-rated", "best",
+"proven", and any outcome language a reader could take as a promise.
+
+**Structured data counts as a claim.** JSON-LD `hasCredential`, `jobTitle`,
+`alumniOf`, and `knowsAbout` are audited exactly like visible copy. A
+machine-readable misrepresentation is still a misrepresentation.
+
+## Output
+
+A table: **claim quoted verbatim · location · verdict (VERIFIED / NOT IN
+REGISTER / FORBIDDEN) · the register line it matches, or what to do instead.**
+
+Then a single line: **PASS** — every assertion traced — or **FAIL**, with the
+count of untraceable claims.
+
+Never rewrite copy yourself. Report, and let the implementer fix it.
diff --git a/.claude/commands/build.md b/.claude/commands/build.md
new file mode 100644
index 0000000..4831689
--- /dev/null
+++ b/.claude/commands/build.md
@@ -0,0 +1,91 @@
+---
+description: The standing execution loop for this repo — plan, implement, adversarial review, resolve, verify, record. Use for every substantive change.
+argument-hint:
+---
+
+**ultrathink**
+
+Task: $ARGUMENTS
+
+Execute the six-phase loop below. Do not skip a phase because the task looks
+small — the loop is the quality mechanism, not ceremony. If a phase genuinely
+does not apply, say which and why before moving on.
+
+---
+
+## Phase 1 — Plan (think hard before writing anything)
+
+1. Read `AGENTS.md` in full if you have not this session. Read **§12 Standing
+ Reminders** and surface anything live to Pouya before you start.
+2. Read the specs in `docs/` that bear on this task.
+3. Restate the task in your own words, and name:
+ - which locked decisions (D1–D16) it touches
+ - which specs govern it
+ - which facts it needs from the §4 Verified register
+4. **Stop and ask if you find a conflict** — between the task and a locked
+ decision, between two specs, or between the task and a fact you do not have.
+ A blocked build is a correct build. Never resolve a conflict by guessing, and
+ never soften a claim to make it defensible.
+5. State your plan before implementing.
+
+## Phase 2 — Implement
+
+Follow `CLAUDE.md` conventions. Zero JavaScript by default. Tokens only, no raw
+hex, no magic numbers. Semantic HTML. Every page gets its metadata.
+
+Where you need a fact you do not have: `TODO(pouya): ` in the
+source **and** a new numbered question in `AGENTS.md` §9. Do not invent it.
+
+## Phase 3 — Adversarial review (this is not optional)
+
+Invoke **both** review agents on the change, in parallel:
+
+- `adversarial-reviewer` — correctness, accessibility, crawlability,
+ performance, security, simplicity
+- `claims-auditor` — every factual assertion traced to `AGENTS.md` §4
+
+**Give them the diff and the specs. Do not give them your reasoning for why the
+work is correct.** Your rationale anchors the reviewer and produces agreement
+instead of review. They form their own view from the artefact; that independence
+is the whole point of the phase.
+
+If the change touches no user-facing copy, `claims-auditor` may be skipped — say
+so explicitly.
+
+## Phase 4 — Resolve
+
+For every finding: **fix it, or decline it with a stated reason.** Silence is not
+a response. A declined finding is recorded in the Change Log with the reasoning,
+so a later reader can see the judgement was made rather than missed.
+
+If you fix anything material, **re-run Phase 3 on the fix.** A patch written
+under review pressure is exactly where the second defect lives.
+
+## Phase 5 — Verify — run it, do not assert it
+
+```bash
+npm run check
+npm run build
+```
+
+Then, as applicable to what changed:
+
+- Serve `dist/` and confirm the page **renders its full content with JavaScript
+ disabled** — the failure this whole project exists to fix
+- `curl` the built HTML and confirm real content, not a shell
+- Lighthouse mobile ≥ 95 on all four categories
+- Every internal link resolves
+- Metadata present: unique title, description, canonical, OG, JSON-LD
+
+**Never report a check as passing that you did not run.** "Should pass" is not a
+result. If you could not run something, say which and why.
+
+## Phase 6 — Record
+
+Append a `AGENTS.md` Change Log entry, newest first: what changed, old → new,
+why, and any decision or plan — including declined findings and anything
+deferred. Update Current Truth in place where the change made a section stale.
+Re-stamp facts you re-checked with today's date.
+
+Then report to Pouya: what shipped, what the review found, what you declined and
+why, and what remains open.
diff --git a/.claude/commands/review.md b/.claude/commands/review.md
new file mode 100644
index 0000000..386fb19
--- /dev/null
+++ b/.claude/commands/review.md
@@ -0,0 +1,23 @@
+---
+description: Run the adversarial review pass on demand — on the working tree, a commit range, or named files.
+argument-hint: [what to review — defaults to uncommitted changes]
+---
+
+**ultrathink**
+
+Scope: $ARGUMENTS
+
+If no scope is given, review the uncommitted working tree (`git status`,
+`git diff`).
+
+Invoke **both** agents in parallel on that scope:
+
+- `adversarial-reviewer`
+- `claims-auditor` — unless nothing user-facing changed, in which case say so
+
+Give them the diff and the relevant specs from `docs/`. **Do not brief them on
+why the code is correct** — that anchors the review and turns it into agreement.
+
+Report findings grouped by severity, most severe first. For each: the defect, the
+concrete failure it produces, and the fix. Do not fix anything yet — Pouya
+decides what gets addressed. Then ask what he wants done.
diff --git a/.claude/commands/wrap.md b/.claude/commands/wrap.md
new file mode 100644
index 0000000..10ed6bf
--- /dev/null
+++ b/.claude/commands/wrap.md
@@ -0,0 +1,35 @@
+---
+description: End-of-session ritual — update AGENTS.md under its constitution and hand back a clean state.
+---
+
+Close out this session properly. The working file is the deliverable that
+outlives the session; a session that ends without updating it has lost its
+reasoning.
+
+1. **Re-read `AGENTS.md`** — the constitution at the top, then Current Truth.
+
+2. **Update Current Truth in place** wherever this session made a section stale:
+ environment, decisions, the §4 register, open questions, risks. Re-stamp any
+ fact you re-verified with today's date. A stale date means it needs
+ re-checking, so do not leave a date you did not earn.
+
+3. **Append one Change Log entry**, newest first, covering:
+ - what was discussed, decided, changed, or planned — **decisions and plans
+ count even if no code was written**
+ - old → new for every change, and why
+ - findings you declined, with the reasoning
+ - anything deferred, and where it is now tracked
+ - questions closed and questions opened, by number
+
+ **Never edit a past entry.** If something earlier was wrong, correct it in
+ today's entry and leave the original as written.
+
+4. **Check §12 Standing Reminders.** Is anything now due? Should something new
+ be added — a decision Pouya parked, or one you made on his behalf that he has
+ not yet ratified?
+
+5. **Leave the tree clean.** `git status` should show only intended changes. No
+ stray build output, no `.env`, no credentials, no `aws-inventory.txt`.
+
+6. Report: what changed in `AGENTS.md`, what is open, and the single most useful
+ next action.
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..ad5d5c4
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,13 @@
+{
+ "$comment": "Project defaults for adr-sml. Deep reasoning and adversarial review are the working agreement here — see CLAUDE.md and .claude/commands/build.md. Verify with /config if a key is not honoured by your Claude Code version.",
+ "alwaysThinkingEnabled": true,
+ "showThinkingSummaries": true,
+ "effortLevel": "high",
+ "permissions": {
+ "deny": [
+ "Read(./.env)",
+ "Read(./.env.*)",
+ "Read(./aws-inventory.txt)"
+ ]
+ }
+}
diff --git a/AGENTS.md b/AGENTS.md
index 3e86f6a..360828b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -101,6 +101,8 @@ without an explicit instruction and a Change Log entry. `[verified 2026-08-25]`
| 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 |
+| D18 | Intake email | **Two emails per submission** — notification to `info@smlcompany.ca`, and a **confirmation receipt to the inquirer**. Pouya, 2026-08-26. The receipt is what requires SES production access (Q19) | Notification only; no email; no form at all |
+| D17 | Working method | **Pouya architects; Claude Code implements and then adversarially reviews its own work.** Six-phase loop encoded in `.claude/commands/build.md`; two independent review agents in `.claude/agents/`; extended thinking on by default. Standing agreement — not restated per prompt | Implement-and-report; single generic reviewer; review only on request |
| 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
@@ -221,7 +223,22 @@ the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — dec
| 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]` |
+| SES identities | Domain `smlcompany.ca` **verified for sending** `[verified 2026-08-26]`; addresses `info@`, `intake@`, `adr@` |
+| SES account | **In the sandbox** — `ProductionAccessEnabled: false` `[verified 2026-08-26]`. Production access required (Q19) |
+| Mail hosting | **Google Workspace** — MX `1 smtp.google.com`; `google._domainkey` present, so Google DKIM is configured `[verified 2026-08-26 — DNS query]` |
+| **SPF** | **ABSENT.** `smlcompany.ca` TXT holds only Anthropic and Google site-verification strings. No `v=spf1` record exists `[verified 2026-08-26 — DNS query]` |
+| **DMARC** | **ABSENT.** No `_dmarc.smlcompany.ca` record `[verified 2026-08-26 — DNS query]` |
+| Required DNS (Namecheap) | TXT `@` → `v=spf1 include:_spf.google.com include:amazonses.com ~all` · TXT `_dmarc` → `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` `[assumed]` — start at `p=none`, tighten after monitoring |
+| SES DKIM — **the set that actually resolves** | `f5puwearz5ek47aiuoskk57ya6ir66ju`, `jdue2r22cxrk27laxlgnjcpby2fq4vxu`, `kznn3cklvwh4gs342rtf5euqymlger53` — all three answer NOERROR at `._domainkey.smlcompany.ca` `[verified 2026-08-26 — DNS]` |
+| SES DKIM — **the set that is BROKEN** | `3zsnvsjg3ddi6hjyndjy6jgjoitvhydl`, `jejgp7na3wdpprsanamct4uxxgw2tyqw`, `xpiwyftpodmgpnzfof3ee7t7fzmlmhnh` — listed in the SES console, present in Namecheap, but **NXDOMAIN**. They were entered with the full name in Namecheap's Host field, so the zone doubled the domain: `._domainkey.smlcompany.ca.smlcompany.ca` resolves, the correct name does not `[verified 2026-08-26 — DNS]` |
+| SES identity — **`ca-central-1` (the one that matters)** | `Verified: true`, `DkimStatus: SUCCESS`, tokens `kznn…` / `jdue…` / `f5pu…` — the set that resolves. **Healthy. Change nothing** `[verified 2026-08-26]` |
+| SES identity — `us-east-1` (unused) | `Verified: false`, `DkimStatus: PENDING`, tokens `3zsn…` / `jejgp…` / `xpiwy…` — the doubled-domain set. A stray identity in a region this project does not use `[verified 2026-08-26]` |
+| ⚠️ **SES sandbox is per-region** | Production access must be requested **in `ca-central-1`**. The AWS console defaults to `us-east-1`; requesting there grants access to the wrong region and the intake email still fails `[verified 2026-08-26]` |
+| SES custom MAIL FROM | **Not configured** — SES reports "No MAIL FROM records found". This is why SES's SPF is unaligned and DMARC rests on DKIM. Optional future improvement, not required `[verified 2026-08-26]` |
+| **DMARC alignment** | SES satisfies DMARC via **DKIM**, not SPF — without a custom MAIL FROM domain its envelope sender is at `amazonses.com` and is unaligned. The SPF record's real job is authenticating **Google Workspace** mail `[verified 2026-08-26]` |
+| **Do not delete** | Two ACM validation CNAMEs → `jkddzztszm.acm-validations.aws`. They renew the `adr.smlcompany.ca` certificate; deleting them breaks HTTPS silently at the next renewal `[verified 2026-08-26]` |
+| Root domain | `@` carries a Namecheap **URL Redirect Record** to `sml-coming-soon.ca`. TXT records coexist with it — adding SPF does not disturb it `[verified 2026-08-26]` |
+| DNS confirmed | `adr.smlcompany.ca` CNAME → `d26v23dhgsp2ta.cloudfront.net` `[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]` |
@@ -262,8 +279,10 @@ Nothing below can be invented. Each needs an answer from Pouya.
| ~~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 |
+| ~~Q18~~ | **ANSWERED 2026-08-26.** `smlcompany.ca` is verified for sending — `true`. The `None` in the earlier listing was a query artefact, not a fault | — |
+| **Q19** | **CONFIRMED AS A BLOCKER 2026-08-26. Request it in `ca-central-1`, not the console's default `us-east-1` — the sandbox is per-region.** `ProductionAccessEnabled: false` — the SES account is in the **sandbox**, so mail reaches only pre-verified addresses. Confirmed with Pouya (D18) that the inquirer receipt is wanted, so production access **is** required. ~24 h AWS turnaround. **The only item on this project with external lead time** | The confirmation half of `/contact/` |
+| ~~Q20~~ | **ANSWERED 2026-08-26.** Both records added by Pouya and independently verified: SPF `v=spf1 include:_spf.google.com include:amazonses.com ~all` on `@`, DMARC `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` on `_dmarc`. Both existing `@` TXT records, Google DKIM, MX, and the `adr` CNAME all confirmed intact | — |
+| ~~Q21~~ | **ANSWERED 2026-08-26.** `ca-central-1` is healthy (`SUCCESS`) on the resolving token set. The broken records belong to a stray `us-east-1` identity this project does not use. **Nothing to fix.** The three dead CNAMEs are inert; leaving them is the low-risk choice | — |
| ~~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/` | — |
@@ -278,7 +297,8 @@ Nothing below can be invented. Each needs an answer from Pouya.
| 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 |
+| **SES is in the sandbox** — confirmed, not hypothetical | **High** | Q19. Until production access is granted, every confirmation email to an inquirer fails silently. The site appears to work; the inquirer concludes they were ignored. Request access early — it is the only dependency with an external turnaround |
+| **No SPF and no DMARC on `smlcompany.ca`** | **High** | Q20. Affects SES *and* the existing Google Workspace mail — Pouya's business email is unauthenticated today, which is a live deliverability problem independent of this project. Gmail and Yahoo both penalise it. Add both records before requesting SES production access; AWS also weighs domain authentication when reviewing |
## 11. Glossary
@@ -322,6 +342,231 @@ never being raised again.
# Change Log
+## 2026-08-26 (k) — Q21 resolved: nothing broken; SES sandbox is per-region
+
+**Who:** Pouya ran the per-region check.
+
+**Q21 closed — the good case.**
+
+| Region | Verified | DKIM | Tokens |
+|---|---|---|---|
+| `ca-central-1` | `true` | **SUCCESS** | `kznn…` `jdue…` `f5pu…` — resolve correctly |
+| `us-east-1` | `false` | PENDING | `3zsn…` `jejgp…` `xpiwy…` — doubled domain |
+
+The region that matters is healthy. The three broken CNAMEs belong to a stray
+`us-east-1` identity that nothing in this project uses; it is PENDING precisely
+*because* those records were entered with the domain doubled. **No fix required.**
+
+Recommendation recorded: **leave the three dead CNAMEs in DNS.** They are inert,
+and the only way to cause harm is deleting the wrong row from a list of six
+visually similar records. Optionally delete the `us-east-1` identity itself so it
+stops reporting PENDING — that is a control-plane action with no DNS risk.
+
+**New finding, and it would have cost a day: the SES sandbox is per-region.**
+`ProductionAccessEnabled: false` was read from `ca-central-1`, which is where the
+intake Lambda, DynamoDB, and API Gateway all live. The AWS console defaults to
+`us-east-1`, and the panel Pouya screenshotted was `us-east-1` — so the natural
+path would have been to request production access in the wrong region, wait a
+day, and find the confirmation email still failing. Flagged in §7 and on Q19.
+
+**Also worth noting for the record:** entry (j)'s correction was itself only
+half the story. DNS proved which records resolved; it could not prove which
+*mattered*. That needed the control plane. Neither source is sufficient alone —
+the data plane says what exists, the control plane says what is expected, and a
+discrepancy between them is the finding.
+
+**Closed:** Q21. **Still blocking:** Q19 only.
+
+---
+
+## 2026-08-26 (j) — SPF/DMARC live; and a correction: three DKIM records are broken
+
+**Who:** Pouya added the two TXT records. Claude verified independently.
+
+**Q20 closed.** SPF and DMARC are both live and correct, confirmed by direct DNS
+query rather than taken on report. Also confirmed unchanged: both existing `@`
+TXT records, `google._domainkey`, `MX 1 smtp.google.com`, and
+`adr.smlcompany.ca` → CloudFront. Nothing was disturbed.
+
+**Correction to entries (h) and (i) — I had the two DKIM sets backwards.**
+
+- Old → new: "`3zsn…`/`jejgp…`/`xpiwy…` are the live set; `f5pu…`/`jdue…`/`kznn…`
+ are inert orphans" → **the opposite.** The `f5pu` set resolves NOERROR at the
+ correct names. The `3zsn` set returns **NXDOMAIN**.
+- **Root cause found.** `3zsnvsjg…._domainkey.smlcompany.ca.smlcompany.ca`
+ resolves; the correct name does not. Those three were entered into Namecheap's
+ Host field with the **full name including the domain**, so Namecheap appended
+ the domain a second time. This is precisely the gotcha flagged in entry (h) —
+ which was written before checking whether the existing records had already
+ fallen into it.
+- **Why I got it wrong:** I inferred "live" from the SES console listing them and
+ did not verify against DNS. The console shows what SES *expects*, not what
+ exists. Lesson worth keeping: a control-plane listing is not evidence about the
+ data plane.
+- **No harm done.** The advice in (i) was "leave both sets alone", which happened
+ to be the safe action either way.
+
+**Q21 opened.** SES is regional, and `ca-central-1` reported
+`VerifiedForSendingStatus: true` — only consistent with the `f5pu` set. So the
+console panel screenshotted was probably a different region's identity. Until
+that is confirmed per region, it is unknown whether the broken records matter.
+Nothing should be deleted before that check.
+
+**Closed:** Q20. **Opened:** Q21. **Still blocking:** Q19 (SES production access).
+
+---
+
+## 2026-08-26 (i) — SES DNS panel resolves the DKIM ambiguity; MAIL FROM absent
+
+**Who:** Pouya sent the SES "Domain DNS records" panel.
+
+**The six DKIM CNAMEs are now fully explained.** SES lists exactly three for
+`smlcompany.ca`: `3zsnvsjg…`, `jejgp7na3…`, `xpiwyftpo…`. The other three in
+Namecheap — `f5puwearz…`, `jdue2r22c…`, `kznn3cklv…` — are **not** referenced by
+SES and are residue from an earlier verification of the same domain. Note the
+TTLs corroborate this: the live set reads "1 min", the orphans "Automatic",
+consistent with two separate additions.
+
+**Recommendation recorded: leave the orphans in place.** They are inert. The only
+way this becomes a problem is deleting the wrong three, which would break SES
+DKIM and therefore DMARC. There is no upside to tidying a DNS zone.
+
+**Custom MAIL FROM is not configured** — "No MAIL FROM records found". This
+independently confirms the correction in entry (h): SES's envelope sender is at
+`amazonses.com`, its SPF pass is unaligned with `smlcompany.ca`, and DMARC is
+satisfied through DKIM alone. Configuring a custom MAIL FROM would add SPF
+alignment as belt-and-braces; it is optional and not on the critical path.
+
+**AWS's suggested DMARC value is `v=DMARC1; p=none;`** — valid but minimal, with
+no `rua`, so it produces no reports. Using `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1`
+instead so the monitoring period actually yields data before tightening to
+`quarantine`.
+
+**Net action for Q20 is two TXT records and nothing else.**
+
+---
+
+## 2026-08-26 (h) — DNS inspected; earlier SPF reasoning corrected
+
+**Who:** Pouya sent the Namecheap DNS panel, asking exactly what to change
+without breaking anything.
+
+**Correction to entry (g) — the SPF reasoning was wrong.** Old → new: "SES needs
+SPF or mail lands in spam" → **SES authenticates via DKIM, not SPF.** Without a
+custom MAIL FROM domain, SES's envelope sender sits at `amazonses.com`, so its
+SPF pass is not aligned with `smlcompany.ca` and contributes nothing to DMARC.
+The six `*.dkim.amazonses.com` CNAMEs already in Namecheap are what make SES
+DMARC-compliant, and they are working. The SPF record's real value is
+authenticating **Google Workspace** mail, which has none. `include:amazonses.com`
+stays because it is harmless and becomes live if a custom MAIL FROM is added.
+Entry (g) is left as written per rule 3; this is the correction.
+
+**Also found in the DNS panel:**
+- Six SES DKIM CNAMEs where three are needed — the domain was verified twice.
+ Harmless, but do not prune without checking SES first.
+- Two ACM validation CNAMEs → `jkddzztszm.acm-validations.aws`. **Flagged as
+ never-delete**: they auto-renew the `adr.smlcompany.ca` certificate, and
+ removing them breaks HTTPS silently at the next renewal rather than
+ immediately. This is the only genuinely destructive mistake available in that
+ panel.
+- `@` carries a Namecheap URL Redirect to `sml-coming-soon.ca`. TXT records
+ coexist with it, so adding SPF does not disturb the redirect.
+
+**DMARC `rua` changed** from `pouya@` to `info@smlcompany.ca` — the mailbox
+confirmed to exist.
+
+**Still blocking:** Q19 (SES production access), Q20 (the two TXT records).
+
+---
+
+## 2026-08-26 (g) — SES purpose clarified; D18; SPF and DMARC found missing
+
+**Who:** Pouya asked why SES is needed given he already has an `info@` mailbox.
+Fair question — the answer exposed two things.
+
+**The distinction, recorded because it will come up again.** A mailbox receives
+mail and a person logs into it. SES lets *code* send mail — the intake Lambda has
+no mailbox and no login. They are complementary, not alternatives: SES sends
+**to** `info@smlcompany.ca`. Verifying the address in SES did not create a
+mailbox; it authorised sending.
+
+**D18 — two emails per submission (new decision).** The sandbox blocks only one
+of the two: the notification to Pouya works (his address is verified), the
+**receipt to the inquirer** does not. That receipt was Claude's addition in
+`docs/05-backend-spec.md`, not part of the original AWS-guide build, so it was
+put back to Pouya as a real choice rather than treated as a defect. **He confirmed
+he wants it** — so Q19 is genuinely blocking rather than optional.
+
+**Q20 opened — the domain has no email authentication at all.** A DNS query of
+`smlcompany.ca` on 2026-08-26 returned:
+
+- TXT: only `anthropic-domain-verification` and `google-site-verification`.
+ **No `v=spf1` record of any kind.**
+- `_dmarc.smlcompany.ca`: **no record.**
+- MX `1 smtp.google.com` and `google._domainkey` present — Google Workspace with
+ DKIM configured.
+- `adr.smlcompany.ca` CNAME → `d26v23dhgsp2ta.cloudfront.net`, as expected.
+
+Old → new (planned): no SPF, no DMARC → one SPF record covering **both** Google
+and Amazon SES, plus a DMARC record starting at `p=none`.
+
+**This is wider than this project.** Pouya's existing business email from
+`smlcompany.ca` is sending unauthenticated today. Google Workspace's DKIM carries
+it, but Gmail and Yahoo both penalise missing SPF and DMARC, so mail he sends as a
+human is already more likely to be filtered than it should be. Raised to **High**
+in §10 and flagged to him directly. Both records should go in before the SES
+production-access request — AWS weighs domain authentication when reviewing, so
+it improves the odds of first-time approval as well.
+
+**Opened:** Q20. **Still blocking:** Q19, Q20.
+
+---
+
+## 2026-08-26 (f) — SES sandbox confirmed; execution protocol built into the repo
+
+**Who:** Pouya. Claude recording.
+
+**Q18 closed, Q19 confirmed as a live blocker.** `smlcompany.ca` **is** verified
+for sending (`true`) — the earlier `None` was a query artefact, not a fault. But
+`ProductionAccessEnabled: false`: **the account is in the SES sandbox.** Mail
+reaches only pre-verified addresses, so an inquirer's confirmation email fails
+silently. Raised from Medium to **High** in §10: this is the failure mode where
+the site looks like it works and the inquirer concludes they were ignored.
+Production access must be requested; ~24 h turnaround, and it is the only
+dependency on this project with external lead time.
+
+**D17 — working method, made structural (new decision).**
+- Old → new: an implicit understanding that Claude Code implements what Pouya
+ decides → **an explicit six-phase loop encoded in the repository**, so it
+ applies without being restated in each prompt.
+- Why: Pouya's direction — he architects, Claude Code programs and then
+ adversarially reviews, with deep reasoning on, permanently.
+- Added:
+ - `.claude/agents/adversarial-reviewer.md` — code, accessibility,
+ crawlability, performance, security, simplicity. Instructed to **treat
+ uncertainty as a defect** and told that an approving review which misses a
+ real defect is a failure.
+ - `.claude/agents/claims-auditor.md` — every factual assertion traced to §4.
+ Separate agent on purpose: a generic reviewer consistently under-weights
+ professional-conduct checks, which is this project's highest-stakes failure
+ mode. It also audits JSON-LD, since a machine-readable misrepresentation is
+ still a misrepresentation.
+ - `.claude/commands/build.md` — the loop. `/review` — phase 3 alone.
+ `/wrap` — the session-end `AGENTS.md` ritual.
+ - `.claude/settings.json` — thinking on, high effort, and a read-deny on
+ `.env*` and `aws-inventory.txt`.
+ - `CLAUDE.md` — the protocol and the reasoning behind it.
+ - `docs/08-execution-protocol.md` — the prompt template Pouya writes from.
+- **The design decision worth preserving:** the reviewers are given the diff and
+ the specs, and deliberately **not** the implementer's rationale. Explaining why
+ the work is correct anchors the reviewer and converts review into agreement.
+ Independence is the mechanism, not a courtesy. If a future change to this
+ protocol removes that, it removes the value.
+
+**Closed:** Q18. **Still blocking:** Q19 (SES production access).
+
+---
+
## 2026-08-26 (e) — AWS inventory captured; Q10 closed; blast-radius risk logged
**Who:** Pouya ran `scripts/aws-discover.sh`. Claude recording.
diff --git a/CLAUDE.md b/CLAUDE.md
index 92594ee..edf7fd2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -39,6 +39,56 @@ If a page needs a fact you do not have:
Read the Forbidden table in §4 before writing any statistic, number, or
superlative.
+## How work is executed here
+
+Pouya is the architect. He makes the decisions and hands you the task. **You
+implement, then you adversarially review your own work before calling it done.**
+This is the standing agreement — it applies to every substantial change without
+being restated in the prompt.
+
+**Run `/build ` for any substantive change.** It encodes the loop:
+
+1. **Plan** — read `AGENTS.md` (including §12 Standing Reminders, and surface
+ anything live), read the governing specs, name the decisions the task touches,
+ and **stop and ask on any conflict**. A blocked build is a correct build.
+2. **Implement** — following the conventions below.
+3. **Adversarial review** — invoke `adversarial-reviewer` and `claims-auditor` in
+ parallel on the diff.
+4. **Resolve** — fix each finding or decline it with a stated reason. Re-review
+ material fixes.
+5. **Verify** — run the checks. Never report a check as passing that you did not
+ run.
+6. **Record** — append the `AGENTS.md` Change Log entry.
+
+`/review` runs phase 3 alone. `/wrap` runs phase 6 at session end.
+
+**Think deeply before acting.** Extended thinking is on by default for this
+project (`.claude/settings.json`), and `/build` and `/review` request it
+explicitly. The planning and review phases are where it earns its cost — a defect
+reasoned out before implementation is far cheaper than one found after.
+
+### Why the review is adversarial, and what would break it
+
+Two rules make the difference between a review and a rubber stamp:
+
+**Do not brief the reviewers on why your work is correct.** Give them the diff
+and the specs, nothing else. Your rationale anchors them, and an anchored
+reviewer produces agreement rather than review. They must form an independent
+view from the artefact — that independence *is* the mechanism.
+
+**The reviewers are instructed to treat uncertainty as a defect.** They will
+sometimes be wrong, and that is the intended trade. Explaining why a finding is
+mistaken costs minutes; a missed defect on a licensed professional's public
+marketing page costs considerably more. Do not read a finding as an accusation,
+and do not argue a reviewer down — either fix it, or record the reason you
+declined it so a later reader can see the judgement was made rather than missed.
+
+**Two reviewers, because they catch different things.** `adversarial-reviewer`
+reads the code. `claims-auditor` reads the copy against the §4 register and knows
+nothing about whether the code is elegant. A generic reviewer consistently
+under-weights the professional-conduct check, which is the highest-stakes failure
+mode on this project — so it gets its own pass.
+
## Commands
```bash
diff --git a/README.md b/README.md
index 2289fa2..25ac38f 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,16 @@ 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.
+## How work is done here
+
+Pouya decides; Claude Code implements and then adversarially reviews its own
+work. Run **`/build `** for any substantive change — it plans, implements,
+runs two independent review agents on the diff, resolves the findings, verifies
+the build, and records the session in `AGENTS.md`. `/review` runs the review pass
+alone; `/wrap` closes a session.
+
+Full protocol and prompt guidance: `docs/08-execution-protocol.md`.
+
## Deployment
Pushes to `main` build and deploy automatically via
diff --git a/docs/05-backend-spec.md b/docs/05-backend-spec.md
index a141b4d..abfe8b2 100644
--- a/docs/05-backend-spec.md
+++ b/docs/05-backend-spec.md
@@ -106,8 +106,53 @@ SES on submission:
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)**.
+**Email authentication — verified 2026-08-26, and it is not in place.**
+
+A DNS query of `smlcompany.ca` found **no SPF record and no DMARC record**. Mail
+is on Google Workspace (MX `1 smtp.google.com`) with Google DKIM configured, and
+the SES domain identity reports verified for sending — but neither SPF nor DMARC
+exists.
+
+**What is already in place** (Namecheap DNS and the SES console, both inspected
+2026-08-26):
+
+| Record | Status |
+|---|---|
+| SES DKIM — `3zsnvsjg…`, `jejgp7na3…`, `xpiwyftpo…` `._domainkey` | **Live.** Matches SES exactly. Never delete |
+| SES DKIM — `f5puwearz…`, `jdue2r22c…`, `kznn3cklv…` `._domainkey` | Orphans from an earlier verification. Inert. **Leave them** — deleting the wrong three breaks DKIM |
+| `google._domainkey` TXT | Google Workspace DKIM. Never delete |
+| Two CNAMEs → `jkddzztszm.acm-validations.aws` | ACM certificate validation. **Never delete** — breaks HTTPS at the next renewal |
+| `adr` CNAME → `d26v23dhgsp2ta.cloudfront.net` | The site |
+| Custom MAIL FROM | **Not configured.** Optional; would add SPF alignment |
+
+Add both of these; neither conflicts with anything above:
+
+| Host | Type | Value |
+|---|---|---|
+| `@` | TXT | `v=spf1 include:_spf.google.com include:amazonses.com ~all` |
+| `_dmarc` | TXT | `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` |
+
+A domain may publish **only one** `v=spf1` record, so both senders go in one
+string. Namecheap TXT values take **no surrounding quotes** — quoting them stores
+the quotes literally and breaks the record.
+
+**Correction to an earlier version of this spec.** SPF is not what authenticates
+SES here. Without a custom MAIL FROM domain, SES uses an envelope sender at
+`amazonses.com`, so its SPF pass is not *aligned* with `smlcompany.ca` and does
+not satisfy DMARC. **SES satisfies DMARC through DKIM alignment** — that is what
+the six CNAMEs above are doing, and it already works. The SPF record's real job
+is authenticating **Google Workspace** mail, which currently has no SPF at all.
+`include:amazonses.com` is harmless and becomes useful if a custom MAIL FROM
+domain is configured later.
+
+Start DMARC at `p=none` — it collects reports without affecting delivery. Move to
+`quarantine` only after reports come back clean. Reports arrive as XML
+attachments, so filter them in Gmail, or drop `rua=` entirely and accept having
+no visibility.
+
+**Do not delete the ACM validation CNAMEs.** They are how the certificate for
+`adr.smlcompany.ca` auto-renews. Removing them breaks HTTPS at the next renewal
+— silently, months later (Q20).
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
diff --git a/docs/08-execution-protocol.md b/docs/08-execution-protocol.md
new file mode 100644
index 0000000..aec8254
--- /dev/null
+++ b/docs/08-execution-protocol.md
@@ -0,0 +1,127 @@
+# 08 — How we work
+
+`AGENTS.md` D17. Pouya architects. Claude Code implements, then adversarially
+reviews its own work. This is a standing agreement encoded in the repository, so
+it does not need restating in every prompt.
+
+---
+
+## The short version
+
+```
+/build the /med-arb/ page
+```
+
+That single command runs: plan → implement → adversarial review → resolve →
+verify → record. It requests deep reasoning, reads `AGENTS.md` and the specs,
+stops if the task conflicts with a locked decision, invokes two independent
+reviewers on the finished diff, resolves what they find, runs the checks, and
+appends the Change Log entry.
+
+You do not need to ask for thinking, for review, or for the record to be updated.
+Those are the agreement, not the request.
+
+## The three commands
+
+| Command | Does |
+|---|---|
+| `/build ` | The full loop. Use for every substantive change |
+| `/review [scope]` | The review pass alone, on the working tree or a named scope. Reports; fixes nothing without your say-so |
+| `/wrap` | End of session — updates `AGENTS.md` under its constitution and leaves the tree clean |
+
+## The two reviewers
+
+Both are defined in `.claude/agents/` and run in parallel on the diff.
+
+**`adversarial-reviewer`** reads the code: correctness and edge cases,
+accessibility, crawlability, performance budgets, security, and whether a
+materially simpler correct version exists.
+
+**`claims-auditor`** reads the copy against `AGENTS.md` §4 and nothing else. It
+extracts every factual assertion — credentials, roles, numbers, languages,
+locations, capabilities, and the JSON-LD — and traces each to the Verified table.
+Anything untraceable is reported and does not ship.
+
+It is a separate agent on purpose. A general-purpose reviewer will happily
+approve elegant code containing a claim that should never have been published,
+because professional-conduct compliance is not what it is looking at. On this
+project that is the highest-stakes failure mode, so it gets its own pass.
+
+**Both are instructed to treat uncertainty as a defect.** They will sometimes be
+wrong. That is the intended trade: explaining why a finding is mistaken costs
+minutes, and a missed defect on a licensed professional's public marketing page
+costs a great deal more.
+
+## The rule that makes it work
+
+**The reviewers are given the diff and the specs — never the implementer's
+explanation of why the work is correct.**
+
+A rationale anchors the reviewer. Told why something is right, a reviewer looks
+for confirmation and finds it; given only the artefact, it forms an independent
+view. That independence is the entire mechanism. Every other detail of this
+protocol is adjustable. This one is not.
+
+---
+
+## Writing a task
+
+The commands carry the process, so your prompt only needs to carry the decision.
+Short and specific beats long and hedged.
+
+**Good:**
+
+```
+/build the /med-arb/ page per docs/01-architecture.md
+```
+
+```
+/build step 5 of the build order — the practice index and all six area pages
+```
+
+```
+/build the intake form on /contact/, per docs/05-backend-spec.md.
+Booking stays parked — reserve the slot, render nothing.
+```
+
+**When you are making a decision rather than assigning work**, say so plainly and
+let it record the decision:
+
+```
+Decision: drop the /for-parties/ page. The plain-language audience can be
+served by a section on /mediation/ instead. Update AGENTS.md and the
+architecture spec, then tell me what else this affects.
+```
+
+**When you want an opinion before committing**, ask for one — do not ask for
+code:
+
+```
+Before building /fees/: read docs/07-fees.md and tell me what a referring
+lawyer would find missing from that page. Do not write anything yet.
+```
+
+## What it will do without being asked
+
+- Stop and ask when a task conflicts with a locked decision, or needs a fact it
+ does not have. A blocked build is a correct build.
+- Leave `TODO(pouya)` in the source and a numbered question in `AGENTS.md` §9
+ rather than inventing a fact or softening a claim into something defensible.
+- Surface `AGENTS.md` §12 Standing Reminders at the start of a substantial
+ session — including R1, your standing instruction about the licensure wording.
+- Refuse to publish a claim that is not in the §4 Verified register.
+- Append a Change Log entry covering decisions and plans, not only code.
+
+## What to watch for
+
+**A review that finds nothing.** Possible, but it should be rare, and it must
+list which lenses were applied and what was checked. "Looks good" is not a
+review — send it back.
+
+**Findings declined without a reason.** Declining is legitimate. Declining
+silently is not; the reasoning belongs in the Change Log so a later reader can
+see that judgement was exercised rather than the finding missed.
+
+**A check reported as passing that was not run.** "Should pass" is not a result.
+If a build, a Lighthouse run, or a JavaScript-disabled render was not actually
+executed, it must say so.