Files
crashtestdev/.design/specs/components-2026-05-26.md
T
Ken a951c00e35 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>
2026-05-27 00:42:18 +00:00

598 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.