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 {
+1 -1
View File
@@ -77,7 +77,7 @@ const { preloadSerifItalic = false, ...seo } = Astro.props;
a fallback first, and a serif-to-fallback swap inside a 96px headline
moves the whole last line.
GEIST MONO IS DELIBERATELY NOT. It sets the eyebrow — 12px, uppercase,
GEIST MONO IS DELIBERATELY NOT. It sets the eyebrow — 13px, uppercase,
0.18em tracking — and the credential labels. A swap there costs one short
line of reflow at a size where the fallback is metrically close, and
preloading it would put 95,688 B of font on the critical path instead of
+2 -1
View File
@@ -906,7 +906,8 @@ const CREDENTIAL_GROUPS = [
not a heading and never carries the <h*>." `Eyebrow.astro` restates it.
The measurable consequence was worse than the rule breach: at 11px these
<h3>s were SMALLER than the 12px eyebrow above them and 5px smaller than
<h3>s were SMALLER than the eyebrow above them (12px at the time, 13px
since 2026-08-31) and 5px smaller than
the 16px list items they head, so "MEMBERSHIPS" was the least legible text
on the page an appointing body reads.
+16 -4
View File
@@ -37,10 +37,13 @@
* Neither is a fact a reader needs, and neither is verified.
* - **Automatic deletion IS stated, and it is the one promise on this page
* that depends on a control nobody has verified.** The handler writes the
* `ttl` attribute; TTL must also be ENABLED on the table, which §7 does not
* record. docs/05's definition of done carries "TTL set and verified by test
* record" and `docs/06`'s cutover checklist now names this page as what that
* item is protecting. It must be verified before this page is public.
* `ttl` attribute, and ⚠️ **writing the attribute is not the mechanism** —
* TTL must be ENABLED on the table, and **§7 records whether it is.**
* docs/05's definition of done carries "TTL set and verified by test
* record" and `docs/06`'s cutover checklist names this page as what that
* item is protecting. **Both halves — `ENABLED` by command, and a test
* record confirmed deleted — before this page is public.** See the
* TODO(pouya) on the retention section below, and §9 Q60.
*
* ⚠️ **NO LICENSURE CLAIM AND NO ANSWER TO THE CAPACITY QUESTION.** A privacy
* policy is where "legal advice" phrasing arrives by convention. §4 records
@@ -205,6 +208,15 @@ const COLLECTED = INTAKE_FIELDS.map((field) => field.label);
</p>
<h2>How long it is kept</h2>
{
/* TODO(pouya): is DynamoDB TTL `ENABLED` on the intake table, and has a
test record been written with a near-future `ttl` and observed to
disappear? AGENTS.md §9 Q60. The sentence below asserts a mechanism,
not just a period — `describe-time-to-live` returned `DISABLED` on
2026-08-31 — and it must not go public until both halves are
confirmed. §7 records the status; do not answer this from the
handler code, which only writes the attribute. */
}
<p>
<strong>{RETENTION_MONTHS} months from the date you send it</strong>,
after which the record is deleted automatically by the database rather
+4 -2
View File
@@ -200,15 +200,17 @@ h4 {
font-style: italic;
}
/* The class is the type treatment; `Eyebrow.astro` is the label component. A real
heading may carry the class — the footer's columns do — but an eyebrow above a
heading is never itself an <h*>, which is what the component enforces. */
.eyebrow {
font-family: var(--font-mono);
font-size: var(--text-xs);
font-size: var(--text-eyebrow);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-eyebrow);
text-transform: uppercase;
color: var(--text-meta);
}
/* An eyebrow is a label, never the page's heading element. */
.eyebrow .dot {
display: inline-block;
inline-size: 6px;
+8 -3
View File
@@ -64,10 +64,15 @@
/* Fluid scale, 360px → 1600px viewport. Ratio widens toward the display
end (1.25 → 1.333) so headlines scale harder than body copy.
--text-2xs is the eyebrow floor docs/02 sets at 11px. Added 2026-08-27:
SiteHeader wrote `0.6875rem` as a literal, step-1 review finding #7. */
--text-2xs: 0.6875rem; /* 11 — eyebrow */
--text-2xs is the 11px floor for a COMPRESSED label — the header tagline
and the print sheet. It stopped being the eyebrow floor on 2026-08-31.
Added 2026-08-27: SiteHeader wrote `0.6875rem` as a literal, step-1
review finding #7. */
--text-2xs: 0.6875rem; /* 11 — label */
--text-xs: 0.75rem; /* 12 — legal */
/* Not a rung on the ladder above: 13px sits between xs and sm because
uppercase mono at 0.18em reads smaller than it measures. Pouya, 2026-08-31. */
--text-eyebrow: 0.8125rem; /* 13 — eyebrow */
--text-sm: 0.875rem; /* 14 — meta */
--text-base: 1rem; /* 16 — body */
--text-lg: clamp(1.0625rem, 0.99rem + 0.32vw, 1.1875rem); /* 17→19 */