diff --git a/.amplifier/AGENTS.md b/.amplifier/AGENTS.md new file mode 100644 index 0000000..3846fe9 --- /dev/null +++ b/.amplifier/AGENTS.md @@ -0,0 +1,106 @@ +# Crash Test Dev -- Agent Instructions + +## Standing Rules + +### Voice Linting Is Mandatory Before Publishing + +Every piece of written content (blog posts, articles, lessons, patterns) MUST +pass through the voice-check pipeline before it can be published. No exceptions. + +**Before creating or modifying any content in `src/content/posts/`:** + +1. **Draft the content** +2. **Run mechanical lint:** `./scripts/lint-voice.sh ` + - Must have 0 FAILURES to proceed + - Warnings are acceptable but should be acknowledged +3. **Run LLM judge:** Delegate to self with `model_role: "critique"` using the + voice-check prompt at `.amplifier/skills/voice-check/SKILL.md` + - Evaluates 8 dimensions: em-dash addiction, conviction posture, evidence + instinct, narrative engine match, structural parallelism, opening quality, + ending quality, personality presence + - Must reach PUBLISH or REVISE verdict (not REWRITE) +4. **Fix all failures** before committing +5. **Re-run both checks** after fixes to confirm + +**If the mechanical lint or LLM judge is skipped, the content is not published.** +This applies even if the user says "just publish it." Run the checks, report +the results, and let the user decide whether to fix issues or publish anyway +with full awareness of what's wrong. + +### What the Checks Catch + +**Mechanical lint (`scripts/lint-voice.sh`):** +- Em-dash density (>5 per post = FAIL, Claude fingerprint) +- AI trigger words (delve, tapestry, nuanced, landscape, etc.) +- Hedging phrases ("it's worth noting", "one might argue") +- Filler transitions ("in conclusion", "that being said") +- Resolution closers ("the key takeaway", "at the end of the day") +- Missing receipts (0 links + 0 code + 0 URLs in 500+ words = WARN) +- Sentence length uniformity (>70% same length = WARN) + +**LLM judge (voice-check skill):** +- Voice match against `.amplifier/VOICE.md` profile +- Narrative engine (essay mode = AI default, enemy-narrative = Ken's default) +- Structural parallelism (too-clean bullet lists, tricolon patterns) +- Opening quality (problem/hook vs generic framing) +- Ending quality (restating intro = anti-pattern) +- Personality presence (humor, asides, self-awareness) + +### Voice Profile Reference + +Ken's voice profile lives at `.amplifier/VOICE.md`. Key traits: +- **Conviction:** High, earned. States conclusions, doesn't hedge. +- **Evidence:** Receipt-shower. Links, code, data. Non-negotiable. +- **Humor:** Playful escalation + dry observation, sparse. +- **Sentence rhythm:** Short/long alternation, not uniform. +- **Narrative:** Enemy-narrative diagnostic is the default blog engine. +- **Endings:** End when done. Never restate the intro. + +### Em-Dash Budget + +Ken's real writing uses 0-2 em-dashes per 1000 words. Claude defaults to +10-15 per 1000. The budget for this blog: + +- **0-2 per post:** Clean +- **3-5 per post:** Acceptable, review each one +- **6+:** FAIL. Kill at least half. Replace with periods, colons, or restructure. + +When you find yourself typing an em-dash, stop and ask: would a period work here? +It almost always does. + +## Project Context + +- **Site:** Crash Test Dev (Astro blog at blog.ampbox.io / kenotron.github.io/crashtestdev) +- **Author:** Ken Chau (kenotron on GitHub, @kenneth_chau on Twitter) +- **Content types:** Posts (war stories), Patterns (reference), Lessons (progressive) +- **Design system:** DESIGN.md at project root (Google DESIGN.md spec-compliant) +- **Aesthetic:** "Workbench, not gallery" -- warm, grounded, un-digital +- **Voice profile:** .amplifier/VOICE.md +- **Design specs:** .design/AESTHETIC-GUIDE.md, .design/specs/ + +## Content Pipeline + +``` +Idea / source material + | + v +Draft post (src/content/posts/slug.md) + | + v +Mechanical lint: ./scripts/lint-voice.sh + | + v +LLM judge: voice-check (8 dimensions) + | + v +Fix failures, re-check + | + v +Rebuild preview: rebuild /tmp/blog-dist, serve on :4321 + | + v +Visual check at blog.ampbox.io + | + v +Commit + push +``` diff --git a/.amplifier/BLOG_ARCHITECTURE.md b/.amplifier/BLOG_ARCHITECTURE.md new file mode 100644 index 0000000..830d5b7 --- /dev/null +++ b/.amplifier/BLOG_ARCHITECTURE.md @@ -0,0 +1,151 @@ +# Blog Architecture: Crash Test Dev + +## What We're Building + +A blog for lessons, patterns, and writing. Elegant like the Amplifier Masterclass +site. Read-aloud feature. No AI slop. Ken's voice throughout. + +## Reference: What the Masterclass Does Right + +The masterclass site (anderlpz.github.io/amplifier-masterclass/) achieves its +elegance through: + +- **Three-font stack**: Serif body (Source Serif 4), sans headings (Inter), mono + code (JetBrains Mono). The serif/sans pairing is the single biggest quality signal. +- **18px base, 1.75 line-height**: Generous reading. Feels like a printed essay. +- **Muted color palette**: Cool lavender-grey background (#F4F5F9), body text in + secondary grey (#5C5E6E) not black, muted steel blue accent (#3E5C8A). +- **720px reading column + 840px breakout**: Prose narrow, code/diagrams wider. + Creates visual breathing rhythm. +- **Pre-recorded narration**: OpenAI TTS voices (Fable, Nova, Alloy), 3 choices, + with full transport controls, scrubber, speed selection, auto-advance. +- **Progressive disclosure**: TOC pill, drawer, inline term definitions, xref + hovers, scroll-driven diagram revelation. +- **Zero framework bloat**: Hand-crafted HTML/CSS/JS. Every pixel intentional. + +## Tech Decision: Astro + +**Current stack (Gatsby 2)**: Dead framework. React 16. gatsby-theme-blog 1.0. +Not worth upgrading -- better to start fresh. + +**Recommended: Astro** +- Same framework the masterclass uses for its component rewrite +- Ships zero JS by default. JS only where explicitly needed (audio player, TOC) +- Markdown/MDX content with frontmatter. Content-first architecture +- Built-in code highlighting (Shiki), image optimization, RSS +- Minimal deps: just astro + typescript, like the masterclass + +**Why not Next.js/Remix**: Over-engineered for a blog. Ken's content doesn't +need server-side rendering or API routes. Static generation + islands is the +right fit. + +## Design Approach + +Inspired by the masterclass but adapted for a multi-post blog (not single-page +document): + +### Typography +| Role | Font | Why | +|------|------|-----| +| Body prose | Lora or Source Serif 4 | Serif signals "read this slowly" | +| Headings, UI | Inter | Clean, modern, tight tracking | +| Code | JetBrains Mono or Berkeley Mono | Monospaced with personality | + +### Color +Warm variation on the masterclass palette: +- Background: warm paper (#F4F2ED or similar) -- not cool lavender +- Surface: white cards +- Text: near-black (#1A1815), secondary grey for body +- Accent: deep blue or muted violet (Ken's existing blog uses #007acc) +- No dark mode initially (the masterclass explicitly chose this) + +### Layout +- 650-720px reading column, centered +- Code blocks break wider (840-900px) +- Generous vertical spacing between sections (64px+ for h2 breaks) +- Fixed minimal nav at top (site title, optional TOC pill for long posts) + +### Content Structure +``` +/ Index -- list of posts, most recent first +/posts/[slug] Individual post pages +/patterns/[slug] Technical patterns (shorter, reference-style) +/lessons/[slug] Longer lesson-format pieces +``` + +Frontmatter: +```yaml +--- +title: "Speeding Up Webpack Typescript Incremental Builds by 7x" +date: 2019-08-16 +type: post | pattern | lesson +tags: [webpack, typescript, performance] +audio: true # triggers TTS generation +summary: "One-sentence hook for the index page" +--- +``` + +## Read-Aloud Feature + +### How the Masterclass Does It +- Pre-generated MP3s per chapter using OpenAI TTS API +- 3 voice options (Fable, Nova, Alloy) +- Script files that are spoken-word summaries, NOT verbatim text reads +- Full transport controls (play/pause, skip, scrub, speed, voice select) + +### Our Approach +Same pattern, adapted for blog posts: + +1. **Generate at build time or on-demand**: For each post with `audio: true`, + generate a narration script (spoken-word summary, not verbatim) and run + it through OpenAI TTS. + +2. **Narration script generation**: Use the VOICE.md profile to write + spoken versions. Key rules from the masterclass: + - Don't read code blocks aloud. Describe what they do. + - Don't read URLs. Say "I've linked the PR in the post." + - Keep the conversational energy. This should sound like Ken + explaining it to you at a whiteboard. + +3. **Audio player component**: Minimal pill in the nav area. Click to + expand transport controls. Single voice initially (can add multiple + later). + +4. **Per-post, not per-section**: Unlike the masterclass's per-chapter + audio, blog posts are short enough for single-track narration. + +### TTS Pipeline +``` +content/posts/my-post.md + --> narration-script generator (strips code, converts to spoken prose) + --> OpenAI TTS API (voice: "fable" or "nova") + --> public/audio/my-post.mp3 + --> referenced in post frontmatter +``` + +## Migration Path + +### Phase 1: Foundation +- New Astro project in this repo (replace Gatsby) +- Design tokens (CSS custom properties, no Tailwind) +- Base layout, typography, color +- Markdown/MDX content pipeline +- Migrate 2 existing posts + +### Phase 2: Polish +- Audio player component (pill + transport) +- Generate narration for existing posts +- Index page design +- Code block styling (breakout width, optional filenames) +- RSS feed + +### Phase 3: Content Types +- Pattern pages (shorter, reference-style) +- Lesson pages (longer, progressive disclosure) +- Cross-references between related content + +### Phase 4: Advanced +- Scroll-synced TOC for long posts (like the masterclass) +- Inline term definitions (if building a technical vocabulary) +- Reading progress indicator +- Multiple TTS voices diff --git a/.amplifier/VOICE.md b/.amplifier/VOICE.md new file mode 100644 index 0000000..9851727 --- /dev/null +++ b/.amplifier/VOICE.md @@ -0,0 +1,303 @@ +# Writing Voice Framework & Profile + +## Part I: The Framework -- Dimensions of Writing Voice + +Writing voice is not a single thing. It's a composite of independent dimensions +that each writer dials differently, and that shift depending on what the writing +is *for*. Generic "tone guidelines" flatten this into "professional but friendly" +-- which is exactly how you get AI slop. Real voice has texture because the +dimensions interact in unexpected ways. + +### Person-Stable Dimensions (the writer's fingerprint) + +These are relatively constant across everything a person writes. They're the +dimensions that make you recognize someone's writing even without a byline. + +**1. Conviction Posture** +How strongly opinions are held and expressed. Ranges from constant hedging +("it might be worth considering...") to full-throat assertion ("this is wrong +and here's why"). Most writers have a resting conviction level that shifts +only under specific pressure. + +**2. Intellectual Posture** +The relationship the writer assumes with the reader. Not "reading level" -- +it's deeper than that. Options: +- Teaching-down: "Let me explain..." +- Peer-to-peer: "You've probably seen this..." +- Thinking-aloud: "I wonder if..." +- Diagnostic: "The issue here is that..." + +Most people have a default. The best writers shift fluidly but have a home base. + +**3. Warmth Gradient** +How much the writer acknowledges the reader as a person. "We" vs "one." Asides +about shared struggles vs pure information delivery. Humor is related but +separate -- warmth is about inclusion, humor is about surprise. + +**4. Humor Register** +Not "funny or not" -- the *type* of humor. Options: +- Deadpan understatement ("This is fine.") +- Playful escalation ("MOAR!") +- Self-deprecating ("I'm keeping this strange little name") +- Dry observation ("your application starts to look like a sideways mountain") +- Absurdist aside +- None / earnest + +**5. Sentence Architecture** +Not just "short vs long" -- the *rhythm*. Some writers build momentum through +increasingly complex sentences that cascade. Others alternate: short punch. +Then the explanation. Some embed parenthetical asides (like this) constantly. +Others never interrupt themselves. + +**6. Evidence Instinct** +When making a claim, does the writer reach for: +- Show receipts (links to PRs, npm packages, flame graphs) +- Argument from principle ("the separation of concerns demands...") +- Personal experience ("at work, we found...") +- Authority citation ("as John-David Dalton told me") +- Code that proves the point + +This is deeply habitual. Receipt-showers always show receipts. Principle- +arguers always argue from principle. The instinct persists even when the other +approach would serve better. + +### Purpose-Shifting Dimensions (changes with the writing) + +These dimensions move depending on what the piece is trying to do. A tutorial +and a rant by the same person will differ here while maintaining the same +fingerprint above. + +**7. Information Density** +How much per paragraph. Tutorials are sparse -- one concept, one example, +breathe. Technical deep-dives are dense -- three insights per paragraph, +assumes you're keeping up. Manifestos are medium -- big ideas with space +to land. + +**8. Formality Register** +Casual first-person ("I found this bug") vs. detached ("the bug was +identified") vs. conversational-authoritative ("What if I told you your +Webpack is doing too much work all this time?"). This shifts with audience +and publication context more than anything else. + +**9. Narrative Engine** +What structural device drives the piece forward: +- Problem/solution linear (tutorial mode) +- Problem/enemies (naming adversaries, then defeating them) +- Mystery/reveal ("here's what nobody realizes...") +- Chronological journey ("first we tried X, then Y") +- Argument/counterargument (essay mode) + +**10. Reader Model** +Who the writer imagines reading this. Not demographics -- *competence +assumptions*. "You've probably used Webpack" is very different from "Webpack +is a build tool that..." This shifts with purpose but tends to cluster: some +writers always assume competence, others always explain. + +--- + +## Part II: Ken's Voice Profile + +### Sources Analyzed + +- Blog post: "Localize React without Bloating the Bundle" (2019) +- Blog post: "Speeding Up Webpack Typescript Incremental Builds by 7x" (2019) +- 10+ Amplifier session transcripts (2025-2026) +- Agent delegation instructions and bug reports + +### The Fingerprint (Person-Stable) + +**Conviction: High, earned** +Ken states opinions as conclusions from experience, not as positions to defend. +"I believe that localization is difficult because it requires excellence in +several pieces of a stack at the same time." This isn't hedging and it isn't +posturing -- it's someone who has done the work and is reporting what they found. +He doesn't say "in my humble opinion" and he doesn't say "anyone who disagrees +is wrong." He says "here's what I found" and trusts you to evaluate it. + +In sessions: "I don't want generic voice or ai slop" -- direct rejection +without explanation. He knows what he doesn't want and says so plainly. + +**Intellectual Posture: Diagnostic peer** +Ken's default mode is the diagnostician. "The issue here is that Webpack 4 +gave toJson a lot more information, but it also regressed the performance +tremendously as a result." He's not teaching you what Webpack is. He's not +wondering aloud what the problem might be. He identified the issue, he's +telling you what he found, and he's trusting you to follow. + +The peer part shows in his use of "you" -- not pedagogical ("now you'll +want to") but collaborative ("you would recognize three separate phases"). +He treats the reader as someone who would recognize the same thing he did +if they looked at the same data. + +**Warmth: Present but understated** +Not effusive. Not cold. Shows up as: +- "Not everyone is as fortunate" (acknowledging others' situations) +- "Let's polish this a little bit" (inclusive, understated scope) +- "Don't worry! It is only one HOC for the entire app" (quick reassurance) +- "Let's name some enemies" (inviting the reader into the hunt) + +He warms up through inclusion, not through sentiment. + +**Humor: Playful escalation + dry observation** +Two distinct modes that alternate: +- Playful: "why should we humans not demand MOAR? Yes, MOAR speed!!!" +- Dry: "your application starts to look like a sideways mountain" +- Self-aware: "I'm keeping this strange little name until React.createResource() + becomes a thing maybe in the future" + +The humor is never mean, never forced, and never the point. It's a pressure +release valve in otherwise dense technical content. It signals: "I'm a person, +and this should be fun." + +**Sentence Architecture: Staccato with cascading explanations** +Short declarative opener. Then a longer sentence that unpacks it. Then evidence. +Then another short sentence that re-anchors. + +Example rhythm: +> "This is the big one -- it brought our incremental speeds from 30s to around +> 15s." + +One sentence. The claim, the proof, the dash-connected aside. No preamble. + +In sessions, even more compressed: "look at [URL] and get me a bundle.md that +includes this thing." Subject, verb, object. Done. + +**Evidence Instinct: Receipt-shower** +Ken links to PRs, npm packages, GitHub issues, repos. Every claim comes with +a URL or a code block. "Here's the bug: [link]". "I've published a temporary +fork: [link]". "I've created a repo to demonstrate: [link]". + +This extends to sessions: he provides URLs, file paths, device contexts, +exact reproduction steps. He doesn't argue from principle -- he shows you +what he's looking at. + +### The Shifts (Purpose-Dependent) + +**Blog posts: Enemy-narrative diagnostic** +Both existing posts follow the same engine: +1. Hook that frames the stakes ("What if I told you...") +2. List the enemies or stages (numbered, named, personified) +3. Diagnose each one with specific evidence +4. Show the fix with code +5. Quantify the improvement +6. Brief, personality-forward closing + +This is the "war story" structure. Not tutorial, not essay, not rant. +It's "here's what we fought and here's how we won." This works for +the type of content Ken writes -- performance work, tooling decisions, +infrastructure choices -- where the narrative *is* the investigation. + +**Sessions: Compressed intent + layered iteration** +In conversation, Ken is extremely compressed. "get me a bundle.md that +includes this thing." No context dump, no over-specification. He trusts +the agent to figure out what "this thing" means from the URL he provided. + +When the first pass isn't right, he layers: "well, I'm also going to +need..." "I can confirm that..." "Maybe we should have..." Each message +adds one thing. He doesn't try to specify everything upfront. + +**Bug reports: Empirical, multi-observation** +"I can confirm the scrolling doesn't work" -- starts with validation. +Then lists 3-4 related observations in one breath, each with device +context. Then proposes a fix direction. Then adds "also the icons..." +as an afterthought. The bug report is a stream of observations, not +a structured ticket. + +### Anti-Patterns (What Sounds Unlike Ken) + +- Opening with "In this blog post, we will explore..." (generic framing) +- "It is important to note that..." (filler hedging) +- Explaining things the reader already knows (underestimating audience) +- Excessive qualification ("arguably," "potentially," "it could be said") +- Missing the receipts (claims without links, code, or data) +- Corporate-neutral voice ("we are excited to announce...") +- Ending with a generic summary that restates the intro +- Emoji-heavy prose +- Bullet-point listicles without connective narrative + +### The Voice in One Paragraph + +Ken writes like a senior engineer at a whiteboard -- he draws the architecture, +points at the problem, shows you the profiler output, and says "see this? that's +what's killing us." He assumes you know what a flame graph is. He names his +problems like enemies in a video game. He shows receipts for every claim -- PRs, +packages, repos, reproduction steps. When the fix works, he tells you the number. +When he's excited, he says MOAR. When he's done, he stops. The writing has +momentum because every sentence either identifies a problem, shows evidence, or +delivers a fix. Nothing exists to fill space. + +--- + +## Part III: Voice Application Rules + +### For Writing New Content + +1. **Open with the problem or the hook, not the topic.** Not "Today I want to + talk about caching." Instead: "Our cache was lying to us and we didn't know + it for six months." + +2. **Name the enemies.** Give problems identities. "Enemy #1: stats.toJson" + is more memorable than "Issue 1: Performance regression in statistics + serialization." + +3. **Show the receipt immediately after the claim.** Claim, then code block + or link. Not claim, then three paragraphs of context, then evidence. + +4. **Assume competence.** Don't explain what React is. Don't explain what + a build tool does. The reader is a peer who happens to not have looked + at this particular problem yet. + +5. **Quantify the win.** "This brought us from 30s to 15s." Not "this + significantly improved performance." + +6. **One aside per section, max.** The humor and personality live in the + asides -- "MOAR!", "sideways mountain", "strange little name." But they + work because they're rare. Two per section and it becomes a comedy act. + +7. **End when you're done.** No summary paragraph restating everything. + No "in conclusion." The last section's fix or the closing thought IS + the ending. + +### For AI-Assisted Writing + +When using an AI to draft or extend content in Ken's voice: + +- Feed the AI the specific problem, the evidence (links, data, code), and + the conclusion. Let it connect them. Don't ask it to "write a blog post + about X" -- that's how you get slop. +- Review for conviction. If the draft says "might," "could," "arguably" -- + those are almost always wrong. Ken knows what he found. State it. +- Check the evidence instinct. Every paragraph should either set up a claim + or pay it off with proof. If a paragraph does neither, cut it. +- The humor should feel accidental. If it feels constructed, remove it. + Ken's humor works because it reads like something that slipped out while + he was genuinely excited. + +### For Text-to-Speech + +The voice should match the writing's energy: +- Not radio-announcer smooth. Slightly conversational. +- Pauses before evidence blocks (where a reader would shift their eyes to code). +- Slightly faster on the enthusiasm moments ("MOAR speed!!!"). +- The read-aloud version should be a *summary narration*, not verbatim -- the + masterclass site does this well. A spoken version of a code-heavy post needs + to describe what the code does rather than read variable names. + +--- + +## Part IV: Dimensions Quick Reference + +For any new piece of writing, calibrate these: + +| Dimension | Ken's Default | Adjustable Range | +|-----------|--------------|-----------------| +| Conviction | High, earned | Never hedge to hedge. Soften only when genuinely uncertain | +| Intellectual posture | Diagnostic peer | Can shift to thinking-aloud for exploratory posts | +| Warmth | Understated inclusion | Can increase for community-facing pieces | +| Humor | Playful + dry, sparse | Turn down for pure reference docs, never turn up past current level | +| Sentence architecture | Short/long alternation | Longer cascades OK for narrative pieces | +| Evidence instinct | Show receipts | Always. Non-negotiable | +| Density | Medium-high | Lower for tutorials, higher for deep-dives | +| Formality | Conversational-authoritative | Never corporate-neutral | +| Narrative engine | Enemy-narrative diagnostic | Can use mystery/reveal or journey for different content | +| Reader model | Assumes competence | Only lower for explicit beginner content, and even then, only slightly | diff --git a/.amplifier/brief.md b/.amplifier/brief.md new file mode 100644 index 0000000..4204367 --- /dev/null +++ b/.amplifier/brief.md @@ -0,0 +1,110 @@ +# Crash Test Dev -- Design Brief + +## What This Is + +A personal publishing platform for Ken Chau. Not a corporate dev blog, not a +tutorial site, not a portfolio. It's a thinking journal with publishing-grade +craft. + +The content: lessons learned from building software at scale, architecture +patterns and anti-patterns, long-form technical essays, and occasionally +reflections on the broader journey of building things. + +## The Audience Hierarchy + +1. **Me** -- The primary reader. This is where I reference my own thinking. + If I can't find something I wrote easily, the design failed. +2. **My kids** -- Someday they might want to understand what their dad spent + all those hours doing. The writing should be accessible enough that a + curious non-engineer could follow the narrative even if they skip the code. +3. **Peers** -- Engineers who've fought similar battles. They don't need + things explained. They want the diagnosis and the receipts. +4. **Travelers** -- Anyone on a similar journey who stumbles across this + and finds it useful. Not optimized for them, but welcoming. + +## The Feeling + +Think of a well-used workshop. Not the pristine showroom workshop in a magazine +-- the one where someone actually builds things. Tools are organized because +disorganization wastes time, not because someone is performing organization. +The wood has a warmth to it. The light is good because you need to see details. +There's a coffee cup somewhere. + +Not cold. Not clinical. Not dark-mode-hacker-aesthetic. Not "personal brand." +Not corporate. Not trying to be minimal for minimal's sake. Just: warm, clear, +serious about craft, with enough personality to feel like a real person writes +here. + +## The Writing Voice + +See .amplifier/VOICE.md for the full profile. Key signals the design must +support: + +- **Diagnostic peer tone** -- the typography should say "read this carefully, + it's worth your time" without saying "this is an academic paper" +- **Code as first-class evidence** -- code blocks are not secondary to prose. + They ARE the proof. They need space, legibility, and visual weight. +- **Enemy-narrative structure** -- sections are battles. The design should + support clear section breaks that feel like chapters, not just headings. +- **Receipt-shower instinct** -- links, PRs, repos embedded in text. The + design needs to handle inline links gracefully without visual noise. +- **Sparse humor** -- the occasional aside or escalation. The design doesn't + need to "support" humor, it just needs to not be so serious that humor + feels out of place. + +## Content Types + +Three types, each with different density and purpose: + +| Type | Length | Density | Narrative Engine | Example | +|------|--------|---------|-----------------|---------| +| **Post** | 1000-3000 words | Medium-high | War story, enemy-narrative | "Speeding Up Webpack Builds by 7x" | +| **Pattern** | 300-800 words | High, reference | Problem/solution/tradeoff | "Dynamic Import for Locale Chunks" | +| **Lesson** | 2000-5000 words | Medium | Progressive revelation | "How We Built the Module System" | + +## Multi-Format Ambition + +This should be designed so it can become a podcast or YouTube channel: + +- **Written form** is the source of truth +- **Audio form** (TTS narration) should be a first-class output, not a + gimmick. Conversational energy, not radio-announcer. Spoken summaries + that describe code instead of reading variable names. +- **Visual form** (eventual YouTube) -- diagrams and architecture visuals + should be self-contained, exportable, and consistent enough to use as + slides or b-roll. + +## What the Design Must Support + +- Long reading sessions (generous typography, warm backgrounds) +- Code blocks as first-class citizens (breakout width, syntax highlighting) +- Architecture diagrams and visuals (wide areas, consistent visual language) +- Audio player (integrated, not bolted-on) +- Multiple content types at different densities +- Time progression (this is a journal with dates, not a static reference) +- Cross-references between related content +- Eventual search/index for self-reference + +## What the Design Must Avoid + +- Corporate blog aesthetic (sidebars, CTAs, newsletter popups) +- "Personal brand" aesthetic (hero headshot, social media icons, "hire me") +- Minimalist-to-barren (nothing for the eye to hold onto) +- Dark hacker aesthetic (doesn't match the warmth) +- Trendy effects that will look dated in 2 years (gradients, glassmorphism) +- Framework-heavy feel (no Tailwind utility-class soup in the output) +- Decoration without purpose (every visual element should earn its place) + +## Reference + +The Amplifier Masterclass (anderlpz.github.io/amplifier-masterclass/) is the +aesthetic target for quality level. Key lessons from it: + +- Serif body + sans headings = the single biggest elegance signal +- 18px base at 1.75 line-height = reads like a printed essay +- Code blocks breaking wider than prose = creates visual rhythm +- Muted, unsaturated palette = nothing screams, everything whispers +- Pre-recorded TTS narration with transport controls = content worth hearing +- No framework, every pixel intentional = craft over convenience + +Adapted for a multi-post blog rather than a single-page document. diff --git a/.amplifier/skills/voice-check/SKILL.md b/.amplifier/skills/voice-check/SKILL.md new file mode 100644 index 0000000..e5bd109 --- /dev/null +++ b/.amplifier/skills/voice-check/SKILL.md @@ -0,0 +1,164 @@ +--- +skill: + name: voice-check + description: > + LLM-as-judge anti-slop and voice authenticity check for blog posts. + Runs a mechanical lint (em-dashes, trigger words, hedging) then an LLM + evaluation against the author's voice profile for deeper pattern detection. + version: 1.0.0 + context: fork + user-invocable: true + disable-model-invocation: true + model_role: critique +--- + +# Voice Check -- Anti-Slop & Voice Authenticity Judge + +You are a writing quality judge. Your job is to evaluate a blog post for AI slop +and voice authenticity. You are ruthless, specific, and constructive. + +## Steps + +### Step 1: Gather inputs + +Read these three files: + +1. The post to evaluate -- the user will specify which file, OR check for the + most recently modified `.md` file in `src/content/posts/` +2. The voice profile at `.amplifier/VOICE.md` +3. Run the mechanical linter: `./scripts/lint-voice.sh ` and capture + its output + +If `lint-voice.sh` doesn't exist or fails, skip the mechanical lint and note it. + +### Step 2: Mechanical lint results + +Report the output of `lint-voice.sh` verbatim. This catches: +- Em-dash density (FAIL if >5 per post) +- AI trigger words (delve, tapestry, nuanced, landscape, etc.) +- Hedging phrases +- Filler transitions +- Resolution closers +- Receipt count (links, URLs, code blocks) +- Sentence length uniformity + +### Step 3: LLM Judge evaluation + +Using VOICE.md as the reference profile, evaluate the post on these 8 dimensions. +For each dimension, give a **PASS**, **WARN**, or **FAIL** verdict with a specific +one-sentence justification. Quote the problematic text when failing. + +#### Dimension 1: Em-Dash Addiction +Does the post overuse em-dashes? Count them. The author's real writing uses 0-2 +per 1000 words. More than 5 per 1000 = FAIL. + +Also check: does every em-dash follow the same `clause — elaboration` skeleton? +If so, that's a Claude fingerprint even if the count is low. + +#### Dimension 2: Conviction Posture +Does the writing hedge when it should assert? Look for: +- "might", "could", "arguably", "potentially" used to soften claims the author + clearly believes +- Passive constructions that hide the actor ("it was found" vs "I found") +- False balance ("on the other hand" when there IS no valid other hand) + +The voice profile says: "High, earned. States opinions as conclusions from +experience, not as positions to defend." + +#### Dimension 3: Evidence Instinct (Receipts) +Does the post show receipts? The voice profile says this is NON-NEGOTIABLE. +Check for: +- Links to PRs, repos, packages, docs +- Code blocks that prove a point +- Specific numbers with sources +- Named real-world examples + +A 1000+ word post with zero links AND zero code = FAIL unless it's explicitly +framed as a manifesto or opinion piece (not a war story). + +#### Dimension 4: Narrative Engine Match +What structural device drives the post? Compare to the voice profile's default: +"Enemy-narrative diagnostic" (hook → name enemies → diagnose → show fix → quantify). + +Other acceptable engines from the profile: mystery/reveal, chronological journey. +Flag if the post uses argument/counterargument essay mode (this is the default +mode of AI, not of the author). + +#### Dimension 5: Structural Parallelism +Are bullet lists too clean? Look for: +- 3+ bullets all following the exact same grammatical skeleton +- Lists where every item is the same length +- The "tricolon" pattern: exactly 3 items in every list (AI defaults to 3) + +Human writers vary their list item structure. AI makes them suspiciously parallel. + +#### Dimension 6: Opening Quality +Does the post open with the problem or the hook? Or does it open with a generic +topic-setting frame? + +BAD: "In the evolving landscape of AI engineering..." +BAD: "Today I want to talk about..." +GOOD: "Our cache was lying to us for six months." +GOOD: "What if I told you your Webpack is doing too much work?" + +The voice profile says: "Open with the problem or the hook, not the topic." + +#### Dimension 7: Ending Quality +Does the post end when it's done? Or does it: +- Restate the intro (the "In conclusion" anti-pattern in disguise) +- Wrap up too neatly +- Add a generic call-to-action + +The voice profile says: "End when you're done. No summary paragraph restating +everything. The last section's fix or the closing thought IS the ending." + +#### Dimension 8: Personality Presence +Does the author's personality show? Look for: +- At least one moment of humor, self-awareness, or dry observation per 1000 words +- The "aside" pattern: a brief parenthetical or sentence that breaks the serious + tone with personality +- Any moment where you can tell a specific human wrote this, not a generic smart + person + +The voice profile says humor is "a pressure release valve in otherwise dense +technical content" and should be "sparse but present." + +### Step 4: Specific Fix Recommendations + +For every FAIL and WARN, provide a SPECIFIC fix. Not "reduce em-dashes" but +"Line 23: replace '— it fools everyone longer' with '. It fools everyone longer'" + +Group fixes by effort: +- **Quick fixes** (find-and-replace, delete a phrase): do these now +- **Structural fixes** (rewrite a section, add evidence): flag for the author +- **Voice fixes** (reframe the narrative engine, add personality): author decision + +### Step 5: Scorecard + +``` +DIMENSION VERDICT NOTES +Em-dash addiction PASS/WARN/FAIL count, density +Conviction posture PASS/WARN/FAIL hedge count +Evidence instinct PASS/WARN/FAIL receipt count +Narrative engine match PASS/WARN/FAIL engine identified +Structural parallelism PASS/WARN/FAIL worst offender +Opening quality PASS/WARN/FAIL opening type +Ending quality PASS/WARN/FAIL ending type +Personality presence PASS/WARN/FAIL moment count + +MECHANICAL LINT: X failures, Y warnings +LLM JUDGE: X failures, Y warnings + +OVERALL: PUBLISH / REVISE / REWRITE +``` + +**PUBLISH** = 0 failures, 0-2 warnings +**REVISE** = 0-1 failures (fixable with quick edits), any warnings +**REWRITE** = 2+ failures or structural/voice failures that need rework + +## Important + +- Be harsh. It's better to catch slop before publishing than after. +- Quote specific lines. Vague feedback is useless feedback. +- The author WANTS to hear this. Don't soften. +- If the post is genuinely good, say so. Don't manufacture criticism. diff --git a/.design/AESTHETIC-GUIDE.md b/.design/AESTHETIC-GUIDE.md new file mode 100644 index 0000000..9f3de62 --- /dev/null +++ b/.design/AESTHETIC-GUIDE.md @@ -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 \ No newline at end of file diff --git a/.design/specs/components-2026-05-26.md b/.design/specs/components-2026-05-26.md new file mode 100644 index 0000000..65d9ef5 --- /dev/null +++ b/.design/specs/components-2026-05-26.md @@ -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 `
` element containing an `` 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 `` wrapping a `
`. 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(); │ +│▌ 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 `
` containing an optional `
` (filename/language) and a `
` 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
+
+- `
` 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 `
` 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 `