From 64bce105f80cf17e1611724329f25c6f569f3012 Mon Sep 17 00:00:00 2001 From: Pouya Lajevardi Date: Mon, 31 Aug 2026 13:44:57 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20eyebrow=2012px=20=E2=86=92=2013px=20fro?= =?UTF-8?q?m=20one=20definition;=20confirm=20and=20gate=20the=20intake=20T?= =?UTF-8?q?TL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of Pouya's rulings of 2026-08-31, committed as one tree at his instruction ("as-is") because AGENTS.md Change Log entry (ai) covers both and splitting it would mean rewriting the record rather than moving it. This is a deliberate departure from one-logical-change-per-commit, recorded here rather than left to be inferred. THE EYEBROW. `--text-eyebrow: 0.8125rem` added to tokens.css; `.eyebrow` in global.css retargeted to it. One edit site, which is what the design system claimed. 13px is not a rung on the modular ladder — it sits between xs and sm deliberately, because uppercase mono at 0.18em tracking reads smaller than it measures. `--text-2xs`'s comment stopped calling itself the eyebrow floor. The rendered sweep (22 pages × 2 widths, 838 mono elements measured over CDP, not grepped) found exactly one escape and one deliberate override: - h2.footer-heading, 176 instances — an ESCAPED EYEBROW. Five declarations byte-identical to `.eyebrow`, differing only in colour. Consolidated to `class="eyebrow footer-heading"`; the scoped rule is now colour + margin. The colour is load-bearing, not decorative: `.eyebrow`'s own `--text-meta` on ink is 3.07:1 and fails. - span.eyebrow.brand-tagline, 21 instances — HELD at `--text-2xs`. Measured: at 13px the header grows 81 → 83.4px while `--header-h` is pinned at 81 and drives `scroll-padding-top`; and with a seventh nav item the CTA lands past `.header-inner`'s content edge by 42px at 1216, 18px at 1240, 26px at 1280 and 1440. Document overflow is 0 in all of those, so no page-level check can see it. Insights is that seventh item. Everything else mono-uppercase is a genuinely different component and was left: the 0.06em `--tracking-wide` family, the 14px mixed-case designation strip, the /bio/ print sheet, /contact/'s form labels. Measured after: contrast unchanged on all 817 instances (11.09 / 8.11 / 5.47 / 5.01:1, all pass at 13px, which is still normal text and needs 4.5:1). Zero document overflow and identical header geometry at 15 widths. Two eyebrows gain a line below 414px — /'s hero, already wrapping at 320px before this, and /insights/'s empty state at 320px only. Accepted, not re-tuned. docs/02's type spec moved 11–12px → 13px and now enumerates the three 11px carve-outs instead of implying there are none. The /type-scale/ proof sheet (d) asked for no longer exists — deleted at build step 2 — so the spec prose is the proof sheet now. THE TTL. backend/intake/handler.mjs CONFIRMED to match `AttributeName=ttl`: it writes `ttl` as a Number, in epoch seconds, at RETENTION_MONTHS = 24. Nothing needed changing for the enable command. Removed `|| 0` from the TTL computation. DynamoDB does not expire an item whose TTL is more than five years past, so `ttl: 0` meant RETAINED FOREVER while /legal/privacy/ promises deletion — a fallback whose failure mode was the exact inverse of the claim it was protecting. Unreachable in practice, which is why it would never have been noticed. A bad value now fails the write. §7 records TTL as DISABLED at first verification, so the privacy policy's automatic-deletion promise was unbacked from the moment it was written. §7 is the only place that status lives; docs/05 and docs/06 carry the constraint and cite §7, because round 2 of review caught this change set reproducing the SES DKIM defect — five copies of a status that is about to be re-stamped. Added, and these are the gate: `TODO(pouya)` on /legal/privacy/'s retention section, §9 Q60, §12 R19. The page does not publish a period, it asserts a MECHANISM — deleted by the database rather than by someone remembering — and nothing in the toolchain can see that. check:claims is frozen with no pattern for it and deploy does not read docs/06. The copy was NOT softened: it is about to be true, and weakening a privacy commitment to make it defensible is the move the rules forbid. What was missing was the gate, not the caveat. Reviewed by adversarial-reviewer, two rounds (D19 cap), 15 findings, all accepted, none declined. claims-auditor did not run — D20. Round 1's findings were almost entirely in prose written that session, and round 2's blocking finding plus its sharpest should-fix were both defects in round 1's own fixes. public/pouya-lajevardi-bio.pdf is deliberately NOT in this commit. It was regenerated and reverted: /bio/'s eyebrow sits inside `.no-print`, so the sheet has no eyebrow at all, and `cmp -l` showed exactly 10 differing bytes, all in /CreationDate and /ModDate. Gates, every one read as an exit status and none through a pipe: check 0, build 0 (22 pages), check:claims 0, og:proof 0, check:intake 0, lint 0, minifier tripwire clean, TODO in dist 0. Lighthouse run twice with identical output: perf 99 on / and 100 on the other 21, a11y 100, best practices 100, SEO 100 on every indexable page, CLS 0.000, LCP 1.50–2.03s. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5 --- AGENTS.md | 159 +++++++++++++++++++++++++++++++- backend/intake/handler.mjs | 29 +++--- docs/02-design-system.md | 30 +++++- docs/05-backend-spec.md | 11 ++- docs/06-deployment.md | 16 ++-- src/components/SiteFooter.astro | 16 ++-- src/components/SiteHeader.astro | 24 +++-- src/layouts/BaseLayout.astro | 2 +- src/pages/about.astro | 3 +- src/pages/legal/privacy.astro | 20 +++- src/styles/global.css | 6 +- src/styles/tokens.css | 11 ++- 12 files changed, 276 insertions(+), 51 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3f47dde..836b080 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -721,7 +721,7 @@ the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — dec | 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]` | +| Intake table | `adr-intake-submissions` (DynamoDB, ca-central-1) `[verified 2026-08-26]`. ⚠️ **TTL WAS `DISABLED` AT FIRST VERIFICATION — Pouya ran `describe-time-to-live` on 2026-08-31 and it returned `DISABLED`.** `/legal/privacy/` has stated since build step 10 that a record is *"deleted automatically by the database rather than by someone remembering to do it"* after 24 months, so **that promise was unbacked from the moment it was written, and it stays unbacked in this record until a test record is confirmed deleted** — the handler wrote the attribute and nothing on the table consumed it. This is the Q22 shape on a public privacy commitment rather than on a deploy control: a documented mechanism that did not exist. **Pouya initiated enabling the same day with `AttributeName=ttl`** ``[assumed — initiated 2026-08-31 on his report; the status was NOT re-read to this record, and DynamoDB reports `ENABLING` for a period before it settles]``**, which is the attribute the handler writes** — `backend/intake/handler.mjs` writes `ttl` as a Number in **epoch seconds** at **24 months** (`RETENTION_MONTHS = 24`, added to `getUTCMonth()`), matching `docs/05` §Retention and the `ttl` row of its item table `[verified 2026-08-31 — read from the handler, not recalled]`. ⚠️ **`ENABLED` IS NOT YET STAMPED HERE, DELIBERATELY.** Enabling was initiated, not confirmed to this record, and DynamoDB reports `ENABLING` for a period before it settles — so the status must be re-run and stamped. **And `ENABLED` would prove the setting, not the behaviour:** `docs/06`'s cutover item requires a test record written with a near-future `ttl` and confirmed to disappear, and it is not ticked by reading this row or the handler code | | SES identities | Domain `smlcompany.ca` **verified for sending** `[verified 2026-08-26]`; addresses `info@`, `intake@`, `adr@` | | SES account | **Production access GRANTED** — out of the sandbox in `ca-central-1`, confirmed by AWS in writing and effective immediately `[verified 2026-08-26 — Q19 closed]`. Mail now reaches unverified recipients, so the inquirer confirmation in D18 works | | Mail hosting | **Google Workspace** — MX `1 smtp.google.com`; `google._domainkey` present, so Google DKIM is configured `[verified 2026-08-26 — DNS query]` | @@ -772,6 +772,7 @@ Nothing below can be invented. Each needs an answer from Pouya. | # | Question | Blocks | |---|---|---| +| **Q60** | ⚠️ **IS DYNAMODB TTL `ENABLED` ON THE INTAKE TABLE, AND HAS A TEST RECORD BEEN OBSERVED TO DISAPPEAR?** Two halves, and neither substitutes for the other. `/legal/privacy/` does not merely publish a retention *period* — it asserts a **mechanism**: *"the record is deleted automatically by the database rather than by someone remembering to do it"*. `describe-time-to-live` returned **`DISABLED`** when Pouya first ran it (2026-08-31; §7 holds the status and its stamp), so the mechanism was not running from the day that sentence was written. **The handler's side is already verified and is not what is being asked** — it writes `ttl` as a Number in epoch seconds at 24 months, which is the attribute name being enabled; writing the attribute is not the mechanism. **Do not answer this from the handler code, and do not answer it from `ENABLED` alone:** `ENABLED` proves the setting, and only a record written with a near-future `ttl` and watched to vanish proves the behaviour. `TODO(pouya)` sits on the retention section of `src/pages/legal/privacy.astro`; `docs/06`'s cutover checklist carries both halves; §12 R19 keeps it surfacing. **Why this is a numbered question and not only a checklist line:** `CLAUDE.md` requires a `TODO(pouya)` plus a §9 row when a page needs a fact the repository does not have, and this page needs one — a cutover checklist fires once, at cutover, and §9 is what a person editing this page reads. Raised by `adversarial-reviewer` round 2, 2026-08-31 | **`/legal/privacy/` going public.** Nothing else — no other page states the mechanism, verified by sweeping `dist/` for the retention vocabulary and reading each hit in context | | ~~Q59~~ | ✅ **RULED AND CLOSED 2026-08-31 — Pouya. OVERTIME RUNS FROM THE SESSION CAP**: the fourth hour of a half day, the seventh of a full day. Not the billed envelope. `/fees/` shipped at build step 9 on this ruling and `docs/07` carries it in full. ⚠️ **THIS ROW NAMED A CONSTANT THAT NO LONGER EXISTS** — `FEES.mediation.overtimeStartsAfterSessionHours` was deleted the same day as dead data: nothing read it, so reversing it would have changed nothing and failed nothing, which is Q22's shape at constant scope. **Where the ruling actually lives:** the trigger is rendered on `/fees/` from `halfDay.hours` / `fullDay.hours`, and `FEES.mediation.reservation` carries the half that publishes as prose. Found by `adversarial-reviewer` round 2 — §9 is what a later implementer reads to find where a ruling is recorded, so pointing it at a deleted identifier is the same defect one layer up. ⚠️ **AND THE RULING CAME WITH A SECOND HALF THAT ANSWERS THE ARITHMETIC ANOMALY THIS ROW EXISTED TO ESCALATE, WHICH THE TRIGGER ALONE COULD NOT.** His words: *"a full day reserves the day; half-day overtime is subject to availability."* **The full-day fee buys the DAY, not six hours of it.** Read as a price comparison the table below says the full-day rate is never the cheaper choice; read knowing what each fee reserves, the $2,000-narrowing-to-$500 spread is the price of certainty rather than a defect. The sentence is `FEES.mediation.reservation` and it publishes **adjacent to the overtime row**, not as a footnote — the same structural rule as `PROCESS_FRAMING` beside the five timings under Q43, because a reader who takes the number and skips the framing has read a different offer. **THE ANOMALY IS NOT CLOSED AND STAYS ON §12 R5.** The gap is in D14's own figures — the half-to-full step is $2,000 against $1,500 for three hours of overtime — and the reservation point explains what it buys without removing it; the spread is largest at three to five hours, which is the band a half-day booking actually overruns into. `docs/07` §Recorded dissent carries the table for the 12-month review. **The original question, kept because the shape of it is the lesson.** *Where does the overtime hour start?* `docs/07`'s card carried *"Overtime, per hour — $500"* and had never said what it was overtime **to**. Q58's ruling settled the two allowances and did not reach this; Q15–Q17's answer records the rate with no trigger. The two candidates were the session cap (3 h / 6 h) and the billed envelope (5 h / 9 h), and this repository was barred from picking one — a fee term is a fact we do not have, and `CLAUDE.md`'s rule for that is a question, not an inference. **It cost two strikes to hold that line:** a first pass at `docs/07`'s Q58 note asserted the session cap as applied fact and `adversarial-reviewer` struck it in the change set that wrote it; a round-1 fix then published the $500 rate on `/for-parties/` beside an unambiguous *"up to 3 hours"*, which **defines the trigger by adjacency** — nothing else on the page is a quantity it can attach to — and round 2 struck that too. Both strikes were right, and the ruling supplied the value they were waiting for | ~~`/fees/`, `/for-parties/`~~ — both now unblocked and shipped | | ~~Q58~~ | **RULED 2026-08-31 — `hours` IS THE SESSION, AND THE AMBIGUITY WAS IN `docs/07` RATHER THAN IN ANY COPY. Pouya owned it in terms:** *"the ambiguity is mine… My `docs/07` wording said "up to 3.5 h, including 2 h preparation", which is genuinely unclear: 3.5 was meant as the TOTAL time committed, of which 2 is preparation — leaving 1.5 hours in the room. Your arithmetic caught it: if prep sat inside, 3.5 and 7 wouldn't be exactly 2×, because preparation doesn't scale with session length. The intended reading is the market's, and my wording obscured it."* **THE CORRECTED CARD, in his words:** *"Half day — up to 3 hours of session. Fee includes up to 2 hours of preparation. $2,000. Full day — up to 6 hours of session. Fee includes up to 3 hours of preparation. $4,000."* His reason for 3 and 6: *"the market convention — Patey and Zuber both publish "all or part of 3 hours" and "all or part of 6 hours", and those were the comparables the rate was set against. Selling 1.5 hours of room time as a half day would be an outlier nobody would recognise."* ⚠️ **ONE PROVENANCE NOTE, and it is R14's rule rather than a doubt about the ruling:** `docs/07`'s committed extract records Patey and Zuber at **3 h** and **6 h** but **does not carry the phrase "all or part of"** — so `docs/07` cites the hours, not the phrase, and the phrase is not attributed to them anywhere in the repository. The hours corroborate the ruling on their own, and ADR Chambers' roster rate in the same table is the clearest corroboration of the *shape*: *"one half hour of preparation time per party **and** up to three hours of mediation"* — preparation counted separately from a three-hour session. **APPLIED:** `docs/07`'s two card rows and its §All parameters confirmed (which prescribed the flat *"including 2 hours"*, the form `/for-parties/` then shipped); `FEES.mediation.*.hours` 3.5 → 3 and 7 → 6 with the semantics in the constant's doc comment; `/for-parties/` now states the session length interpolated from the constant and the preparation allowance **as a cap**. **The preparation allowance is CAPPED and must be published as capped** — *"including **up to** 2 hours"*, never the flat form and never "preparation included". **`/fees/` is UNBLOCKED for build step 9.** **The question as raised is preserved below.** **DOES `hours` IN THE MEDIATION RATE CARD MEAN THE LENGTH OF THE DAY, OR THE BILLED ENVELOPE INCLUDING PREPARATION?** `docs/07-fees.md` reads *"Half day — **up to 3.5 h, including 2 h preparation**"* and *"Full day — up to 7 h, including 3 h preparation"*. Taken at face value, 3.5 is the whole billed envelope and the **time in the room is 1.5 h** for a half day and **4 h** for a full day. **Against that reading:** 3.5 and 7 are exactly 2×, which they would not be if preparation sat inside them (1.5 vs 4 is not 2×). So either the card's wording is wrong in the one document that is the authority on money, or `FEES.mediation.*.hours` in `src/data/site.ts` does not mean what a page would naturally publish it as. **This was one sentence from shipping.** A draft of `/for-parties/` answered *"What happens on the day?"* with *"A half day is about 3.5 hours"* — the envelope presented as the day, to the reader least able to check it. The sentence was removed rather than resolved by inference; the page now says only that you book a half day or a full day. **What is needed:** one line from Pouya saying which the 3.5 and 7 are. Then `docs/07`'s two rows or `site.ts`'s field gains the correction, and the semantics go in the constant's doc comment (a warning is there now). **`/fees/` at build step 9 publishes this table and cannot be built without the answer.** Raised by `adversarial-reviewer`, 2026-08-30 | **Nothing.** No page stated a duration while the question was open — the one draft sentence that did was removed rather than reconciled, which is why the ruling had nothing to correct in public copy | | ~~Q57~~ | **CLOSED 2026-08-31 — NO SEVENTH UNDERTAKING, AND THE PAGE IS COMPLETE AS IT STANDS.** Pouya: *"`/process/` stating when conflicts are run and what the check needs is complete. A reader assumes the outcome, and the obvious undertaking ("if a conflict is found I decline") adds nothing a reader doesn't already infer. Your restraint was right — §4's gate held. Record it closed rather than open, so it stops appearing in the live list."* **So this is a closure, not a deferral:** the answer is that the page says nothing further, which was one of the two outcomes the question named. §4 gains no seventh conduct undertaking and `CONDUCT_UNDERTAKINGS` still holds six. **APPLIED:** the `TODO(pouya)` is removed from `src/pages/process.astro` §Conflicts and replaced with the ruling, so a later reader finds the decision where the question was rather than an open marker; the file header's *"see the TODO below"* is corrected to cite this closure. `src/` now carries **zero** live `TODO(pouya)` markers. **The question as raised is preserved below.** **WHAT HAPPENS WHEN A CONFLICTS CHECK TURNS SOMETHING UP?** `/process/` §Conflicts ships saying **when** the check runs (the intake call, before anything is agreed) and **what it needs** (full legal names of the parties, the parent or affiliate actually behind the dispute, counsel on each side). It stops there, and the stop is deliberate: **any sentence naming the outcome is a SEVENTH conduct undertaking**, and §4's gate for that class is one line — *"an undertaking may be published only where Pouya has made it in terms. Not 'would obviously agree to', not 'follows from the process' — said."* *"If a conflict appears I decline the appointment"* is exactly what that gate refuses to let this repository infer, however obvious it looks. **What is needed:** one sentence from Pouya, in his words, or a decision that the page says nothing further. `TODO(pouya)` sits at `src/pages/process.astro` §Conflicts. Raised at build step 6, 2026-08-30 | **Nothing.** The section shipped accurate and unchanged; what closed is whether anything more was owed | @@ -928,6 +929,7 @@ never being raised again. | ~~R16~~ | ✅ **DISCHARGED 2026-08-31 — build step 9, which is the trigger this row set. The one-page PDF exists, is committed at `public/pouya-lajevardi-bio.pdf`, and `/about/` links it.** Q45's two open sub-decisions are both taken, and the second one is what makes the first safe. **(a) Generated at build, or authored once? NEITHER — and the third option answers this row's actual objection.** The bio is a PAGE, `src/pages/bio.astro`, so every line of it is reviewed by the same apparatus as every other page: `astro check`, `npm run check:claims` on the built HTML, the per-step code review, and the cutover claims pass. `npm run bio:pdf` then RENDERS the PDF from the built page through Chrome, with **no new dependency** — `chrome-launcher` is already a devDependency because Lighthouse needs it. That is aimed squarely at what this row said the problem was: *"It is the one artefact class this project's review apparatus cannot reach… a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again."* A rendering of a reviewed page is back inside the apparatus. **It is deliberately NOT part of `astro build`** — CI has no Chrome, and a build step that cannot run in CI is Q22's shape again — so the PDF is committed, which R14 wants anyway. **(b) Does it carry anything the site does not? NO, and that is the answer that avoids the §4 question this row flagged.** Every line renders from `CREDENTIALS`, `ROLE`, `BOUTIQUE`, `PRACTICE_AREAS`, `FEES` and `CONTACT`. No matter list — which this row correctly said *"would collide with §4 Forbidden directly"* — no referees, and no figure that is not on `/fees/`. The fee summary IS there, because this row's own reasoning is that an appointment proposal needs the rate card as much as the bio. ⚠️ **`npm run bio:pdf` ASSERTS ONE PAGE and writes nothing if the count is wrong** — counted from the PDF's own page objects, cross-checked against the page tree's `/Count`. A one-page bio that silently becomes two is invisible from the source, because it depends on the print stylesheet, the paper size and how much §4 has grown. **And reading the rendered PDF caught a real breach the source review had not:** the opening sentence read *"I act as a neutral in commercial disputes — as a mediator, as an arbitrator in commercial matters…"*, whose leading clause scoped **mediation**, which Q56 leaves unscoped deliberately. `/bio/` is `noindex` and out of the sitemap: it is a condensed duplicate of `/about/` and `/fees/`. **What is NOT discharged:** nothing in the build regenerates the PDF. It must be re-rendered and re-committed whenever `/bio/`, §4, the rate card or the print styles change, and `docs/06`'s cutover checklist carries that | 2026-08-28 | It is the one artefact class this project's review apparatus cannot reach. A web page is re-reviewed by every audit and re-checked by every build; a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again | | R17 | **THE DEPLOY KEY'S FIRST ROTATION IS DUE 2026-11-26, AND THAT DATE IS THE WHOLE ROW.** Access key for `adr-sml-deploy` created **2026-08-26**; D3 commits to **quarterly** rotation. Pouya's instruction when Q22 closed: *"Put the DATE in the R-series reminder, not the intention."* **Rotation procedure is in `docs/06-deployment.md` §Key rotation** — create the second key, update the Gitea secrets, verify a deploy, **then** delete the first; never delete before verifying, or the next push fails with no way to authenticate the fix. **Then set the next date here (2027-02-26) rather than deleting the row.** Also re-run **all nine** `simulate-principal-policy` checks after any policy edit (they are listed and reproducible in `docs/reference/deploy-credential-verification.md` §5) — the narrowness is the control (§10), and a widened policy is invisible from inside the repo. **The key is currently UNUSED — `AccessKeyLastUsed` returns `ServiceName`/`Region` = `N/A` and no `LastUsedDate`** — because deploys still run locally via `npm run deploy`; that does not pause the clock, and an unused long-lived key is exactly the one nobody remembers to rotate | 2026-08-28 | This is the reminder D3 has been waiting for a subject since 2026-08-26. §10 records the reason it matters more here than on a normal project: the secret lives on **jointly-administered** infrastructure, and the account also holds another business's client-database backups. A static key on shared infrastructure is only as safe as its age and its scope, and **nothing in the repo, the build, or AWS will prompt anyone on either.** §7 carries the inventory; this row carries the date | | R18 | **THE SIX `docs/reference/` EXTRACTS BEHIND `/practice/*` ARE DATED 2026-08-29, AND SIX SHIPPED SENTENCES TURN ON FACTS THAT MOVE.** Build step 5 put statute, regulation, tribunal and bill status onto public pages — sourced, but **sourced as at one day**. The volatile ones, in order of how fast they move: **(a) federal Bill C-36** — `/practice/technology/` says it *"was introduced in June 2026 and was at second reading when this page was written"*; if it receives royal assent the page is wrong about the most load-bearing fact on it. **(b) the Ontario regulation under `Electricity Act` s. 28.1** — `/practice/energy/` says it *"had not been made as of August 2026"*. **(c) the ERO 026-0853 consultation**, comment period to **12 September 2026**. **(d) the Tribunals Ontario annual report** — `/practice/insurance/` publishes FY2024-25 figures and the extract records that a 2025-26 edition was never ruled out. **(e) the SABS**, amended with effect 1 July 2026. **(f) the ADRIC National Mediation Rules**, under review by ADRIC's own committee. **(g) ADRIC's Code of Ethics** — added 2026-08-30, build step 6. `/process/` §Confidentiality quotes it verbatim from `docs/reference/adr-institution-names.md` (retrieved 2026-08-29) **with a live link to ADRIC's page**, which is what makes it checkable and also what makes a stale quotation visible. It is the slowest-moving item here — a professional code, not a bill — so it does not change the cadence; it is listed because the trigger below says "all six" and there are now seven. **THE TRIGGER: re-check all seven extracts before any cutover, and before any republish that turns on one of them — the same event trigger R10 uses.** Then re-stamp the extract. **A page that was true when it was written and is false when it is read is still a false page**, and this is the first change set on the project to put that class of fact into public copy at volume | 2026-08-29 | Six sentences, six files, one retrieval date. Nothing here fires on its own; a fact with a shelf life and no owner is exactly what §12 exists for | +| R19 | ⚠️ **DYNAMODB TTL BACKS A PUBLISHED PRIVACY PROMISE AND `/legal/privacy/` ASSERTS THE MECHANISM, NOT JUST THE PERIOD.** **§7 records the status and its stamp; this row deliberately does not restate it** — one place for a service status, or the copy that goes stale is the one nobody re-reads. **THE TRIGGER, and its two halves are not interchangeable: re-run `describe-time-to-live` and confirm `ENABLED`, THEN write a record with a near-future `ttl` and confirm it actually disappears.** `ENABLED` proves the setting; only the test record proves the behaviour. Writing the attribute proves neither — the handler's side is verified and is not what this row is about. Both halves are on `docs/06`'s cutover checklist and the question is §9 Q60. Re-stamp §7 the same day, **and when you do, sweep for the copies: this fact reached five files outside §7 in one change set and had to be pulled back.** Close this row only when the test record has been observed to vanish | 2026-08-31 | **This is R9's exact shape at higher stakes.** R9 exists because the SES alarms are configured and notify nobody until one subscription is confirmed — a control that reads as covered and is not. Here the control backs a **statement to the public on a privacy policy**, which is the one class of claim this project treats as unrecoverable, and the failure is silent in both directions: nothing in the repo, the build or AWS reports that records are accumulating forever. A cutover checklist fires once; §12 is read aloud every session | | ~~R7~~ | **RATIFIED / SUPERSEDED 2026-08-26.** (a) Cache-policy table matching the pipeline — **accepted**; documenting what the pipeline does beats documenting an intention. (b) `s3:AbortMultipartUpload` omitted — **accepted, reasoning corrected**: the lifecycle rule does not exist and is therefore not the cover; the actual cover is that `aws s3 sync` only goes multipart above 8 MB and the largest asset is a 357 KB portrait. Recorded in `docs/06-deployment.md` with a revisit trigger. (c) The `aws s3 ls` pre-flight — **superseded** by the variable guard now running as the workflow's first step | 2026-08-26 | — | | ~~R8~~ | **PROMOTED TO A RULE 2026-08-26.** A reminder was too weak for a pattern that survived three entries. *A sweep is a command, not a claim* now sits in `CLAUDE.md` under Conventions, in `/build` Phase 6, and in `/wrap` step 3: any claim that a change was applied across files must cite the command and be written only after reading its output | 2026-08-26 | — | @@ -935,6 +937,161 @@ never being raised again. # Change Log +## 2026-08-31 (ai) — The eyebrow goes to 13 px from one definition, and the sweep by rendering found one escaped copy of it and one deliberate override; DynamoDB TTL was `DISABLED` while `/legal/privacy/` promised automatic deletion + +**Type:** design-system change (site-wide) + an operational fact recorded + a one-line handler correction. + +**Scope:** Pouya's two items of 2026-08-31. (1) The mono eyebrow reads too small against the display type — raise it one step, at the single definition, and sweep for escaped copies **by rendering rather than by grep**. (2) He ran `describe-time-to-live` on the intake table and it returned `DISABLED`; confirm the handler's attribute before he re-runs the enable command, and record the state. `claims-auditor` did not run — D20. No copy changed, so nothing in §4 is touched. + +--- + +### 1. The eyebrow: 12 px → 13 px + +**Old → new.** `.eyebrow` in `src/styles/global.css` — the one definition `docs/02` gives it — moves from `font-size: var(--text-xs)` (12 px) to `font-size: var(--text-eyebrow)` (**0.8125rem, 13 px**). With `0.18em` tracking the letter-spacing follows it from 2.16 px to 2.34 px, which is the reason 1 px is visible here at all. + +**`--text-eyebrow` is a new token and it is deliberately not a rung.** The scale runs 11 / 12 / 14 / 16 at the small end; 13 px sits between `--text-xs` and `--text-sm` because uppercase mono at this tracking reads a size smaller than it measures. Written as a token rather than a literal for the reason step 1's review already established once — `SiteHeader` wrote `0.6875rem` as a literal and that was finding #7 — and so that `.eyebrow` and the footer's column headings cannot drift apart again. + +**`--text-2xs`'s comment was wrong the moment this landed and is corrected.** It read `/* 11 — eyebrow */` and described itself as *"the eyebrow floor docs/02 sets at 11px"*. It is now the **compressed-label** floor: the header tagline and the `/bio/` print sheet. Nothing about the eyebrow. + +**`docs/02` (c).** The eyebrow rule read *"mono, 11–12 px, `0.18em` tracking"*; it now reads 13 px, states that 13 px is not a rung and why, records that there is one definition and that the footer headings carry the class rather than repeating it, and adds a line for what `--text-2xs` is now for. The scale line read *"Tokens `--text-xs` through `--text-6xl`"* and now names `--text-2xs` as the floor and `--text-eyebrow` alongside. + +⚠️ **(d)'s "type-scale page" does not exist, and this is reported rather than quietly skipped.** The proof sheet at `/type-scale/` was **deleted at build step 2** — `AGENTS.md` records it in two places, and `docs/05` §314 and `docs/reference/brand-assets.md` §87 both note the deletion. There is no rendered proof sheet to bring into line with reality; `docs/02`'s prose is the type spec, and that is what (c) updated. + +### 2. The sweep, by rendering — and what it judged + +**Instrument.** Headless Chrome over CDP, `--force-prefers-reduced-motion`, **all 22 built pages at 1280 and 390 CSS px**, enumerating every element whose *computed* `font-family` contains `Geist Mono` **and** which is `text-transform: uppercase` **or** carries non-`normal` tracking **and** has a text node of its own. **838 rows, both revisions.** Deliberately a superset of "mono + uppercase + tracked", so an element that is eyebrow-shaped on two of the three axes could not hide. Run against `dist/`, not the dev server. + +**Distinct treatments, before → after.** Counts are instances across 22 pages × 2 widths. + +| element | before | after | judgement | +|---|---|---|---| +| `p.eyebrow` (222) | 12 px / 2.16 | **13 px / 2.34** | the eyebrow itself | +| `h2.footer-heading` (176) | 12 px / 2.16 | **13 px / 2.34** | ⚠️ **ESCAPED EYEBROW — consolidated into the class.** Five declarations that were a byte-for-byte copy of `.eyebrow` (family, size, weight, tracking, uppercase), differing only in colour. Raising the eyebrow would have left all four footer columns behind at 12 px | +| `span.eyebrow.brand-tagline` (21 shown) | 11 px / 1.98 | 11 px / 1.98 | **eyebrow with a deliberate, measured override — held.** It carries `.eyebrow`, so it would have followed to 13 px. The string measures **283.1 px** at 11 px; 13 px adds ~51 px to a one-row header whose true fit `docs/02` puts at **1047 px** with seven nav items. The override is a layout constraint, and its comment now says so instead of citing "the eyebrow floor in docs/02" | +| `label`, `legend`, `span.req`, `dt` on `/contact/` (46) | 11 px / 1.98 | unchanged | **different component** — form labels and definition terms, not section labels. See the batched list: they now sit 2 px below the eyebrow on the page that collects inquiries | +| `h2`, `p.sheet-strap` on `/bio/` (22) | 11 px / 1.98 | unchanged | **different component** — the print sheet, compressed because `npm run bio:pdf` asserts one page. Its print block already overrides to 7–9 pt | +| `span.visually-hidden` (12) | 11 px / 1.98 | unchanged | inherits from `label`; never rendered | +| `dt.def-name` (100), `span.pill` (36), Breadcrumbs `a`/`span` (48), `dd.credential-label` (22), `p.step-timing` (20), `span.step-n` (20), `li` (12) | 12 px / **0.72** | unchanged | **different idiom — `--tracking-wide` (0.06 em), not `--tracking-eyebrow` (0.18 em).** A third of the tracking is what separates these from the eyebrow, and Pouya named pills as legitimate | +| `p.footer-designation` (44), `span.designation-part` (8), `span.sep` (6), `p.sheet-desigs` (2) | 14 px, **mixed case** | unchanged | **different component** — the designation strip, which Pouya named as legitimate. Not uppercase, so not eyebrow-shaped | + +**So: one escaped copy, one deliberate override, and every other mono-uppercase element is a genuinely different component.** The consolidation is `