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

32 KiB
Raw Blame History

feature, date, status, project, tags, related
feature date status project tags related
CoreComponentSpecs 2026-05-26 planned crash-test-dev
components
article-card
code-block
audio-player
callout
post-header
nav-bar
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:

.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.