fix: purge AI slop from judgment essay, add voice linting pipeline

- Killed 16 em-dashes (Claude fingerprint, 15.9/1000 words -> 0)
- Replaced all clause-dash-elaboration patterns with periods, colons, restructuring
- Removed 'The One-Sentence Version' section (restated the intro, voice anti-pattern)
- Broke tricolon at lines 56-58 (too-clean parallel structure)
- Collapsed 'How to Actually Help' listicle into connective prose
- Added receipt link for ~100x inference cost claim
- Heading dashes replaced with colons (Tradesman Analogy, Excellence vs Functional)

New tooling:
- scripts/lint-voice.sh: mechanical anti-slop linter (em-dashes, trigger words,
  hedging, filler, receipts, sentence uniformity)
- .amplifier/skills/voice-check/SKILL.md: LLM-as-judge voice authenticity check
  (8 dimensions against VOICE.md profile)
- .amplifier/AGENTS.md: standing rule requiring both checks before any publish

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
Ken
2026-05-27 00:42:18 +00:00
parent 917c78a6b3
commit a951c00e35
13 changed files with 4755 additions and 30 deletions
+161
View File
@@ -0,0 +1,161 @@
# Aesthetic Guide: Crash Test Dev
**Created:** 2026-05-26
**Status:** Active
**Author aesthetic:** Ken Chau's personal technical publishing site
---
## User's Vision (Preserved)
**Raw input:**
> A well-used workshop. Not a pristine showroom — the one where someone actually builds things.
> Tools are organized because disorganization wastes time. The wood has warmth.
> The light is good because you need to see details. There's a coffee cup somewhere.
**Audience hierarchy:** Ken first (personal reference), his kids someday, peers second, other travelers last.
**The site is:** A thinking journal with publishing-grade craft. War stories about building software at scale — webpack performance battles, React localization strategies, architecture patterns. Names problems like enemies. Shows receipts.
---
## Aesthetic Direction
### 1. Visual Metaphor and Personality
The governing metaphor is **the workbench, not the gallery**. Think of a woodworker's shop where someone has spent years building real things: the surfaces show honest use, the light comes from where you need it, the tools hang in order because order serves the work. Nothing is decorative for its own sake. Everything earns its presence.
"Crash Test Dev" itself carries kinetic energy — impact testing, deliberate stress, the willingness to break things in controlled conditions and report what happened. The aesthetic should feel **steady, not flashy**. The confidence of someone who has already done the hard thing and is now writing it down clearly. No performance of expertise. Just the evidence and the story.
The personality is dry warmth. A person who cares deeply about craft but doesn't need you to know that — you figure it out from the precision of the writing and the quality of the page it sits on.
### 2. Color Philosophy
Warm, grounded, and deliberately un-digital. Where the Amplifier Masterclass palette lives in cool lavender-grays and steel blues, Crash Test Dev shifts the entire temperature toward **aged paper, warm stone, and natural wood**.
The base surface should feel like heavy unbleached stock — not pure white, not yellow, but the warm off-white of a quality notebook page. Text should be warm near-black, the brown-black of good ink rather than the blue-black of digital defaults. This warm-neutral foundation is the site's primary material.
Accents should come from **fired earth and aged metal** — a muted terracotta or rust for emphasis, a tarnished brass for subtle highlights. These are not bright; they're the colors of materials that have been used, heated, and cooled. Secondary tones draw from warm mid-grays with a stone or clay undertone, never blue-gray or cool silver.
Contrast should be high for readability but not harsh. The Itten cold-warm contrast principle applies here: warmth in both the lights and darks creates visual comfort even at strong contrast ratios. Aim for WCAG AAA on body text against the paper surface.
### 3. Typography Philosophy
Three voices, each with a clear job. This is the core elegance of the site.
**Serif for prose** — the reading voice. A serif body face signals "this is meant to be read at the pace of thought." It carries the authority of published writing without the stiffness of academic text. Source Serif 4 or a similar transitional serif: readable at 18-19px, with enough personality to feel human but enough regularity to disappear during sustained reading. Weight 400 for body, 600 for inline emphasis. The serif is the site's primary voice — it's how Ken sounds on the page.
**Sans-serif for structure** — the navigation voice. Headings, UI labels, metadata, timestamps, tags. The sans face organizes and directs without competing with the prose. Inter or a similar humanist sans: warm enough to belong on this page, precise enough to create clear hierarchy. Semi-bold (600) for headings creates contrast with the lighter serif body without shouting.
**Monospace for evidence** — the proof voice. Code blocks, terminal output, file paths, inline code. This is where Ken shows receipts. JetBrains Mono or equivalent: generous width, clear character differentiation, ligatures optional. The monospace voice should feel like a different register entirely — factual, verifiable, this-is-what-actually-happened.
The interplay between these three is the entire typographic identity. Prose flows in serif. Structure appears in sans. Proof arrives in mono. The reader always knows which voice is speaking.
### 4. Spatial Philosophy
The page breathes like a well-set book, with one key deviation: **code breaks the column**.
Prose lives in a reading column around 720px — roughly 65-70 characters per line at 18px serif, which is the sweet spot for sustained reading. Generous line-height (1.7-1.75) because these are dense technical narratives, not skimmable listicles. Paragraph spacing should be generous enough to let ideas land.
Code blocks break wider — approximately 840px — creating a visual rhythm of **narrow-wide-narrow** as the reader moves through a post. This breakout isn't decorative; it reflects the reality that code needs horizontal space and that code is a different kind of content. The width shift signals the register change from narrative to evidence.
Vertical rhythm follows the content's natural structure: generous space above H2s (a full breath between sections), tighter space between a heading and its first paragraph (they belong together), and enough air around code blocks that they feel like exhibited artifacts, not inline interruptions.
The audio player, when present, should sit in the prose column's width — it's a companion to the reading, not a separate interface. It belongs where you'd put a sticky note: near the top, unobtrusive, reachable.
### 5. Material Quality
Every surface should feel like it has physical weight and texture, without literally simulating materials.
The page background carries the faintest warmth — not a texture image, but a color warm enough to feel like paper under good light. Surfaces (cards, code blocks, callouts) use subtle warm-gray backgrounds that feel like slightly different weights of the same stock, not floating panels. Borders, when used, should feel like score lines or folds — thin, warm, understated.
Shadows are minimal and warm-toned. No dramatic elevation or layered depth systems. If something casts a shadow, it's the small, soft shadow of a card sitting on a desk — barely there, suggesting just enough physicality. Code blocks should feel slightly inset, like a different material embedded in the page — a subtle background shift and perhaps a left-edge accent line in the terracotta/rust accent.
Nothing gleams. Nothing floats. Nothing blurs. The material vocabulary is: **paper, ink, wood, stone, iron**. Flat but not sterile. Physical but not skeuomorphic.
### 6. What This Is NOT
**Not cold or clinical.** No blue-grays, no pure white backgrounds, no thin light-weight fonts creating an airy sterile feel. This site has body heat.
**Not dark-hacker aesthetic.** No dark backgrounds with neon syntax highlighting, no terminal-green-on-black theatrics. Ken's work happens in daylight.
**Not "personal brand."** No logo-heavy headers, no consistent color-accent-as-identity system, no "building in public" performance aesthetic. The craft IS the brand. The writing IS the identity.
**Not corporate.** No card grids, no hero banners, no CTAs, no engagement patterns. This is a library, not a landing page.
**Not minimalist-to-barren.** Minimalism here means nothing wasted, not nothing present. The page should feel furnished and warm, not empty and austere.
**Not trendy.** No glassmorphism, no wild gradients, no animated mesh backgrounds, no oversized display type for its own sake. Every effect used in 2024-2026 that will look dated by 2028 should be avoided. The reference point is: will this page look as good in five years? Ten?
---
## Emotional Direction
**Primary feeling:** Quiet confidence with warmth
**Personality adjectives:** Crafted, warm, clear, honest, unhurried
**What readers should feel:**
- First impression: "Someone built this with care"
- During reading: "I can focus here — the page serves the writing"
- After reading: "This person knows what they're talking about, and I want to come back"
---
## Design Tokens Direction (For design-system-architect)
The following are aesthetic intentions, not final values. The design-system-architect should translate these into concrete CSS custom properties.
### Color Intent
- **Surface base:** Warm off-white, like heavy unbleached paper stock
- **Text primary:** Warm near-black, brown-black ink
- **Text secondary:** Warm mid-gray, stone undertone
- **Accent primary:** Muted terracotta/rust — fired earth, not bright red
- **Accent secondary:** Tarnished brass/dark gold — aged metal highlight
- **Code surface:** Slightly cooler warm-gray, like a different paper weight
- **Border/divider:** Warm light gray, barely there, like a score line
### Typography Intent
- **Prose (serif):** Source Serif 4 or equivalent transitional serif, 18-19px base, weight 400, line-height 1.7-1.75
- **Structure (sans):** Inter or equivalent humanist sans, weight 600 for headings, weight 400-500 for UI
- **Evidence (mono):** JetBrains Mono or equivalent, slightly smaller than prose (~0.9em)
- **Type scale:** ~1.25 ratio (Major Third), conservative — hierarchy through weight and family contrast, not dramatic size jumps
### Spatial Intent
- **Prose column:** ~720px max-width (~65ch at 18px serif)
- **Code breakout:** ~840px max-width
- **Paragraph spacing:** Generous (1.5em+)
- **Section spacing:** Very generous above H2 (3-4rem), tighter below (1-1.5rem)
- **Code block spacing:** Generous vertical margin (2-2.5rem), slight inset feel
### Shadow/Depth Intent
- **Minimal.** Warm-toned, soft, barely-there desk shadows only
- **Code blocks:** Subtle inset feel via background shift + left accent line
- **No elevation system.** Nothing floats.
### Corner Treatment
- **Subtle softness:** 4-6px radius on cards/code blocks — just enough to not feel sharp, not enough to feel "designed"
- **Rationale:** Workshop surfaces have worn edges, not machined radii
---
## Multi-Format Considerations
The warm palette and serif/sans pairing should translate to:
- **Podcast cover art:** Terracotta accent + warm near-black + the "Crash Test Dev" name in sans-serif
- **YouTube thumbnails:** Sans-serif headings at display weight, warm paper background, high contrast
- **Presentation slides:** Same color philosophy, sans-serif for slide content, serif for pull-quotes
The visual identity is the PALETTE + TYPOGRAPHY PAIRING, not a logo or mark.
---
## Evolution
**Last updated:** 2026-05-26
**Changes:** Initial creation from Ken's brief
This guide evolves as the site grows. Update when:
- Ken provides aesthetic feedback after seeing implementations
- New content types emerge (podcast, video, slides)
- Dark mode is eventually considered
- The site's audience shifts or expands
+598
View File
@@ -0,0 +1,598 @@
---
feature: CoreComponentSpecs
date: 2026-05-26
status: planned
project: crash-test-dev
tags: [components, article-card, code-block, audio-player, callout, post-header, nav-bar]
related: [AESTHETIC-GUIDE.md, design-tokens-2026-05-26.md]
---
# Component Specifications — Crash Test Dev
Six components designed against the established token system. Every value traces to a named token. Every choice traces to the aesthetic guide.
**Governing principle:** These components are surfaces on the workbench, not floating UI panels. They use the same material vocabulary as the page — paper weight differences, score lines, inset materials, ink — never elevation, glow, or blur.
---
## 1. article-card
**Purpose:** An index card laid on the desk. Lists a post, pattern, or lesson with enough context to decide whether to pick it up and read.
### Layout
```
┌─────────────────────────────────────────────────────────┐
│ [PATTERN] 12 min read │ ← badge + meta row
│ │
│ Webpack Tree-Shaking Is Lying to You │ ← title
│ │
│ That bundle analyzer showing 100% tree-shaken? │ ← summary
│ The runtime tells a different story. Here's how to │
│ catch the lie and fix it. │
│ │
│ May 14, 2026 │ ← date
└─────────────────────────────────────────────────────────┘
```
### Anatomy
The card is a `<article>` element containing an `<a>` that wraps the entire card (the card IS the link). Internal structure is a vertical stack.
- **Container:** `max-width: var(--width-prose)` (720px). Full-bleed within the prose column.
- **Background:** `var(--surface-card)` — white card stock laid on the paper base.
- **Border:** `1px solid var(--border-subtle)` — a score line, not a frame.
- **Border radius:** `var(--rounded-lg)` (6px) — worn edges.
- **Shadow:** `var(--shadow-sm)` — barely perceptible. Card sitting on desk, not hovering above it.
- **Padding:** `var(--space-3)` (24px) all sides.
### Typography & Content
| Element | Font | Size | Weight | Color | Line-height |
|---|---|---|---|---|---|
| Content type badge | `var(--font-structure)` | `var(--type-xs)` (12px) | 600 (semibold) | `var(--accent-primary)` | `var(--leading-normal)` (1.5) |
| Reading time | `var(--font-structure)` | `var(--type-xs)` (12px) | 400 | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) |
| Title | `var(--font-structure)` | `var(--type-xl)` (28px) | 600 (semibold) | `var(--text-primary)` | `var(--leading-snug)` (1.25) |
| Summary | `var(--font-prose)` | `var(--type-base)` (18px) | 400 | `var(--text-secondary)` | `var(--leading-prose)` (1.7) |
| Date | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) |
### Spacing (Internal)
| Between | Value | Token |
|---|---|---|
| Badge row → Title | `var(--space-1.5)` | 12px |
| Title → Summary | `var(--space-1)` | 8px |
| Summary → Date | `var(--space-2)` | 16px |
| Between cards (vertical stack) | `var(--space-2)` | 16px |
**Badge row:** Flexbox, `justify-content: space-between`, `align-items: center`. Badge left, reading time right.
**Badge styling:** Text only — uppercase via `text-transform: uppercase`, `letter-spacing: 0.05em`. No background pill, no border. The terracotta color IS the badge. Consistent with "nothing is decorative for its own sake."
### States
| State | Change | Tokens |
|---|---|---|
| **Default** | As described above | — |
| **Hover** | Shadow deepens; title color shifts to accent | `var(--shadow-md)`, title → `var(--accent-primary)` |
| **Active (mousedown)** | Shadow returns to sm; subtle inward shift | `var(--shadow-sm)`, `transform: translateY(1px)` |
| **Focus-visible** | 2px outline in terracotta, 2px offset | `outline: 2px solid var(--focus-ring)`, `outline-offset: 2px` |
**Transition:** `transition: box-shadow var(--duration-fast) var(--ease-default), transform var(--duration-fast) var(--ease-default)`. Title color transition: `var(--duration-fast)`.
### Accessibility
- Entire card is a single `<a>` wrapping a `<article>`. Screen readers announce the title as the link text.
- Badge text uses `aria-label` on the article to provide context: e.g., `aria-label="Pattern: Webpack Tree-Shaking Is Lying to You"`.
- Focus ring visible only on keyboard navigation (`:focus-visible`).
- Touch target: entire card surface (well above 44×44px minimum).
### Why this design
A card sitting on a desk has contact shadow, not drop shadow — `shadow-sm` at rest, `shadow-md` on hover is the full depth vocabulary of this system, and the card uses both ends of it because it's the most interactive surface on the index page.
---
## 2. code-block
**Purpose:** The evidence container. A different material embedded in the page — heavier paper, slightly recessed, with a terracotta accent marking where proof begins.
### Layout
```
┌─ filename/language label
│▌ src/bundler/tree-shake.ts │
│▌ │
│▌ export function analyzeExports(module: Module) { │
│▌ const used = new Set<string>(); │
│▌ for (const ref of module.references) { │
│▌ if (ref.type === 'import') { │
│▌ used.add(ref.name); │
│▌ } │
│▌ } │
│▌ return module.exports.filter(e => !used.has(e.name)); │
│▌ } │
│▌ │
│ │
└── 3px terracotta accent line
```
### Anatomy
The code block is a `<figure>` containing an optional `<figcaption>` (filename/language) and a `<pre><code>` block. It breaks wider than prose.
- **Container:** `max-width: var(--width-code)` (840px). Centered. When the viewport is narrower, it respects `var(--gutter)` (24px) side padding.
- **Background:** `var(--surface-code)` — heavier paper weight. The material shift IS the signal.
- **Border:** `1px solid var(--border-subtle)` top, right, and bottom. **Left border:** `3px solid var(--accent-primary)` — the terracotta accent line.
- **Border radius:** `var(--rounded-md)` (4px).
- **Shadow:** `none` — code blocks are inset, not elevated. They sit *in* the desk, not *on* it.
- **Padding:** `var(--space-3)` (24px) all sides. Left padding accounts for the 3px accent (so `calc(var(--space-3) - 3px)` on the left, or simply treat the border as outside the padding box).
- **Vertical margin:** `var(--space-5)` (40px) above and below — exhibited artifact spacing.
- **Overflow:** `overflow-x: auto` for horizontal scroll on long lines.
### Typography
| Element | Font | Size | Weight | Color | Line-height |
|---|---|---|---|---|---|
| Code text | `var(--font-evidence)` | 1rem (16px) | 400 | `var(--code-text)` | `var(--leading-relaxed)` (1.6) |
| Filename/language label | `var(--font-structure)` | `var(--type-xs)` (12px) | 500 (medium) | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) |
**Filename label:** Right-aligned within the code block, sitting at the top. Padding-bottom `var(--space-1)` (8px) separating it from code content. If no filename, no label — the code speaks for itself.
### States
| State | Change | Tokens |
|---|---|---|
| **Default** | As described above | — |
| **Horizontal overflow** | Subtle fade-out on the right edge (CSS mask gradient) to indicate scrollable content. Scrollbar styled warm. | — |
| **Focus (keyboard tab into scrollable region)** | `outline: 2px solid var(--focus-ring)`, `outline-offset: -2px` (inset, since the block is inset) | `var(--focus-ring)` |
**Scrollbar styling (where supported):**
- Track: `var(--surface-code)` (invisible against background)
- Thumb: `var(--border-default)` — warm, quiet
- Thumb hover: `var(--text-tertiary)` — slightly more visible
- Width: 6px, border-radius: `var(--rounded-full)`
### Syntax Highlighting Palette Direction
The syntax theme should stay within the warm vocabulary. No neon, no saturated primaries.
| Token type | Color direction | Notes |
|---|---|---|
| Keywords | `var(--accent-primary)` (terracotta) | Control flow, declarations |
| Strings | `var(--accent-secondary)` (tarnished brass) | Literal values |
| Comments | `var(--text-tertiary)` | Quiet, receding |
| Functions/methods | `var(--text-primary)` | Bold weight (600) for emphasis, not color |
| Types/classes | `var(--accent-primary)` at 80% opacity | Slightly softer than keywords |
| Numbers | `var(--accent-secondary)` | Groups with strings as "literal values" |
| Punctuation | `var(--text-secondary)` | Present but not prominent |
### Accessibility
- `<pre>` has `tabindex="0"` when content overflows horizontally, making it keyboard-scrollable.
- `role="region"` with `aria-label` describing the code (e.g., `aria-label="Code: src/bundler/tree-shake.ts"`).
- Filename in `<figcaption>` is programmatically associated via `aria-labelledby`.
- Syntax highlighting uses color + context (not color alone) — keywords are identifiable by position, not just hue.
- All syntax colors maintain minimum 4.5:1 contrast on `surface-code`.
### Why this design
The narrow→wide→narrow rhythm as you scroll through a post mirrors the register shift between narrative and evidence — your eye physically moves to a wider surface when the voice changes from serif prose to mono proof, then returns to the narrower reading column.
---
## 3. audio-player
**Purpose:** A companion to reading, not a separate interface. Like a sticky note near the top of the page: "You can also listen to this."
### Layout — Collapsed (Default)
```
┌──────────────────────────┐
│ ▶ Listen · 8 min │
└──────────────────────────┘
```
A pill. Inline with prose flow, sitting at the top of the article just below the post-header.
- **Container:** Inline-flex pill. `border-radius: var(--rounded-full)` (9999px).
- **Background:** `var(--surface-inset)` — slightly recessed, like a different stock weight.
- **Border:** `1px solid var(--border-subtle)`.
- **Padding:** `var(--space-0.5)` (4px) vertical, `var(--space-2)` (16px) horizontal.
- **Shadow:** `none` — it's inset, resting.
- **Height:** Determined by content. Approximately 36px (above 44px touch target when accounting for the click area — see accessibility note).
| Element | Font | Size | Weight | Color |
|---|---|---|---|---|
| Play icon (▶) | — | 14px (optical) | — | `var(--accent-primary)` |
| "Listen" label | `var(--font-structure)` | `var(--type-sm)` (14px) | 500 (medium) | `var(--text-secondary)` |
| Duration | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-tertiary)` |
**Internal spacing:** `var(--space-1)` (8px) between icon and "Listen", `var(--space-0.5)` (4px) between "Listen" and `·` separator, same before duration.
### Layout — Expanded (Playing/Paused)
```
┌─────────────────────────────────────────────────────────┐
│ ❚❚ ━━━━━━━━━━━━━━━━━━━━━━━━━●━━━━━━━ 2:14 / 8:02 │
│ ▲ progress bar 1× │
└─────────────────────────────────────────────────────────┘
```
Expands to `max-width: var(--width-prose)` (720px). Same pill shape gives way to a rounded-rectangle.
- **Container:** `max-width: var(--width-prose)`. `border-radius: var(--rounded-lg)` (6px).
- **Background:** `var(--surface-inset)`.
- **Border:** `1px solid var(--border-subtle)`.
- **Padding:** `var(--space-1.5)` (12px) vertical, `var(--space-2)` (16px) horizontal.
- **Layout:** Two rows.
- **Row 1:** Play/pause button | Progress bar | Time display. Flexbox, `align-items: center`.
- **Row 2:** Right-aligned speed control. Only visible in expanded state.
| Element | Font | Size | Weight | Color |
|---|---|---|---|---|
| Play/pause icon | — | 18px (optical) | — | `var(--accent-primary)` |
| Time (current / total) | `var(--font-evidence)` | `var(--type-xs)` (12px) | 400 | `var(--text-tertiary)` |
| Speed button ("1×") | `var(--font-evidence)` | `var(--type-xs)` (12px) | 400 | `var(--text-secondary)` |
**Progress bar:**
- Track: `var(--border-subtle)`, height 3px, `border-radius: var(--rounded-full)`.
- Fill (elapsed): `var(--accent-primary)` (terracotta).
- Scrubber knob: 12px circle, `var(--accent-primary)`, `border: 2px solid var(--surface-inset)`. Only visible on hover/focus.
- Track clickable height: 24px (transparent hit area above 44px minimum on mobile with padding).
### Spacing
| Between | Value | Token |
|---|---|---|
| Play/pause → progress bar | `var(--space-1.5)` | 12px |
| Progress bar → time | `var(--space-1.5)` | 12px |
| Row 1 → Row 2 | `var(--space-0.5)` | 4px |
| Player → first prose paragraph | `var(--space-4)` | 32px |
### States
| State | Change |
|---|---|
| **Collapsed (default)** | Pill form as described. Click anywhere to expand and play. |
| **Collapsed hover** | Background shifts to `var(--surface-code)` — slightly warmer. Cursor pointer. |
| **Expanded — playing** | Pause icon shown. Progress bar animates. Time updates. |
| **Expanded — paused** | Play icon shown. Progress bar static. |
| **Speed button hover** | Color shifts to `var(--accent-primary)`. |
| **Speed button active** | Cycles: 1× → 1.25× → 1.5× → 2× → 1×. |
| **Focus-visible (any interactive element)** | `outline: 2px solid var(--focus-ring)`, `outline-offset: 2px`. |
| **Loading** | Collapsed pill shows "Loading…" in place of duration. Subtle opacity pulse on the label (opacity 0.6→1.0, `var(--duration-slow)` cycle). |
| **Error** | Pill shows "Unavailable" in `var(--text-tertiary)`. Play icon replaced with a dash. Not clickable. |
**Expand/collapse transition:** Height and border-radius animate over `var(--duration-slow)` (250ms) with `var(--ease-in-out)`. This is the one layout animation in the system — justified because the shape change is meaningful (compact → full controls).
### Accessibility
- Play/pause is a `<button>` with `aria-label="Play audio version"` / `aria-label="Pause audio"`.
- Progress bar is `<input type="range">` with `aria-label="Audio progress"`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext="2 minutes 14 seconds of 8 minutes 2 seconds"`.
- Speed button is a `<button>` with `aria-label="Playback speed: 1x"`.
- Collapsed pill is a `<button>` with `aria-label="Listen to audio version, 8 minutes"`.
- Minimum touch target: 44×44px on all interactive elements (play/pause button padded to meet this; progress bar hit area expanded).
### Why this design
The player starts as a pill because it's an offer, not a demand. It earns its space only when activated. The inset surface and quiet typography say "I'm part of this page" — integrated like a tool hanging on the pegboard, not bolted on like an afterthought.
---
## 4. callout
**Purpose:** A sticky note on the workbench. Interrupts prose flow to surface a pattern, anti-pattern, warning, or note — then returns you to reading.
### Layout
```
┌────────────────────────────────────────────────────────┐
▐ PATTERN │
▐ │
▐ The narrow-wide-narrow rhythm isn't just aesthetic — │
▐ it signals a register change. When the reader's eye │
▐ hits wider content, they unconsciously shift into │
▐ "evidence evaluation" mode. │
└────────────────────────────────────────────────────────┘
```
### Anatomy
An `<aside>` element within the prose column. Left-border accent with inset surface.
- **Container:** `max-width: var(--width-prose)` (720px). Full-bleed within prose column.
- **Background:** `var(--surface-inset)` — recessed surface, different stock weight.
- **Border:** `1px solid var(--border-subtle)` top, right, bottom. **Left border:** `3px solid` — color varies by type (see below).
- **Border radius:** `var(--rounded-lg)` (6px).
- **Padding:** `var(--space-3)` (24px) all sides.
- **Vertical margin:** `var(--space-4)` (32px) above and below.
- **Shadow:** `none` — it's inset, like the code block.
### Callout Types & Left Border Colors
| Type | Left border color | Label color | Rationale |
|---|---|---|---|
| **Note** (default) | `var(--border-default)` | `var(--text-secondary)` | Neutral — just a sidebar thought. |
| **Pattern** | `var(--accent-primary)` (terracotta) | `var(--accent-primary)` | Terracotta = emphasis, the primary accent. A pattern is something to adopt. |
| **Anti-pattern** | `var(--error-border)` | `var(--error-text)` | Clay-red signals caution without alarm. Something to avoid. |
| **Warning** | `var(--warning-border)` | `var(--warning-text)` | Amber/brass for "proceed carefully." |
| **Tip** | `var(--accent-secondary)` (tarnished brass) | `var(--accent-secondary)` | Brass = secondary highlight. A useful but optional insight. |
### Typography
| Element | Font | Size | Weight | Color | Line-height |
|---|---|---|---|---|---|
| Label (e.g., "PATTERN") | `var(--font-structure)` | `var(--type-xs)` (12px) | 600 (semibold) | Varies by type (see above) | `var(--leading-normal)` (1.5) |
| Body text | `var(--font-prose)` | `var(--type-base)` (18px) | 400 | `var(--text-primary)` | `var(--leading-prose)` (1.7) |
**Label styling:** `text-transform: uppercase`, `letter-spacing: 0.05em`. Same treatment as the article-card badge — the system has one way to do labels.
### Spacing (Internal)
| Between | Value | Token |
|---|---|---|
| Label → body text | `var(--space-1)` | 8px |
| Between paragraphs in body | `var(--space-3)` | 24px (standard paragraph gap) |
### States
Callouts are static content — no interactive states. They are read, not clicked.
If the callout contains links, those links follow standard prose link styling: `var(--accent-primary)` text, underline, hover → `var(--accent-primary-hover)`.
### Accessibility
- `<aside>` element with `role="note"` (or `role="complementary"` if substantial).
- `aria-label` includes the callout type: e.g., `aria-label="Pattern callout"`.
- Label text is decorative/redundant with aria-label — can be `aria-hidden="true"` to avoid double-announcement.
- Body text contrast: `var(--text-primary)` on `var(--surface-inset)` = 11.93:1 (AAA). No issues.
- Left border color is not the sole information carrier — the label text also identifies the callout type.
### Why this design
The left-border accent mirrors the code block's terracotta edge — creating a consistent visual grammar for "pay attention, this is a different voice." The inset surface says "this is set into the page" while the border color says "here's what kind of aside this is." Two signals, zero decoration.
---
## 5. post-header
**Purpose:** Establishes the article's identity. The writing is the hero — the header frames it with just enough structure to orient the reader, then gets out of the way.
### Layout
```
POST
Webpack Tree-Shaking Is Lying to You
May 14, 2026 · 12 min read
┌──────────────────────────┐
│ ▶ Listen · 8 min │
└──────────────────────────┘
```
Centered within the prose column. Vertical stack, center-aligned.
### Anatomy
A `<header>` element at the top of the article page. All content centered.
- **Container:** `max-width: var(--width-prose)` (720px). Center-aligned.
- **Background:** Transparent — it lives on `var(--surface-base)`, the page itself.
- **No border, no shadow.** The header is typography, not a container.
- **Top padding:** `var(--space-8)` (64px) from the nav-bar. Breathing room.
- **Bottom margin:** `var(--space-8)` (64px) before the first prose paragraph. A chapter-break-scale pause.
### Typography
| Element | Font | Size | Weight | Color | Line-height | Alignment |
|---|---|---|---|---|---|---|
| Content type badge | `var(--font-structure)` | `var(--type-xs)` (12px) | 600 (semibold) | `var(--accent-primary)` | `var(--leading-normal)` (1.5) | Center |
| Title (h1) | `var(--font-structure)` | `var(--type-3xl)` (44px) | 700 (bold) | `var(--text-primary)` | `var(--leading-tight)` (1.15) | Center |
| Meta line (date + reading time) | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) | Center |
**Title:** `letter-spacing: -0.02em` — negative tracking at display size, per the type system spec. Max 3 lines before it feels wrong — titles should be concise.
**Badge:** Same `text-transform: uppercase`, `letter-spacing: 0.05em` treatment as article-card and callout. System-wide label pattern.
**Meta line:** Date and reading time separated by ` · ` (space-middot-space). The middot is `var(--text-tertiary)`. Both in the structure voice at caption weight.
### Spacing (Internal)
| Between | Value | Token |
|---|---|---|
| Badge → Title | `var(--space-2)` | 16px |
| Title → Meta line | `var(--space-2)` | 16px |
| Meta line → Audio pill (if present) | `var(--space-3)` | 24px |
### States
The post-header is static. No interactive states on the header itself.
- The **badge** is not a link (unlike on index pages — here you're already on the post).
- The **audio pill** has its own interactive states (see audio-player component).
- The **date** could optionally link to an archive, using standard link treatment: `var(--accent-primary)`, underline on hover.
### Responsive Behavior
| Viewport | Title size | Top padding | Bottom margin |
|---|---|---|---|
| ≥ 720px | `var(--type-3xl)` (44px) | `var(--space-8)` (64px) | `var(--space-8)` (64px) |
| < 720px | `var(--type-2xl)` (36px) | `var(--space-6)` (48px) | `var(--space-6)` (48px) |
| < 480px | `var(--type-xl)` (28px) | `var(--space-5)` (40px) | `var(--space-5)` (40px) |
The title steps down the type scale at narrow viewports. Line-height adjusts proportionally: 1.15 → 1.25 → 1.25. Everything else stays the same — the hierarchy is preserved, just the title scale adapts.
### Accessibility
- `<header>` with `role="banner"` scoped to the article (not the page — the nav has page-level banner).
- Title is `<h1>` — the only h1 on the page.
- Badge is `<span>` (not heading). Associated with the h1 via proximity, not ARIA — screen readers will encounter it naturally before the title.
- Meta content uses `<time datetime="2026-05-14">` for machine-readable dates.
### Why this design
No hero image, no gradient, no illustration. The h1 at 44px Inter Bold IS the visual weight of the page. The centered stack with generous vertical breathing creates the feeling of opening a well-typeset book to a chapter page — the title, the metadata, then silence before the prose begins.
---
## 6. nav-bar
**Purpose:** Present but not demanding. A quiet shelf at the top of the workshop wall — you know where the tools are, but they don't shout at you while you're reading.
### Layout
```
Crash Test Dev Posts Patterns Lessons
──────────────────────────────────────────────────────────────
```
A single row. Site name left, content type links right. Bottom border separates nav from page content.
### Anatomy
A `<nav>` element containing the site name and navigation links.
- **Container:** `max-width: var(--width-container)` (1100px). Centered. Horizontal padding: `var(--gutter)` (24px).
- **Background:** `var(--surface-base)` — the same as the page. The nav doesn't have its own surface. It's part of the desk.
- **Border bottom:** `1px solid var(--border-subtle)` — a score line marking where navigation ends and content begins.
- **Height:** Determined by content + padding. Approximately 56px.
- **Padding:** `var(--space-2)` (16px) vertical.
- **Position:** `position: static`. The nav scrolls with the page. On a reading site, you don't need persistent navigation — you need an uninterrupted reading column. (If later analysis shows readers frequently nav-jump, this could become `sticky` with `top: 0` and `background: var(--surface-base)` — but start static.)
### Typography
| Element | Font | Size | Weight | Color | Letter-spacing |
|---|---|---|---|---|---|
| Site name | `var(--font-structure)` | `var(--type-base)` (18px) | 600 (semibold) | `var(--text-primary)` | `-0.01em` |
| Nav links | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-secondary)` | `0` |
| Active nav link | `var(--font-structure)` | `var(--type-sm)` (14px) | 500 (medium) | `var(--text-primary)` | `0` |
**Site name:** Not a logo, not display type. 18px Inter Semibold — the same size as body text but in the structure voice. Confident without being loud. It's a nameplate on the workbench, not a sign above the shop.
### Spacing
| Between | Value | Token |
|---|---|---|
| Site name ↔ nav links | Flexbox `justify-content: space-between` | — |
| Between nav links | `var(--space-4)` (32px) | Enough air to feel unhurried |
| Nav bottom border → page content | `var(--space-6)` (48px) on index pages, handled by page layout | — |
### States
| State | Element | Change | Tokens |
|---|---|---|---|
| **Default** | Nav links | `var(--text-secondary)`, no underline | — |
| **Hover** | Nav links | Color → `var(--text-primary)` | `transition: color var(--duration-fast)` |
| **Active (current page)** | Nav link | Color → `var(--text-primary)`, weight → 500 (medium). Underline: `2px solid var(--accent-primary)`, offset `6px` below text | `text-underline-offset: 6px` |
| **Focus-visible** | Any link | `outline: 2px solid var(--focus-ring)`, `outline-offset: 2px` | `var(--focus-ring)` |
| **Site name hover** | Site name | Color → `var(--accent-primary)` | `transition: color var(--duration-fast)` |
**Active link underline:** A 2px terracotta line sitting 6px below the text baseline. This is the one place in the nav where accent color appears — a quiet "you are here" without a highlight pill, active tab, or any other noisy indicator.
### Responsive Behavior
| Viewport | Layout | Changes |
|---|---|---|
| ≥ 600px | Row: name left, links right | As described |
| < 600px | Stack: name centered on top, links centered below | Name and links each center-aligned. Gap between: `var(--space-1)` (8px). Links in a single row, gap reduced to `var(--space-3)` (24px). |
No hamburger menu. Three links don't need one. A hamburger would be over-engineering — the anti-pattern of hiding three items behind a tap. If the site grows to need one, revisit then.
### Accessibility
- `<nav>` with `aria-label="Site navigation"`.
- Current page link has `aria-current="page"`.
- Site name is an `<a href="/">` — always links home.
- All links have minimum touch target height of 44px (achieved via padding on the nav container and line-height on links).
- Skip-to-content link: A visually hidden `<a href="#main-content">Skip to content</a>` as the first child of `<body>`, appearing on focus. Styled: `var(--surface-card)` background, `var(--text-primary)` text, `var(--space-1)` padding, absolutely positioned at top-left on focus.
### Why this design
The nav earns its restraint from the aesthetic guide's "not personal brand" directive. No logo, no color accent in the name, no visual weight competing with the prose below. The score line at the bottom is the thinnest possible assertion: "navigation is above this line, content is below." Three words, three links, one line. Done.
---
## Cross-Component Patterns
### The Label System
Three components share an identical label treatment — article-card badge, callout label, and post-header badge:
```css
.label {
font-family: var(--font-structure);
font-size: var(--type-xs); /* 12px */
font-weight: 600; /* semibold */
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: var(--leading-normal); /* 1.5 */
/* Color varies by context */
}
```
This is intentional. One system for labeling content types. Recognize it once, understand it everywhere.
### The Inset Surface Grammar
Three components use the "different material" pattern — code-block, callout, and audio-player:
| Component | Surface | Left accent | Meaning |
|---|---|---|---|
| code-block | `var(--surface-code)` | `3px var(--accent-primary)` | "This is evidence" |
| callout | `var(--surface-inset)` | `3px` varies by type | "This is an aside" |
| audio-player | `var(--surface-inset)` | none | "This is a tool" |
All three are inset (no shadow), all three use a background shift to signal "different material," and the code-block and callout share the left-accent pattern. The audio player omits the accent because it's interactive, not annotative.
### The Depth Vocabulary (Complete)
The entire system uses exactly these depth levels:
| Level | Shadow | Surface | Used by |
|---|---|---|---|
| **Page** | none | `var(--surface-base)` | Page background, nav-bar, post-header |
| **Card** | `var(--shadow-sm)` | `var(--surface-card)` | article-card |
| **Card (hover)** | `var(--shadow-md)` | `var(--surface-card)` | article-card on hover |
| **Inset** | none | `var(--surface-inset)` | callout, audio-player |
| **Inset (code)** | none | `var(--surface-code)` | code-block |
Nothing floats. The maximum depth in the system is `shadow-md` on a hovered card. That's it.
### Motion Budget
| Transition | Duration | Easing | Property |
|---|---|---|---|
| Link/button color changes | `var(--duration-fast)` (100ms) | `var(--ease-default)` | `color` |
| Card shadow on hover | `var(--duration-fast)` (100ms) | `var(--ease-default)` | `box-shadow` |
| Card press | `var(--duration-fast)` (100ms) | `var(--ease-default)` | `transform` |
| Audio player expand/collapse | `var(--duration-slow)` (250ms) | `var(--ease-in-out)` | `height`, `border-radius` |
| Audio progress bar scrubber appear | `var(--duration-normal)` (150ms) | `var(--ease-default)` | `opacity` |
All set to `0ms` under `prefers-reduced-motion: reduce`. Non-negotiable.
---
## Implementation Priority
Recommended build order based on dependencies and page coverage:
1. **nav-bar** — present on every page, frames everything else
2. **post-header** — top of every article, depends on the label system
3. **code-block** — the evidence container, used in every technical post
4. **callout** — aside blocks within prose, shares grammar with code-block
5. **article-card** — index pages, the only elevated surface in the system
6. **audio-player** — optional per-post feature, most complex interactive states
---
## Success Criteria
- **Token traceability:** Every color, size, space, radius, shadow, and duration in the implemented CSS traces to a named custom property. Zero magic numbers.
- **Aesthetic coherence:** A page using all six components should feel like one material surface with variations in weight and purpose — not six different "components."
- **Quiet confidence:** If someone inspects the page and thinks "there's not much design here," the design succeeded. The craft is in what's NOT there.
- **Five-year test:** Nothing in these specs will look dated in 2031. Paper, ink, and careful spacing don't go out of style.
+584
View File
@@ -0,0 +1,584 @@
---
feature: DesignTokenSystem
date: 2026-05-26
status: planned
project: crash-test-dev
tags: [tokens, system, color, typography, spacing, layout]
related: [AESTHETIC-GUIDE.md]
---
# Design Token System — Crash Test Dev
## User's Spark (preserved)
> A well-used workshop. Not a pristine showroom — the one where someone actually builds things.
> Tools are organized because disorganization wastes time. The wood has warmth.
> The light is good because you need to see details. There's a coffee cup somewhere.
**Metaphor:** The workbench, not the gallery.
**Materials vocabulary:** Paper, ink, wood, stone, iron.
**Personality:** Dry warmth. Quiet confidence. Evidence over performance.
---
## Complete Token System
```json
{
"colors": {
"_meta": {
"temperature": "warm across all values — no blue-grays, no cool neutrals",
"philosophy": "Aged paper, brown-black ink, fired earth, tarnished brass. Every color drawn from the workbench vocabulary: materials that have been used, heated, and cooled."
},
"surface": {
"base": { "value": "#F5F0E8", "description": "Page background. Heavy unbleached paper stock — warm off-white, not pure white, not yellow." },
"card": { "value": "#FEFCF7", "description": "Reading surface. White card stock laid on the paper base — for article bodies, elevated panels." },
"code": { "value": "#EEEAE2", "description": "Code block background. Heavier paper weight — subtle warm-gray shift from prose background." },
"inset": { "value": "#E8E3DA", "description": "Callout/aside background. Slightly recessed surface, like a different stock weight." }
},
"text": {
"primary": { "value": "#2B2421", "description": "Body text. Warm near-black — brown-black ink, NOT blue-black digital default.", "contrastOnBase": "13.44:1 (AAA)", "contrastOnCard": "14.87:1 (AAA)" },
"secondary": { "value": "#6B5F55", "description": "Metadata, captions, bylines. Warm mid-gray with stone undertone.", "contrastOnBase": "5.46:1 (AA)", "contrastOnCard": "6.04:1 (AA)" },
"tertiary": { "value": "#948880", "description": "Timestamps, tags, disabled text. Lighter warm gray.", "contrastOnBase": "3.04:1 (AA-UI)", "contrastOnCard": "3.36:1 (AA-UI)" }
},
"accent": {
"primary": { "value": "#A0522D", "description": "Links, emphasis, code block left-edge accent. Muted terracotta/sienna — fired earth, not bright red.", "contrastOnBase": "4.95:1 (AA)", "contrastOnCard": "5.48:1 (AA)" },
"primaryHover": { "value": "#8B4726", "description": "Link hover state. Darker rust — the terracotta after more firing.", "contrastOnBase": "6.12:1 (AA)" },
"secondary": { "value": "#7D6C2F", "description": "Special callouts, highlights. Tarnished brass/dark gold — aged metal.", "contrastOnBase": "4.56:1 (AA)", "contrastOnCard": "5.05:1 (AA)" },
"secondaryHover": { "value": "#6B5C28", "description": "Brass hover state. Darker, more patinated." }
},
"code": {
"text": { "value": "#3D342E", "description": "Code text. Warm dark brown, slightly lighter than body text for the different register.", "contrastOnCode": "10.12:1 (AAA)" }
},
"border": {
"subtle": { "value": "#DDD7CD", "description": "Decorative score lines, faint dividers. Barely there — like a fold in paper. No contrast requirement (decorative)." },
"default": { "value": "#C2BAA9", "description": "General-purpose separators, card edges. Visible but quiet." },
"strong": { "value": "#8E857A", "description": "Functional UI borders (form inputs, focused elements). Meets 3:1 on both base and card.", "contrastOnBase": "3.20:1 (AA-UI)", "contrastOnCard": "3.54:1 (AA-UI)" }
},
"focus": {
"ring": { "value": "#A0522D", "description": "Keyboard focus indicator. Terracotta — visible, warm, consistent with accent.", "contrastOnBase": "4.95:1 (AA-UI)" }
},
"state": {
"success": {
"bg": { "value": "#E8EDDF", "description": "Warm sage green tint." },
"text": { "value": "#4A5E3A", "description": "Dark moss green.", "contrastOnBg": "5.96:1 (AA)" },
"border": { "value": "#B5C4A0", "description": "Muted sage border." }
},
"warning": {
"bg": { "value": "#F2E8D0", "description": "Warm amber tint." },
"text": { "value": "#7A5C1F", "description": "Dark warm amber.", "contrastOnBg": "5.10:1 (AA)" },
"border": { "value": "#D4BC7C", "description": "Muted gold border." }
},
"error": {
"bg": { "value": "#F2DED6", "description": "Warm rose/clay tint." },
"text": { "value": "#8B3A2A", "description": "Dark terracotta.", "contrastOnBg": "5.92:1 (AA)" },
"border": { "value": "#CFA090", "description": "Muted clay border." }
},
"info": {
"bg": { "value": "#E5E2DA", "description": "Warm neutral tint." },
"text": { "value": "#4A4540", "description": "Warm dark gray.", "contrastOnBg": "7.32:1 (AAA)" },
"border": { "value": "#B8B2A8", "description": "Warm mid gray border." }
}
},
"shadow": {
"color": { "value": "rgba(43, 36, 33, 0.06)", "description": "Warm-toned shadow base. Derived from text-primary. Barely-there desk shadow." }
}
},
"typography": {
"_meta": {
"philosophy": "Three voices, three jobs. Serif for prose (Ken's voice). Sans for structure (navigation, headings). Mono for evidence (code, proof). The reader always knows which voice is speaking.",
"scaleRatio": 1.25,
"scaleName": "Major Third",
"scaleBase": "18px"
},
"families": {
"prose": { "value": "'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif", "role": "Reading voice — body text, blockquotes, long-form prose" },
"structure": { "value": "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", "role": "Navigation voice — headings, UI labels, metadata" },
"evidence": { "value": "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace", "role": "Proof voice — code blocks, terminal output, file paths" }
},
"scale": {
"xs": { "value": "0.75rem", "px": 12, "step": -2, "use": "footnotes, legal text" },
"sm": { "value": "0.875rem", "px": 14, "step": -1, "use": "captions, meta, timestamps, tags" },
"base": { "value": "1.125rem", "px": 18, "step": 0, "use": "body prose" },
"lg": { "value": "1.375rem", "px": 22, "step": 1, "use": "h4 headings" },
"xl": { "value": "1.75rem", "px": 28, "step": 2, "use": "h3 headings" },
"2xl": { "value": "2.25rem", "px": 36, "step": 3, "use": "h2 headings — section titles" },
"3xl": { "value": "2.75rem", "px": 44, "step": 4, "use": "h1 headings — article title" }
},
"lineHeight": {
"tight": { "value": 1.15, "use": "display headings (h1)" },
"snug": { "value": 1.25, "use": "headings (h2, h3)" },
"heading": { "value": 1.35, "use": "smaller headings (h4)" },
"normal": { "value": 1.5, "use": "UI text, captions" },
"relaxed": { "value": 1.6, "use": "code blocks, blockquotes" },
"prose": { "value": 1.7, "use": "body prose — generous for dense technical narrative" }
},
"weight": {
"regular": { "value": 400, "use": "body prose, code" },
"medium": { "value": 500, "use": "UI labels, caption emphasis" },
"semibold": { "value": 600, "use": "headings, inline emphasis in prose" },
"bold": { "value": 700, "use": "h1 title, strong emphasis" }
},
"styles": {
"body": {
"fontFamily": "prose",
"fontSize": "1.125rem",
"fontWeight": 400,
"lineHeight": 1.7,
"letterSpacing": "normal",
"description": "Ken's voice on the page. 18px Source Serif 4 at generous line-height for sustained technical reading."
},
"bodySmall": {
"fontFamily": "prose",
"fontSize": "0.875rem",
"fontWeight": 400,
"lineHeight": 1.6,
"letterSpacing": "0.005em",
"description": "Footnotes, secondary prose passages. Still serif — still Ken's voice, just quieter."
},
"h1": {
"fontFamily": "structure",
"fontSize": "2.75rem",
"fontWeight": 700,
"lineHeight": 1.15,
"letterSpacing": "-0.02em",
"description": "Article title. 44px Inter Bold — confident, unhurried. Negative tracking tightens at display size."
},
"h2": {
"fontFamily": "structure",
"fontSize": "2.25rem",
"fontWeight": 600,
"lineHeight": 1.25,
"letterSpacing": "-0.015em",
"description": "Section heading. 36px Inter Semibold — chapter breaks in the narrative."
},
"h3": {
"fontFamily": "structure",
"fontSize": "1.75rem",
"fontWeight": 600,
"lineHeight": 1.3,
"letterSpacing": "-0.01em",
"description": "Subsection heading. 28px Inter Semibold — organizes without competing with prose."
},
"h4": {
"fontFamily": "structure",
"fontSize": "1.375rem",
"fontWeight": 600,
"lineHeight": 1.35,
"letterSpacing": "-0.005em",
"description": "Minor heading. 22px Inter Semibold — just enough hierarchy shift to signal structure."
},
"blockquote": {
"fontFamily": "prose",
"fontSize": "1.25rem",
"fontWeight": 400,
"lineHeight": 1.6,
"letterSpacing": "normal",
"fontStyle": "italic",
"description": "Pull quotes, cited text. 20px Source Serif 4 italic — a gentle size step up signals 'someone else's words' without shouting."
},
"caption": {
"fontFamily": "structure",
"fontSize": "0.875rem",
"fontWeight": 500,
"lineHeight": 1.5,
"letterSpacing": "0.01em",
"description": "Image captions, metadata labels, timestamps, tags. 14px Inter Medium — the structure voice at small scale."
},
"codeInline": {
"fontFamily": "evidence",
"fontSize": "0.9em",
"fontWeight": 400,
"lineHeight": "inherit",
"letterSpacing": "normal",
"description": "Inline code within prose. 0.9em relative to parent — the evidence voice embedded in narrative."
},
"codeBlock": {
"fontFamily": "evidence",
"fontSize": "1rem",
"fontWeight": 400,
"lineHeight": 1.6,
"letterSpacing": "normal",
"description": "Code blocks. 16px JetBrains Mono — factual, verifiable, this-is-what-actually-happened."
}
}
},
"spacing": {
"_meta": {
"base": "8px",
"philosophy": "The page breathes like a well-set book. Generous vertical rhythm for dense technical narrative. Space above H2 is a full breath — section breaks are chapter breaks."
},
"scale": {
"0": { "value": "0", "px": 0 },
"px": { "value": "1px", "px": 1 },
"0.5":{ "value": "0.25rem", "px": 4, "gridMultiple": "0.5x" },
"1": { "value": "0.5rem", "px": 8, "gridMultiple": "1x" },
"1.5":{ "value": "0.75rem", "px": 12, "gridMultiple": "1.5x" },
"2": { "value": "1rem", "px": 16, "gridMultiple": "2x" },
"2.5":{ "value": "1.25rem", "px": 20, "gridMultiple": "2.5x" },
"3": { "value": "1.5rem", "px": 24, "gridMultiple": "3x" },
"4": { "value": "2rem", "px": 32, "gridMultiple": "4x" },
"5": { "value": "2.5rem", "px": 40, "gridMultiple": "5x" },
"6": { "value": "3rem", "px": 48, "gridMultiple": "6x" },
"8": { "value": "4rem", "px": 64, "gridMultiple": "8x" },
"10": { "value": "5rem", "px": 80, "gridMultiple": "10x" },
"12": { "value": "6rem", "px": 96, "gridMultiple": "12x" }
},
"semantic": {
"paragraphGap": { "value": "1.5rem", "px": 24, "note": "~1.33em at 18px body. Generous — lets ideas land." },
"headingAboveH2": { "value": "4rem", "px": 64, "note": "Section breaks are chapter breaks. Full breath between sections." },
"headingBelowH2": { "value": "1rem", "px": 16, "note": "Tight — the heading belongs to its content, not the space above." },
"headingAboveH3": { "value": "2.5rem", "px": 40, "note": "Sub-section breath. Less than H2, still generous." },
"headingBelowH3": { "value": "0.75rem", "px": 12, "note": "Snug — content follows immediately." },
"headingAboveH4": { "value": "2rem", "px": 32, "note": "Minor heading spacing." },
"headingBelowH4": { "value": "0.5rem", "px": 8, "note": "Very tight — H4 is almost inline with its content." },
"codeBlockVertical": { "value": "2.5rem", "px": 40, "note": "Code blocks as exhibited artifacts — generous vertical clearance." },
"codeBlockPadding": { "value": "1.5rem", "px": 24, "note": "Internal padding of code blocks." },
"codeBlockAccentWidth": { "value": "3px", "note": "Left-edge accent line on code blocks." },
"listItemGap": { "value": "0.5rem", "px": 8, "note": "Breathing room between list items." },
"blockquoteIndent": { "value": "1.5rem", "px": 24, "note": "Left indent/border offset for blockquotes." },
"sectionDivider": { "value": "3rem", "px": 48, "note": "Vertical space when using a horizontal rule." }
}
},
"rounded": {
"_meta": {
"philosophy": "Workshop surfaces have worn edges, not machined radii. Just enough softness to not feel sharp, not enough to feel 'designed.'"
},
"none": { "value": "0" },
"sm": { "value": "2px", "use": "inline code, small tags, badges" },
"md": { "value": "4px", "use": "code blocks, buttons, small elements" },
"lg": { "value": "6px", "use": "cards, callout boxes" },
"xl": { "value": "8px", "use": "larger panels, modals (if ever needed)" },
"full": { "value": "9999px", "use": "pills, avatars, toggle tracks" }
},
"layout": {
"_meta": {
"philosophy": "Prose lives in a reading column. Code breaks wider. The width shift signals the register change from narrative to evidence."
},
"prose": { "value": "45rem", "px": 720, "approxCh": "~65ch at 18px serif", "description": "Reading column max-width. Sweet spot for sustained reading." },
"codeBreak": { "value": "52.5rem", "px": 840, "description": "Code block max-width. Wider breakout creates narrow-wide-narrow rhythm." },
"container": { "value": "68.75rem","px": 1100, "description": "Outer container max-width. Provides margin for the prose column." },
"sideGutter": { "value": "1.5rem", "px": 24, "description": "Minimum horizontal padding on small screens." }
},
"shadows": {
"_meta": {
"philosophy": "Nothing gleams. Nothing floats. If something casts a shadow, it's the small soft shadow of a card sitting on a desk — barely there."
},
"sm": { "value": "0 1px 3px rgba(43, 36, 33, 0.04)", "use": "Subtle lift — barely perceptible. Cards at rest." },
"md": { "value": "0 2px 8px rgba(43, 36, 33, 0.06)", "use": "Card on a desk — the maximum depth in this system." },
"none": { "value": "none", "use": "Explicit no-shadow override." }
},
"motion": {
"_meta": {
"philosophy": "Fast, barely-there. The confidence of something that doesn't need to announce itself. Reduce to zero for prefers-reduced-motion."
},
"duration": {
"instant": { "value": "0ms", "use": "Reduced motion fallback." },
"fast": { "value": "100ms", "use": "Color transitions (hover, focus)." },
"normal": { "value": "150ms", "use": "Standard interactions — the site's default." },
"slow": { "value": "250ms", "use": "Layout shifts, expand/collapse (rare)." }
},
"easing": {
"default": { "value": "cubic-bezier(0.25, 0.1, 0.25, 1.0)", "use": "Standard ease — smooth, natural deceleration." },
"inOut": { "value": "cubic-bezier(0.45, 0.05, 0.55, 0.95)", "use": "Symmetric transitions." }
},
"reducedMotion": {
"rule": "@media (prefers-reduced-motion: reduce)",
"behavior": "All durations set to 0ms. All transitions disabled. Non-negotiable."
}
},
"rationale": {
"colorPhilosophy": {
"title": "Why these specific colors",
"explanation": [
"The surface-base (#F5F0E8) was tuned to sit precisely between 'warm' and 'yellow.' Its RGB channels (245, 240, 232) taper from R→G→B, placing the warmth in the red channel without letting it become cream or beige. It reads as 'good light on heavy paper' — the brief's exact intent.",
"Text-primary (#2B2421) avoids the blue-black of browser defaults (which typically live around #1a1a2e or #333). The brown-black comes from keeping R > G > B at the dark end: (43, 36, 33). This is ink with warmth, not ink with coldness.",
"The accent-primary (#A0522D, CSS 'sienna') is a historically grounded earth pigment — literally fired earth. At 4.95:1 contrast on base, it passes AA for body text and works as link color without being 'red' or 'alarming.' The hover state (#8B4726) darkens toward the earth rather than toward black.",
"Accent-secondary (#7D6C2F) captures tarnished brass — the green-gold of aged copper alloy, not the bright gold of decoration. It needed darkening from the first candidate to meet AA (4.56:1), but the constraint improved the color: real tarnished brass IS darker than you'd first guess.",
"State colors (success, warning, error, info) were designed within the warm palette. No saturated primaries. Success uses sage-moss (#4A5E3A on #E8EDDF), warning uses amber (#7A5C1F on #F2E8D0), error uses deeper terracotta (#8B3A2A on #F2DED6). Even the info state avoids blue entirely — it's warm neutral (#4A4540 on #E5E2DA)."
]
},
"contrastStrategy": {
"title": "How we exceeded WCAG requirements",
"explanation": [
"The brief demanded WCAG AAA (7:1) for body text on base. We achieved 13.44:1 — nearly double the requirement. This provides comfortable reading even in imperfect lighting conditions or on devices with lower contrast ratios.",
"Secondary text at 5.46:1 (AA requires 4.5:1) gives comfortable headroom. Tertiary text at 3.04:1 meets AA-UI (3:1) — appropriate since it's used only for supplementary information like timestamps and tags, never for essential content.",
"Code text (#3D342E) on the code surface (#EEEAE2) achieves 10.12:1 (AAA). Code is evidence — it must be maximally readable.",
"Border tokens are split into three tiers: subtle (decorative, no contrast requirement — the 'score line'), default (separators), and strong (#8E857A at 3.20:1, for functional UI elements). This acknowledges that a publishing site's dividers are decorative, not interactive, while still providing accessible borders when needed."
]
},
"typographyRationale": {
"title": "Three voices, mathematically grounded",
"explanation": [
"The Major Third ratio (1.25) was chosen because the brief explicitly asked for conservative hierarchy. At 1.25, the jump from body (18px) to H4 (22px) is perceptible but not dramatic — the family change from serif to sans does most of the work. Weight and family, not size, create the hierarchy.",
"Heading sizes round to clean values: 22, 28, 36, 44px. Each pair's ratio stays within 1.17-1.29 of true Major Third — close enough for mathematical coherence, rounded enough for pixel clarity.",
"The 18px body base was set by the brief, and it's correct for sustained technical reading in a serif face. At 1.125rem, it inherits browser zoom gracefully.",
"Code at 16px (1rem / 0.9em of body) respects the 'different register' described in the aesthetic guide. JetBrains Mono at 16px with 1.6 line-height gives comfortable code reading without competing with the prose voice.",
"Letter-spacing uses negative tracking for headings (-0.02em at h1, tapering to -0.005em at h4) because Inter at display sizes needs tightening to feel intentional. Body prose and code use normal tracking — the fonts were designed for these sizes.",
"Blockquote at 20px italic is the only off-scale value — sitting between base (18) and lg (22). This is deliberate: a pull quote should feel like a different register, not a heading. The italic + size shift + left border treatment (defined in spacing) makes the voice unmistakable."
]
},
"spacingRationale": {
"title": "Book typography meets code breakout",
"explanation": [
"The 8px grid provides the full range from fine-grain (4px component padding) to macro (96px section spacing). All semantic tokens map onto grid multiples.",
"H2 above-spacing (64px / 4rem) creates the 'chapter break' the aesthetic guide describes. At nearly 4x the paragraph gap, it creates a decisive pause in the scroll. H2 below-spacing (16px / 1rem) is deliberately tight — the heading belongs to what follows, not what precedes it.",
"Paragraph gap (24px) is ~1.33em at 18px base — within the 1.2-1.5em range specified, and landing on the 8px grid (3x base).",
"Code block vertical margin (40px / 2.5rem) positions code as 'exhibited artifacts' — clearly separated from prose without creating visual gaps. The 3px left accent line in terracotta signals the register change from narrative to evidence.",
"The prose column at 720px (45rem) produces ~65 characters per line at 18px Source Serif 4 — the typographic sweet spot for sustained reading. Code breaks to 840px (52.5rem) creating the narrow-wide-narrow rhythm described in the aesthetic guide."
]
},
"borderRadiusRationale": {
"title": "Worn edges, not machined radii",
"explanation": [
"Four meaningful stops: 2px (inline code), 4px (code blocks, buttons), 6px (cards), 8px (larger panels). The progression feels like increasingly worn edges on workshop materials — slight softening, never roundness.",
"No radius exceeds 8px except 'full' (pills/avatars). Nothing on this site should look like a mobile app button or a glassmorphism card."
]
},
"motionRationale": {
"title": "Fast, barely-there, respectful",
"explanation": [
"150ms is the site's default transition — fast enough to feel instant, slow enough to not be jarring. Color transitions (hover, focus) at 100ms. Layout transitions at 250ms (rare — this is a reading site, not an app).",
"prefers-reduced-motion reduces all durations to 0ms, no exceptions. This is non-negotiable.",
"Only GPU-accelerated properties (transform, opacity) should be animated. Color transitions are the exception — they're so fast (100ms) that the CPU cost is negligible."
]
},
"whatWeDidNotDo": {
"title": "Intentional omissions",
"explanation": [
"No dark mode tokens. The aesthetic guide explicitly says 'Ken's work happens in daylight.' Dark mode can be added later if needed — the semantic naming makes it straightforward.",
"No elevation system. The brief says 'nothing floats.' Two shadow tokens (sm, md) are the maximum depth vocabulary.",
"No animation choreography. A publishing site doesn't need entrance animations, scroll-triggered effects, or page transitions. Links change color. That's it.",
"No breakpoint tokens. The layout is a single column that narrows gracefully. The prose column (720px) and code breakout (840px) handle their own responsive behavior within the 1100px container.",
"No z-index scale. Nothing overlaps. The page is flat paper with ink on it."
]
}
}
}
```
---
## Contrast Audit Results
Every text/background pairing was tested computationally. Full results:
### Body Text (WCAG AAA — 7:1 required)
| Pairing | Ratio | Result |
|---|---|---|
| `text-primary` (#2B2421) on `surface-base` (#F5F0E8) | **13.44:1** | PASS |
| `text-primary` (#2B2421) on `surface-card` (#FEFCF7) | **14.87:1** | PASS |
| `text-primary` (#2B2421) on `surface-code` (#EEEAE2) | **12.71:1** | PASS |
| `text-primary` (#2B2421) on `surface-inset` (#E8E3DA) | **11.93:1** | PASS |
### Secondary Text (WCAG AA — 4.5:1 required)
| Pairing | Ratio | Result |
|---|---|---|
| `text-secondary` (#6B5F55) on `surface-base` (#F5F0E8) | **5.46:1** | PASS |
| `text-secondary` (#6B5F55) on `surface-card` (#FEFCF7) | **6.04:1** | PASS |
| `text-secondary` (#6B5F55) on `surface-code` (#EEEAE2) | **5.16:1** | PASS |
### Tertiary Text (WCAG AA UI — 3:1 required)
| Pairing | Ratio | Result |
|---|---|---|
| `text-tertiary` (#948880) on `surface-base` (#F5F0E8) | **3.04:1** | PASS |
| `text-tertiary` (#948880) on `surface-card` (#FEFCF7) | **3.36:1** | PASS |
### Accent Links (WCAG AA — 4.5:1 required)
| Pairing | Ratio | Result |
|---|---|---|
| `accent-primary` (#A0522D) on `surface-base` (#F5F0E8) | **4.95:1** | PASS |
| `accent-primary` (#A0522D) on `surface-card` (#FEFCF7) | **5.48:1** | PASS |
| `accent-primary` (#A0522D) on `surface-code` (#EEEAE2) | **4.68:1** | PASS |
| `accent-primaryHover` (#8B4726) on `surface-base` (#F5F0E8) | **6.12:1** | PASS |
| `accent-secondary` (#7D6C2F) on `surface-base` (#F5F0E8) | **4.56:1** | PASS |
| `accent-secondary` (#7D6C2F) on `surface-card` (#FEFCF7) | **5.05:1** | PASS |
### Code Text (WCAG AAA — 7:1 required)
| Pairing | Ratio | Result |
|---|---|---|
| `code-text` (#3D342E) on `surface-code` (#EEEAE2) | **10.12:1** | PASS |
### State Colors (WCAG AA — 4.5:1 required)
| Pairing | Ratio | Result |
|---|---|---|
| `success-text` (#4A5E3A) on `success-bg` (#E8EDDF) | **5.96:1** | PASS |
| `warning-text` (#7A5C1F) on `warning-bg` (#F2E8D0) | **5.10:1** | PASS |
| `error-text` (#8B3A2A) on `error-bg` (#F2DED6) | **5.92:1** | PASS |
| `info-text` (#4A4540) on `info-bg` (#E5E2DA) | **7.32:1** | PASS |
### Functional Borders (WCAG AA UI — 3:1 required)
| Pairing | Ratio | Result |
|---|---|---|
| `border-strong` (#8E857A) on `surface-base` (#F5F0E8) | **3.20:1** | PASS |
| `border-strong` (#8E857A) on `surface-card` (#FEFCF7) | **3.54:1** | PASS |
**Note:** `border-subtle` and `border-default` are decorative (score lines, separators) and do not carry WCAG contrast requirements per [WCAG 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html) — decorative borders that don't convey information are exempt. `border-strong` is provided for any functional UI borders that DO need 3:1.
---
## Implementation Notes
### CSS Custom Properties (recommended structure)
```css
:root {
/* Surfaces */
--surface-base: #F5F0E8;
--surface-card: #FEFCF7;
--surface-code: #EEEAE2;
--surface-inset: #E8E3DA;
/* Text */
--text-primary: #2B2421;
--text-secondary: #6B5F55;
--text-tertiary: #948880;
/* Accents */
--accent-primary: #A0522D;
--accent-primary-hover: #8B4726;
--accent-secondary: #7D6C2F;
--accent-secondary-hover: #6B5C28;
/* Code */
--code-text: #3D342E;
/* Borders */
--border-subtle: #DDD7CD;
--border-default: #C2BAA9;
--border-strong: #8E857A;
/* Focus */
--focus-ring: #A0522D;
/* State: Success */
--success-bg: #E8EDDF;
--success-text: #4A5E3A;
--success-border: #B5C4A0;
/* State: Warning */
--warning-bg: #F2E8D0;
--warning-text: #7A5C1F;
--warning-border: #D4BC7C;
/* State: Error */
--error-bg: #F2DED6;
--error-text: #8B3A2A;
--error-border: #CFA090;
/* State: Info */
--info-bg: #E5E2DA;
--info-text: #4A4540;
--info-border: #B8B2A8;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(43, 36, 33, 0.04);
--shadow-md: 0 2px 8px rgba(43, 36, 33, 0.06);
/* Typography families */
--font-prose: 'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif;
--font-structure: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-evidence: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace;
/* Spacing */
--space-0: 0;
--space-px: 1px;
--space-0-5: 0.25rem;
--space-1: 0.5rem;
--space-1-5: 0.75rem;
--space-2: 1rem;
--space-2-5: 1.25rem;
--space-3: 1.5rem;
--space-4: 2rem;
--space-5: 2.5rem;
--space-6: 3rem;
--space-8: 4rem;
--space-10: 5rem;
--space-12: 6rem;
/* Border radius */
--rounded-none: 0;
--rounded-sm: 2px;
--rounded-md: 4px;
--rounded-lg: 6px;
--rounded-xl: 8px;
--rounded-full: 9999px;
/* Layout */
--width-prose: 45rem;
--width-code: 52.5rem;
--width-container: 68.75rem;
--gutter: 1.5rem;
/* Motion */
--duration-fast: 100ms;
--duration-normal: 150ms;
--duration-slow: 250ms;
--ease-default: cubic-bezier(0.25, 0.1, 0.25, 1.0);
--ease-in-out: cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
@media (prefers-reduced-motion: reduce) {
:root {
--duration-fast: 0ms;
--duration-normal: 0ms;
--duration-slow: 0ms;
}
}
```
### Font Loading (recommended)
```html
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Fonts — load only weights used -->
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,wght@0,400;0,600;1,400&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
```
---
## Success Criteria
- **Functional:** All tokens resolve to concrete CSS values. Zero undefined variables.
- **Aesthetic:** Every color reads warm. The page feels like paper and ink under good light.
- **Accessibility:** AAA body text (13.44:1), AA secondary (5.46:1), AA accent links (4.95:1). All verified.
- **Performance:** Three fonts, minimal weights. No heavy shadow/blur calculations.
- **Longevity:** Nothing here will look dated in 5 years. Paper, ink, and careful spacing don't go out of style.