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
+7 -9
View File
@@ -71,7 +71,7 @@ const aboutLinks = [
<div class="footer-grid">
<nav class="footer-nav" aria-label="Footer">
<div class="footer-col">
<h2 class="footer-heading">Practice areas</h2>
<h2 class="eyebrow footer-heading">Practice areas</h2>
<ul role="list">
{
PRACTICE_AREAS.map((area) => (
@@ -85,7 +85,7 @@ const aboutLinks = [
</div>
<div class="footer-col">
<h2 class="footer-heading">Process</h2>
<h2 class="eyebrow footer-heading">Process</h2>
<ul role="list">
{
processLinks.map((link) => (
@@ -98,7 +98,7 @@ const aboutLinks = [
</div>
<div class="footer-col">
<h2 class="footer-heading">About</h2>
<h2 class="eyebrow footer-heading">About</h2>
<ul role="list">
{
aboutLinks.map((link) => (
@@ -112,7 +112,7 @@ const aboutLinks = [
</nav>
<div class="footer-col footer-contact">
<h2 class="footer-heading">Contact</h2>
<h2 class="eyebrow footer-heading">Contact</h2>
<ul role="list">
<li><a href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a></li>
<li><span class="footer-meta">{CONTACT.phoneFallback}</span></li>
@@ -207,12 +207,10 @@ const aboutLinks = [
gap: var(--space-7) var(--space-6);
}
/* Type comes from the global `.eyebrow` class on the element; only what differs
is here. The colour is load-bearing, not decorative: gold-l on ink is
11.09:1 and `.eyebrow`'s own `--text-meta` on ink is 3.07:1, which fails. */
.footer-heading {
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-eyebrow);
text-transform: uppercase;
color: var(--text-inverse-2);
margin-block-end: var(--space-4);
}
+18 -6
View File
@@ -190,14 +190,25 @@ const isHome = path === '/';
gap: var(--space-05);
}
/* The tagline appears only where there is room for it — see the 76rem block.
Measured: at 11px with 0.18em tracking the string is ~285px wide, and
restoring it under the name pushed the one-row header past its content box
by 18px at 1024 with six items and 84px with seven. The brand name carries
Measured: at 11px with 0.18em tracking the string is 283.1px wide. (Two
figures for 1024px were struck 2026-08-31: below 66rem `.nav` takes
`flex-basis: 100%` and `.header-cta` is `display: none`, so there is no
one-row header there and no CTA box to be past — neither number could be
re-derived. See the rule at the 66rem block below.) The brand name carries
the identity on its own; the tagline is a flourish, and `/` opens with the
same words as the hero eyebrow (docs/01). */
.brand-tagline {
display: none;
font-size: var(--text-2xs); /* 11px — the eyebrow floor in docs/02 */
/* HELD AT 11px, BELOW THE `.eyebrow` THIS ELEMENT CARRIES. Two reasons, both
measured 2026-08-31 at 13px. (1) `--header-h` is 81px and the header goes
to 83.4px — `global.css` drives `scroll-padding-top` from that token. (2)
With a seventh nav item the CTA lands past `.header-inner`'s content edge
by 42px at 1216, 18px at 1240 and 26px at 1280 and 1440 — it does not
resolve as the viewport widens. Insights is that item; `showInsights`
turns it on at two published articles. ⚠️ Document overflow is 0 in every
one of those cases: flex lets the CTA sit past the padding edge without
growing the document, so a page-level check cannot see this. */
font-size: var(--text-2xs);
white-space: nowrap;
}
.brand-name {
@@ -385,8 +396,9 @@ const isHome = path === '/';
.nav-list {
flex-wrap: nowrap;
flex: none;
/* 16px from 66rem, widening to 24px at 76rem where there is room for it.
Measured with seven items at every width from 1024px up. */
/* 16px from 66rem, widening to 24px at 80rem — see that block; the 76rem
block deliberately does NOT widen it. Measured with seven items at every
width from 1024px up. */
column-gap: var(--space-4);
}
.header-cta {