feat: eyebrow 12px → 13px from one definition; confirm and gate the intake TTL

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-08-31 13:44:57 -04:00
co-authored by Claude Opus 5
parent 9f2d2eeb04
commit 64bce105f8
12 changed files with 276 additions and 51 deletions
+27 -3
View File
@@ -98,7 +98,8 @@ 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`.
the display end. Tokens `--text-2xs` through `--text-6xl` in `tokens.css`, plus
`--text-eyebrow` at 13 px — see the eyebrow rule below for why it is not a rung.
**Rules.**
@@ -107,8 +108,31 @@ the display end. Tokens `--text-xs` through `--text-6xl` in `tokens.css`.
- Display line-height `0.95``1.05`; letter-spacing `-0.02em`.
- Body line-height `1.6`. Measure capped at `68ch` — the old site ran full-bleed
paragraphs at 1400 px, which is unreadable.
- Eyebrows: mono, 1112 px, `0.18em` tracking, uppercase, always paired with a
real heading. An eyebrow is not a heading and never carries the `<h*>`.
- Eyebrows: mono, **13 px** (`--text-eyebrow`), `0.18em` tracking, uppercase,
always paired with a real heading. **13 px is not a rung on the scale** — it
sits between `--text-xs` and `--text-sm` because uppercase mono at this
tracking reads smaller than it measures, and at 12 px it was being lost
against the display type (Pouya, 2026-08-31; it read 1112 px until then).
There is **one definition**, `.eyebrow` in `global.css`; the footer's column
headings carry that class rather than repeating it. An eyebrow is not a
heading and never carries the `<h*>` — a real heading may carry the class.
**"One definition" means one definition of the 13 px eyebrow.** The four 11 px
blocks in the next bullet repeat the mono/`0.18em`/uppercase treatment
deliberately and are not covered by it.
- `--text-2xs` (11 px) is the compressed-label floor, not the eyebrow floor.
**Three carve-outs, and they are deliberate rather than incidental** — do not
read this line as licence for an 11 px label anywhere else. (a) The header
tagline, and **it is load-bearing today, not at some future step:** 13 px takes
the header to 83.4 px while `--header-h` is 81 px, which `global.css` feeds
into `scroll-padding-top`. A seventh nav item is the second reason and that one
does arrive with Insights. Both measurements are in `SiteHeader.astro`. (b) The `/bio/` print sheet, which
`npm run bio:pdf` holds to one page. (c) On `/contact/`, both the
`<label>`/`<legend>` set and the direct-contact `<dl>` terms.
⚠️ **(c) is the weak one and is flagged as such.** 11 px uppercase mono
measures 5.47 : 1 and breaches no floor in this document or in WCAG, but it
now sits two steps below the eyebrow on the page that collects inquiries, for
no reason beyond it having been written that way. Raised with Pouya
2026-08-31; it is an accepted size, not a ratified one.
- 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.
+9 -2
View File
@@ -177,7 +177,7 @@ elsewhere — §7 has the table name and region.
| `sk` | `<ISO-8601 timestamp>` |
| fields | as above |
| `sourceIp`, `userAgent` | abuse investigation only |
| `ttl` | epoch seconds — **automatic deletion** |
| `ttl` | epoch seconds — **the input to automatic deletion; see §Retention for why writing it is not the mechanism** |
**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
@@ -188,6 +188,13 @@ principal.
**24 months, enforced by DynamoDB TTL.** Not a policy someone remembers — a
mechanism that runs whether anyone remembers or not.
⚠️ **WRITING THE ATTRIBUTE IS NOT THE MECHANISM.** The handler supplies `ttl`;
TTL must also be **enabled on the table**, and **`AGENTS.md` §7 records whether
it is — this section deliberately does not.** So the paragraph above is a
statement about the design and not about the running system until the cutover
item below is ticked on **both** halves: `ENABLED` by command, and a test record
observed to disappear.
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
@@ -366,7 +373,7 @@ Plausible or Fathom, cookieless, no consent banner.
- [x] **Honeypot live.** ⚠️ **The timing check is NOT implemented** — see deviation 1 above; it is unimplementable on a CDN-cached static page and would be a control that does nothing
- [ ] **Rate limit configured** — API Gateway throttling, 5 requests / 5 minutes per source IP. Not expressible in handler code; not done
- [x] **The form's own protection is the `Origin` check, not CORS** — see deviation 2. CORS on the endpoint still to be restricted for scripted calls
- [ ] **TTL set and verified by test record.** ⚠️ **THIS ONE BACKS A PUBLISHED PROMISE.** `/legal/privacy/` states that records are deleted automatically after 24 months. The handler writes the `ttl` attribute; TTL must also be **enabled on the table**, and §7 does not record that it is. Until this is verified the page promises a mechanism that may not run
- [ ] **TTL set and verified by test record.** ⚠️ **THIS ONE BACKS A PUBLISHED PROMISE, AND THE SETTING WAS MEASURED OFF ON 2026-08-31.** `/legal/privacy/` states that records are deleted automatically after 24 months. The handler writes the `ttl` attribute — epoch seconds, 24 months, confirmed against this spec `[verified 2026-08-31]` — but `describe-time-to-live` returned **`DISABLED`** when Pouya first ran it, so the page promised a mechanism that was **not running**, from the day it was written. Enabling was initiated the same day with `AttributeName=ttl`; §7 records the state and deliberately does not yet stamp `ENABLED`. **The test record is what closes this, not the status:** `ENABLED` proves the setting, a record written with a near-future `ttl` and observed to vanish proves the behaviour
- [ ] KMS customer-managed key and PITR enabled. **Neither is claimed on `/legal/privacy/`** — the page says "encrypted at rest", which is true of every DynamoDB table unconditionally, and does not mention either of these because §7 does not verify them
- [ ] Both emails send; SPF/DKIM/DMARC aligned; inbox-tested, not spam-tested
- [ ] DLQ and CloudWatch alarm configured. The handler writes to DynamoDB **before** sending mail, so a replay cannot lose a submission
+10 -6
View File
@@ -497,12 +497,16 @@ Then invalidate `/*`.
CODE.** `/legal/privacy/` states that a record is *"deleted automatically
by the database rather than by someone remembering to do it"* after 24
months. The handler writes the `ttl` attribute; **TTL must also be enabled
on the table**, and §7 does not record that it is. Until this is verified,
that page promises a mechanism that may not run — which on a privacy policy
is a false statement to the public. `aws dynamodb describe-time-to-live
--table-name <the table §7 records>` and confirm
`TimeToLiveStatus: ENABLED`, then write a record with a near-future `ttl`
and confirm it disappears.
on the table** — ⚠️ **§7 records whether it is, and it has not always
been**, which is why this item cannot be ticked from the handler code: the
handler only writes the attribute. On a privacy policy an unbacked
mechanism is a false statement to the public. `aws dynamodb
describe-time-to-live --table-name <the table §7 records>` and confirm
`TimeToLiveStatus: ENABLED` — then, because **`ENABLED` proves the setting
and not the behaviour**, write a record with a near-future `ttl` and
confirm it actually disappears. Both halves, or the item stays open. §9
Q60 and §12 R19 track it; `src/pages/legal/privacy.astro` carries the
matching `TODO(pouya)`.
- [ ] **`npm run check:intake`** — the form's field table and the Lambda's agree
on names, requiredness, caps and option sets. They are two independent
tables on purpose (`docs/05`); this is what stops them drifting