fix: purge AI slop from judgment essay, add voice linting pipeline
- Killed 16 em-dashes (Claude fingerprint, 15.9/1000 words -> 0) - Replaced all clause-dash-elaboration patterns with periods, colons, restructuring - Removed 'The One-Sentence Version' section (restated the intro, voice anti-pattern) - Broke tricolon at lines 56-58 (too-clean parallel structure) - Collapsed 'How to Actually Help' listicle into connective prose - Added receipt link for ~100x inference cost claim - Heading dashes replaced with colons (Tradesman Analogy, Excellence vs Functional) New tooling: - scripts/lint-voice.sh: mechanical anti-slop linter (em-dashes, trigger words, hedging, filler, receipts, sentence uniformity) - .amplifier/skills/voice-check/SKILL.md: LLM-as-judge voice authenticity check (8 dimensions against VOICE.md profile) - .amplifier/AGENTS.md: standing rule requiring both checks before any publish 🤖 Generated with Amplifier Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
@@ -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 <file>`
|
||||||
|
- 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 <file>
|
||||||
|
|
|
||||||
|
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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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 |
|
||||||
@@ -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.
|
||||||
@@ -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 <post-file>` 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.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
---
|
||||||
|
feature: CoreComponentSpecs
|
||||||
|
date: 2026-05-26
|
||||||
|
status: planned
|
||||||
|
project: crash-test-dev
|
||||||
|
tags: [components, article-card, code-block, audio-player, callout, post-header, nav-bar]
|
||||||
|
related: [AESTHETIC-GUIDE.md, design-tokens-2026-05-26.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Component Specifications — Crash Test Dev
|
||||||
|
|
||||||
|
Six components designed against the established token system. Every value traces to a named token. Every choice traces to the aesthetic guide.
|
||||||
|
|
||||||
|
**Governing principle:** These components are surfaces on the workbench, not floating UI panels. They use the same material vocabulary as the page — paper weight differences, score lines, inset materials, ink — never elevation, glow, or blur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. article-card
|
||||||
|
|
||||||
|
**Purpose:** An index card laid on the desk. Lists a post, pattern, or lesson with enough context to decide whether to pick it up and read.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ [PATTERN] 12 min read │ ← badge + meta row
|
||||||
|
│ │
|
||||||
|
│ Webpack Tree-Shaking Is Lying to You │ ← title
|
||||||
|
│ │
|
||||||
|
│ That bundle analyzer showing 100% tree-shaken? │ ← summary
|
||||||
|
│ The runtime tells a different story. Here's how to │
|
||||||
|
│ catch the lie and fix it. │
|
||||||
|
│ │
|
||||||
|
│ May 14, 2026 │ ← date
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anatomy
|
||||||
|
|
||||||
|
The card is a `<article>` element containing an `<a>` that wraps the entire card (the card IS the link). Internal structure is a vertical stack.
|
||||||
|
|
||||||
|
- **Container:** `max-width: var(--width-prose)` (720px). Full-bleed within the prose column.
|
||||||
|
- **Background:** `var(--surface-card)` — white card stock laid on the paper base.
|
||||||
|
- **Border:** `1px solid var(--border-subtle)` — a score line, not a frame.
|
||||||
|
- **Border radius:** `var(--rounded-lg)` (6px) — worn edges.
|
||||||
|
- **Shadow:** `var(--shadow-sm)` — barely perceptible. Card sitting on desk, not hovering above it.
|
||||||
|
- **Padding:** `var(--space-3)` (24px) all sides.
|
||||||
|
|
||||||
|
### Typography & Content
|
||||||
|
|
||||||
|
| Element | Font | Size | Weight | Color | Line-height |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Content type badge | `var(--font-structure)` | `var(--type-xs)` (12px) | 600 (semibold) | `var(--accent-primary)` | `var(--leading-normal)` (1.5) |
|
||||||
|
| Reading time | `var(--font-structure)` | `var(--type-xs)` (12px) | 400 | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) |
|
||||||
|
| Title | `var(--font-structure)` | `var(--type-xl)` (28px) | 600 (semibold) | `var(--text-primary)` | `var(--leading-snug)` (1.25) |
|
||||||
|
| Summary | `var(--font-prose)` | `var(--type-base)` (18px) | 400 | `var(--text-secondary)` | `var(--leading-prose)` (1.7) |
|
||||||
|
| Date | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) |
|
||||||
|
|
||||||
|
### Spacing (Internal)
|
||||||
|
|
||||||
|
| Between | Value | Token |
|
||||||
|
|---|---|---|
|
||||||
|
| Badge row → Title | `var(--space-1.5)` | 12px |
|
||||||
|
| Title → Summary | `var(--space-1)` | 8px |
|
||||||
|
| Summary → Date | `var(--space-2)` | 16px |
|
||||||
|
| Between cards (vertical stack) | `var(--space-2)` | 16px |
|
||||||
|
|
||||||
|
**Badge row:** Flexbox, `justify-content: space-between`, `align-items: center`. Badge left, reading time right.
|
||||||
|
|
||||||
|
**Badge styling:** Text only — uppercase via `text-transform: uppercase`, `letter-spacing: 0.05em`. No background pill, no border. The terracotta color IS the badge. Consistent with "nothing is decorative for its own sake."
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
| State | Change | Tokens |
|
||||||
|
|---|---|---|
|
||||||
|
| **Default** | As described above | — |
|
||||||
|
| **Hover** | Shadow deepens; title color shifts to accent | `var(--shadow-md)`, title → `var(--accent-primary)` |
|
||||||
|
| **Active (mousedown)** | Shadow returns to sm; subtle inward shift | `var(--shadow-sm)`, `transform: translateY(1px)` |
|
||||||
|
| **Focus-visible** | 2px outline in terracotta, 2px offset | `outline: 2px solid var(--focus-ring)`, `outline-offset: 2px` |
|
||||||
|
|
||||||
|
**Transition:** `transition: box-shadow var(--duration-fast) var(--ease-default), transform var(--duration-fast) var(--ease-default)`. Title color transition: `var(--duration-fast)`.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- Entire card is a single `<a>` wrapping a `<article>`. Screen readers announce the title as the link text.
|
||||||
|
- Badge text uses `aria-label` on the article to provide context: e.g., `aria-label="Pattern: Webpack Tree-Shaking Is Lying to You"`.
|
||||||
|
- Focus ring visible only on keyboard navigation (`:focus-visible`).
|
||||||
|
- Touch target: entire card surface (well above 44×44px minimum).
|
||||||
|
|
||||||
|
### Why this design
|
||||||
|
|
||||||
|
A card sitting on a desk has contact shadow, not drop shadow — `shadow-sm` at rest, `shadow-md` on hover is the full depth vocabulary of this system, and the card uses both ends of it because it's the most interactive surface on the index page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. code-block
|
||||||
|
|
||||||
|
**Purpose:** The evidence container. A different material embedded in the page — heavier paper, slightly recessed, with a terracotta accent marking where proof begins.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ filename/language label
|
||||||
|
│
|
||||||
|
│▌ src/bundler/tree-shake.ts │
|
||||||
|
│▌ │
|
||||||
|
│▌ export function analyzeExports(module: Module) { │
|
||||||
|
│▌ const used = new Set<string>(); │
|
||||||
|
│▌ for (const ref of module.references) { │
|
||||||
|
│▌ if (ref.type === 'import') { │
|
||||||
|
│▌ used.add(ref.name); │
|
||||||
|
│▌ } │
|
||||||
|
│▌ } │
|
||||||
|
│▌ return module.exports.filter(e => !used.has(e.name)); │
|
||||||
|
│▌ } │
|
||||||
|
│▌ │
|
||||||
|
│ │
|
||||||
|
▲
|
||||||
|
└── 3px terracotta accent line
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anatomy
|
||||||
|
|
||||||
|
The code block is a `<figure>` containing an optional `<figcaption>` (filename/language) and a `<pre><code>` block. It breaks wider than prose.
|
||||||
|
|
||||||
|
- **Container:** `max-width: var(--width-code)` (840px). Centered. When the viewport is narrower, it respects `var(--gutter)` (24px) side padding.
|
||||||
|
- **Background:** `var(--surface-code)` — heavier paper weight. The material shift IS the signal.
|
||||||
|
- **Border:** `1px solid var(--border-subtle)` top, right, and bottom. **Left border:** `3px solid var(--accent-primary)` — the terracotta accent line.
|
||||||
|
- **Border radius:** `var(--rounded-md)` (4px).
|
||||||
|
- **Shadow:** `none` — code blocks are inset, not elevated. They sit *in* the desk, not *on* it.
|
||||||
|
- **Padding:** `var(--space-3)` (24px) all sides. Left padding accounts for the 3px accent (so `calc(var(--space-3) - 3px)` on the left, or simply treat the border as outside the padding box).
|
||||||
|
- **Vertical margin:** `var(--space-5)` (40px) above and below — exhibited artifact spacing.
|
||||||
|
- **Overflow:** `overflow-x: auto` for horizontal scroll on long lines.
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
| Element | Font | Size | Weight | Color | Line-height |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Code text | `var(--font-evidence)` | 1rem (16px) | 400 | `var(--code-text)` | `var(--leading-relaxed)` (1.6) |
|
||||||
|
| Filename/language label | `var(--font-structure)` | `var(--type-xs)` (12px) | 500 (medium) | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) |
|
||||||
|
|
||||||
|
**Filename label:** Right-aligned within the code block, sitting at the top. Padding-bottom `var(--space-1)` (8px) separating it from code content. If no filename, no label — the code speaks for itself.
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
| State | Change | Tokens |
|
||||||
|
|---|---|---|
|
||||||
|
| **Default** | As described above | — |
|
||||||
|
| **Horizontal overflow** | Subtle fade-out on the right edge (CSS mask gradient) to indicate scrollable content. Scrollbar styled warm. | — |
|
||||||
|
| **Focus (keyboard tab into scrollable region)** | `outline: 2px solid var(--focus-ring)`, `outline-offset: -2px` (inset, since the block is inset) | `var(--focus-ring)` |
|
||||||
|
|
||||||
|
**Scrollbar styling (where supported):**
|
||||||
|
- Track: `var(--surface-code)` (invisible against background)
|
||||||
|
- Thumb: `var(--border-default)` — warm, quiet
|
||||||
|
- Thumb hover: `var(--text-tertiary)` — slightly more visible
|
||||||
|
- Width: 6px, border-radius: `var(--rounded-full)`
|
||||||
|
|
||||||
|
### Syntax Highlighting Palette Direction
|
||||||
|
|
||||||
|
The syntax theme should stay within the warm vocabulary. No neon, no saturated primaries.
|
||||||
|
|
||||||
|
| Token type | Color direction | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Keywords | `var(--accent-primary)` (terracotta) | Control flow, declarations |
|
||||||
|
| Strings | `var(--accent-secondary)` (tarnished brass) | Literal values |
|
||||||
|
| Comments | `var(--text-tertiary)` | Quiet, receding |
|
||||||
|
| Functions/methods | `var(--text-primary)` | Bold weight (600) for emphasis, not color |
|
||||||
|
| Types/classes | `var(--accent-primary)` at 80% opacity | Slightly softer than keywords |
|
||||||
|
| Numbers | `var(--accent-secondary)` | Groups with strings as "literal values" |
|
||||||
|
| Punctuation | `var(--text-secondary)` | Present but not prominent |
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- `<pre>` has `tabindex="0"` when content overflows horizontally, making it keyboard-scrollable.
|
||||||
|
- `role="region"` with `aria-label` describing the code (e.g., `aria-label="Code: src/bundler/tree-shake.ts"`).
|
||||||
|
- Filename in `<figcaption>` is programmatically associated via `aria-labelledby`.
|
||||||
|
- Syntax highlighting uses color + context (not color alone) — keywords are identifiable by position, not just hue.
|
||||||
|
- All syntax colors maintain minimum 4.5:1 contrast on `surface-code`.
|
||||||
|
|
||||||
|
### Why this design
|
||||||
|
|
||||||
|
The narrow→wide→narrow rhythm as you scroll through a post mirrors the register shift between narrative and evidence — your eye physically moves to a wider surface when the voice changes from serif prose to mono proof, then returns to the narrower reading column.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. audio-player
|
||||||
|
|
||||||
|
**Purpose:** A companion to reading, not a separate interface. Like a sticky note near the top of the page: "You can also listen to this."
|
||||||
|
|
||||||
|
### Layout — Collapsed (Default)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ ▶ Listen · 8 min │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
A pill. Inline with prose flow, sitting at the top of the article just below the post-header.
|
||||||
|
|
||||||
|
- **Container:** Inline-flex pill. `border-radius: var(--rounded-full)` (9999px).
|
||||||
|
- **Background:** `var(--surface-inset)` — slightly recessed, like a different stock weight.
|
||||||
|
- **Border:** `1px solid var(--border-subtle)`.
|
||||||
|
- **Padding:** `var(--space-0.5)` (4px) vertical, `var(--space-2)` (16px) horizontal.
|
||||||
|
- **Shadow:** `none` — it's inset, resting.
|
||||||
|
- **Height:** Determined by content. Approximately 36px (above 44px touch target when accounting for the click area — see accessibility note).
|
||||||
|
|
||||||
|
| Element | Font | Size | Weight | Color |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Play icon (▶) | — | 14px (optical) | — | `var(--accent-primary)` |
|
||||||
|
| "Listen" label | `var(--font-structure)` | `var(--type-sm)` (14px) | 500 (medium) | `var(--text-secondary)` |
|
||||||
|
| Duration | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-tertiary)` |
|
||||||
|
|
||||||
|
**Internal spacing:** `var(--space-1)` (8px) between icon and "Listen", `var(--space-0.5)` (4px) between "Listen" and `·` separator, same before duration.
|
||||||
|
|
||||||
|
### Layout — Expanded (Playing/Paused)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ ❚❚ ━━━━━━━━━━━━━━━━━━━━━━━━━●━━━━━━━ 2:14 / 8:02 │
|
||||||
|
│ ▲ progress bar 1× │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Expands to `max-width: var(--width-prose)` (720px). Same pill shape gives way to a rounded-rectangle.
|
||||||
|
|
||||||
|
- **Container:** `max-width: var(--width-prose)`. `border-radius: var(--rounded-lg)` (6px).
|
||||||
|
- **Background:** `var(--surface-inset)`.
|
||||||
|
- **Border:** `1px solid var(--border-subtle)`.
|
||||||
|
- **Padding:** `var(--space-1.5)` (12px) vertical, `var(--space-2)` (16px) horizontal.
|
||||||
|
- **Layout:** Two rows.
|
||||||
|
- **Row 1:** Play/pause button | Progress bar | Time display. Flexbox, `align-items: center`.
|
||||||
|
- **Row 2:** Right-aligned speed control. Only visible in expanded state.
|
||||||
|
|
||||||
|
| Element | Font | Size | Weight | Color |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Play/pause icon | — | 18px (optical) | — | `var(--accent-primary)` |
|
||||||
|
| Time (current / total) | `var(--font-evidence)` | `var(--type-xs)` (12px) | 400 | `var(--text-tertiary)` |
|
||||||
|
| Speed button ("1×") | `var(--font-evidence)` | `var(--type-xs)` (12px) | 400 | `var(--text-secondary)` |
|
||||||
|
|
||||||
|
**Progress bar:**
|
||||||
|
- Track: `var(--border-subtle)`, height 3px, `border-radius: var(--rounded-full)`.
|
||||||
|
- Fill (elapsed): `var(--accent-primary)` (terracotta).
|
||||||
|
- Scrubber knob: 12px circle, `var(--accent-primary)`, `border: 2px solid var(--surface-inset)`. Only visible on hover/focus.
|
||||||
|
- Track clickable height: 24px (transparent hit area above 44px minimum on mobile with padding).
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
|
||||||
|
| Between | Value | Token |
|
||||||
|
|---|---|---|
|
||||||
|
| Play/pause → progress bar | `var(--space-1.5)` | 12px |
|
||||||
|
| Progress bar → time | `var(--space-1.5)` | 12px |
|
||||||
|
| Row 1 → Row 2 | `var(--space-0.5)` | 4px |
|
||||||
|
| Player → first prose paragraph | `var(--space-4)` | 32px |
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
| State | Change |
|
||||||
|
|---|---|
|
||||||
|
| **Collapsed (default)** | Pill form as described. Click anywhere to expand and play. |
|
||||||
|
| **Collapsed hover** | Background shifts to `var(--surface-code)` — slightly warmer. Cursor pointer. |
|
||||||
|
| **Expanded — playing** | Pause icon shown. Progress bar animates. Time updates. |
|
||||||
|
| **Expanded — paused** | Play icon shown. Progress bar static. |
|
||||||
|
| **Speed button hover** | Color shifts to `var(--accent-primary)`. |
|
||||||
|
| **Speed button active** | Cycles: 1× → 1.25× → 1.5× → 2× → 1×. |
|
||||||
|
| **Focus-visible (any interactive element)** | `outline: 2px solid var(--focus-ring)`, `outline-offset: 2px`. |
|
||||||
|
| **Loading** | Collapsed pill shows "Loading…" in place of duration. Subtle opacity pulse on the label (opacity 0.6→1.0, `var(--duration-slow)` cycle). |
|
||||||
|
| **Error** | Pill shows "Unavailable" in `var(--text-tertiary)`. Play icon replaced with a dash. Not clickable. |
|
||||||
|
|
||||||
|
**Expand/collapse transition:** Height and border-radius animate over `var(--duration-slow)` (250ms) with `var(--ease-in-out)`. This is the one layout animation in the system — justified because the shape change is meaningful (compact → full controls).
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- Play/pause is a `<button>` with `aria-label="Play audio version"` / `aria-label="Pause audio"`.
|
||||||
|
- Progress bar is `<input type="range">` with `aria-label="Audio progress"`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext="2 minutes 14 seconds of 8 minutes 2 seconds"`.
|
||||||
|
- Speed button is a `<button>` with `aria-label="Playback speed: 1x"`.
|
||||||
|
- Collapsed pill is a `<button>` with `aria-label="Listen to audio version, 8 minutes"`.
|
||||||
|
- Minimum touch target: 44×44px on all interactive elements (play/pause button padded to meet this; progress bar hit area expanded).
|
||||||
|
|
||||||
|
### Why this design
|
||||||
|
|
||||||
|
The player starts as a pill because it's an offer, not a demand. It earns its space only when activated. The inset surface and quiet typography say "I'm part of this page" — integrated like a tool hanging on the pegboard, not bolted on like an afterthought.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. callout
|
||||||
|
|
||||||
|
**Purpose:** A sticky note on the workbench. Interrupts prose flow to surface a pattern, anti-pattern, warning, or note — then returns you to reading.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────┐
|
||||||
|
▐ PATTERN │
|
||||||
|
▐ │
|
||||||
|
▐ The narrow-wide-narrow rhythm isn't just aesthetic — │
|
||||||
|
▐ it signals a register change. When the reader's eye │
|
||||||
|
▐ hits wider content, they unconsciously shift into │
|
||||||
|
▐ "evidence evaluation" mode. │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anatomy
|
||||||
|
|
||||||
|
An `<aside>` element within the prose column. Left-border accent with inset surface.
|
||||||
|
|
||||||
|
- **Container:** `max-width: var(--width-prose)` (720px). Full-bleed within prose column.
|
||||||
|
- **Background:** `var(--surface-inset)` — recessed surface, different stock weight.
|
||||||
|
- **Border:** `1px solid var(--border-subtle)` top, right, bottom. **Left border:** `3px solid` — color varies by type (see below).
|
||||||
|
- **Border radius:** `var(--rounded-lg)` (6px).
|
||||||
|
- **Padding:** `var(--space-3)` (24px) all sides.
|
||||||
|
- **Vertical margin:** `var(--space-4)` (32px) above and below.
|
||||||
|
- **Shadow:** `none` — it's inset, like the code block.
|
||||||
|
|
||||||
|
### Callout Types & Left Border Colors
|
||||||
|
|
||||||
|
| Type | Left border color | Label color | Rationale |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Note** (default) | `var(--border-default)` | `var(--text-secondary)` | Neutral — just a sidebar thought. |
|
||||||
|
| **Pattern** | `var(--accent-primary)` (terracotta) | `var(--accent-primary)` | Terracotta = emphasis, the primary accent. A pattern is something to adopt. |
|
||||||
|
| **Anti-pattern** | `var(--error-border)` | `var(--error-text)` | Clay-red signals caution without alarm. Something to avoid. |
|
||||||
|
| **Warning** | `var(--warning-border)` | `var(--warning-text)` | Amber/brass for "proceed carefully." |
|
||||||
|
| **Tip** | `var(--accent-secondary)` (tarnished brass) | `var(--accent-secondary)` | Brass = secondary highlight. A useful but optional insight. |
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
| Element | Font | Size | Weight | Color | Line-height |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Label (e.g., "PATTERN") | `var(--font-structure)` | `var(--type-xs)` (12px) | 600 (semibold) | Varies by type (see above) | `var(--leading-normal)` (1.5) |
|
||||||
|
| Body text | `var(--font-prose)` | `var(--type-base)` (18px) | 400 | `var(--text-primary)` | `var(--leading-prose)` (1.7) |
|
||||||
|
|
||||||
|
**Label styling:** `text-transform: uppercase`, `letter-spacing: 0.05em`. Same treatment as the article-card badge — the system has one way to do labels.
|
||||||
|
|
||||||
|
### Spacing (Internal)
|
||||||
|
|
||||||
|
| Between | Value | Token |
|
||||||
|
|---|---|---|
|
||||||
|
| Label → body text | `var(--space-1)` | 8px |
|
||||||
|
| Between paragraphs in body | `var(--space-3)` | 24px (standard paragraph gap) |
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
Callouts are static content — no interactive states. They are read, not clicked.
|
||||||
|
|
||||||
|
If the callout contains links, those links follow standard prose link styling: `var(--accent-primary)` text, underline, hover → `var(--accent-primary-hover)`.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- `<aside>` element with `role="note"` (or `role="complementary"` if substantial).
|
||||||
|
- `aria-label` includes the callout type: e.g., `aria-label="Pattern callout"`.
|
||||||
|
- Label text is decorative/redundant with aria-label — can be `aria-hidden="true"` to avoid double-announcement.
|
||||||
|
- Body text contrast: `var(--text-primary)` on `var(--surface-inset)` = 11.93:1 (AAA). No issues.
|
||||||
|
- Left border color is not the sole information carrier — the label text also identifies the callout type.
|
||||||
|
|
||||||
|
### Why this design
|
||||||
|
|
||||||
|
The left-border accent mirrors the code block's terracotta edge — creating a consistent visual grammar for "pay attention, this is a different voice." The inset surface says "this is set into the page" while the border color says "here's what kind of aside this is." Two signals, zero decoration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. post-header
|
||||||
|
|
||||||
|
**Purpose:** Establishes the article's identity. The writing is the hero — the header frames it with just enough structure to orient the reader, then gets out of the way.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
|
||||||
|
Webpack Tree-Shaking Is Lying to You
|
||||||
|
|
||||||
|
May 14, 2026 · 12 min read
|
||||||
|
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ ▶ Listen · 8 min │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Centered within the prose column. Vertical stack, center-aligned.
|
||||||
|
|
||||||
|
### Anatomy
|
||||||
|
|
||||||
|
A `<header>` element at the top of the article page. All content centered.
|
||||||
|
|
||||||
|
- **Container:** `max-width: var(--width-prose)` (720px). Center-aligned.
|
||||||
|
- **Background:** Transparent — it lives on `var(--surface-base)`, the page itself.
|
||||||
|
- **No border, no shadow.** The header is typography, not a container.
|
||||||
|
- **Top padding:** `var(--space-8)` (64px) from the nav-bar. Breathing room.
|
||||||
|
- **Bottom margin:** `var(--space-8)` (64px) before the first prose paragraph. A chapter-break-scale pause.
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
| Element | Font | Size | Weight | Color | Line-height | Alignment |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Content type badge | `var(--font-structure)` | `var(--type-xs)` (12px) | 600 (semibold) | `var(--accent-primary)` | `var(--leading-normal)` (1.5) | Center |
|
||||||
|
| Title (h1) | `var(--font-structure)` | `var(--type-3xl)` (44px) | 700 (bold) | `var(--text-primary)` | `var(--leading-tight)` (1.15) | Center |
|
||||||
|
| Meta line (date + reading time) | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-tertiary)` | `var(--leading-normal)` (1.5) | Center |
|
||||||
|
|
||||||
|
**Title:** `letter-spacing: -0.02em` — negative tracking at display size, per the type system spec. Max 3 lines before it feels wrong — titles should be concise.
|
||||||
|
|
||||||
|
**Badge:** Same `text-transform: uppercase`, `letter-spacing: 0.05em` treatment as article-card and callout. System-wide label pattern.
|
||||||
|
|
||||||
|
**Meta line:** Date and reading time separated by ` · ` (space-middot-space). The middot is `var(--text-tertiary)`. Both in the structure voice at caption weight.
|
||||||
|
|
||||||
|
### Spacing (Internal)
|
||||||
|
|
||||||
|
| Between | Value | Token |
|
||||||
|
|---|---|---|
|
||||||
|
| Badge → Title | `var(--space-2)` | 16px |
|
||||||
|
| Title → Meta line | `var(--space-2)` | 16px |
|
||||||
|
| Meta line → Audio pill (if present) | `var(--space-3)` | 24px |
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
The post-header is static. No interactive states on the header itself.
|
||||||
|
|
||||||
|
- The **badge** is not a link (unlike on index pages — here you're already on the post).
|
||||||
|
- The **audio pill** has its own interactive states (see audio-player component).
|
||||||
|
- The **date** could optionally link to an archive, using standard link treatment: `var(--accent-primary)`, underline on hover.
|
||||||
|
|
||||||
|
### Responsive Behavior
|
||||||
|
|
||||||
|
| Viewport | Title size | Top padding | Bottom margin |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ≥ 720px | `var(--type-3xl)` (44px) | `var(--space-8)` (64px) | `var(--space-8)` (64px) |
|
||||||
|
| < 720px | `var(--type-2xl)` (36px) | `var(--space-6)` (48px) | `var(--space-6)` (48px) |
|
||||||
|
| < 480px | `var(--type-xl)` (28px) | `var(--space-5)` (40px) | `var(--space-5)` (40px) |
|
||||||
|
|
||||||
|
The title steps down the type scale at narrow viewports. Line-height adjusts proportionally: 1.15 → 1.25 → 1.25. Everything else stays the same — the hierarchy is preserved, just the title scale adapts.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- `<header>` with `role="banner"` scoped to the article (not the page — the nav has page-level banner).
|
||||||
|
- Title is `<h1>` — the only h1 on the page.
|
||||||
|
- Badge is `<span>` (not heading). Associated with the h1 via proximity, not ARIA — screen readers will encounter it naturally before the title.
|
||||||
|
- Meta content uses `<time datetime="2026-05-14">` for machine-readable dates.
|
||||||
|
|
||||||
|
### Why this design
|
||||||
|
|
||||||
|
No hero image, no gradient, no illustration. The h1 at 44px Inter Bold IS the visual weight of the page. The centered stack with generous vertical breathing creates the feeling of opening a well-typeset book to a chapter page — the title, the metadata, then silence before the prose begins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. nav-bar
|
||||||
|
|
||||||
|
**Purpose:** Present but not demanding. A quiet shelf at the top of the workshop wall — you know where the tools are, but they don't shout at you while you're reading.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
Crash Test Dev Posts Patterns Lessons
|
||||||
|
──────────────────────────────────────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
A single row. Site name left, content type links right. Bottom border separates nav from page content.
|
||||||
|
|
||||||
|
### Anatomy
|
||||||
|
|
||||||
|
A `<nav>` element containing the site name and navigation links.
|
||||||
|
|
||||||
|
- **Container:** `max-width: var(--width-container)` (1100px). Centered. Horizontal padding: `var(--gutter)` (24px).
|
||||||
|
- **Background:** `var(--surface-base)` — the same as the page. The nav doesn't have its own surface. It's part of the desk.
|
||||||
|
- **Border bottom:** `1px solid var(--border-subtle)` — a score line marking where navigation ends and content begins.
|
||||||
|
- **Height:** Determined by content + padding. Approximately 56px.
|
||||||
|
- **Padding:** `var(--space-2)` (16px) vertical.
|
||||||
|
- **Position:** `position: static`. The nav scrolls with the page. On a reading site, you don't need persistent navigation — you need an uninterrupted reading column. (If later analysis shows readers frequently nav-jump, this could become `sticky` with `top: 0` and `background: var(--surface-base)` — but start static.)
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
| Element | Font | Size | Weight | Color | Letter-spacing |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Site name | `var(--font-structure)` | `var(--type-base)` (18px) | 600 (semibold) | `var(--text-primary)` | `-0.01em` |
|
||||||
|
| Nav links | `var(--font-structure)` | `var(--type-sm)` (14px) | 400 | `var(--text-secondary)` | `0` |
|
||||||
|
| Active nav link | `var(--font-structure)` | `var(--type-sm)` (14px) | 500 (medium) | `var(--text-primary)` | `0` |
|
||||||
|
|
||||||
|
**Site name:** Not a logo, not display type. 18px Inter Semibold — the same size as body text but in the structure voice. Confident without being loud. It's a nameplate on the workbench, not a sign above the shop.
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
|
||||||
|
| Between | Value | Token |
|
||||||
|
|---|---|---|
|
||||||
|
| Site name ↔ nav links | Flexbox `justify-content: space-between` | — |
|
||||||
|
| Between nav links | `var(--space-4)` (32px) | Enough air to feel unhurried |
|
||||||
|
| Nav bottom border → page content | `var(--space-6)` (48px) on index pages, handled by page layout | — |
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
| State | Element | Change | Tokens |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Default** | Nav links | `var(--text-secondary)`, no underline | — |
|
||||||
|
| **Hover** | Nav links | Color → `var(--text-primary)` | `transition: color var(--duration-fast)` |
|
||||||
|
| **Active (current page)** | Nav link | Color → `var(--text-primary)`, weight → 500 (medium). Underline: `2px solid var(--accent-primary)`, offset `6px` below text | `text-underline-offset: 6px` |
|
||||||
|
| **Focus-visible** | Any link | `outline: 2px solid var(--focus-ring)`, `outline-offset: 2px` | `var(--focus-ring)` |
|
||||||
|
| **Site name hover** | Site name | Color → `var(--accent-primary)` | `transition: color var(--duration-fast)` |
|
||||||
|
|
||||||
|
**Active link underline:** A 2px terracotta line sitting 6px below the text baseline. This is the one place in the nav where accent color appears — a quiet "you are here" without a highlight pill, active tab, or any other noisy indicator.
|
||||||
|
|
||||||
|
### Responsive Behavior
|
||||||
|
|
||||||
|
| Viewport | Layout | Changes |
|
||||||
|
|---|---|---|
|
||||||
|
| ≥ 600px | Row: name left, links right | As described |
|
||||||
|
| < 600px | Stack: name centered on top, links centered below | Name and links each center-aligned. Gap between: `var(--space-1)` (8px). Links in a single row, gap reduced to `var(--space-3)` (24px). |
|
||||||
|
|
||||||
|
No hamburger menu. Three links don't need one. A hamburger would be over-engineering — the anti-pattern of hiding three items behind a tap. If the site grows to need one, revisit then.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- `<nav>` with `aria-label="Site navigation"`.
|
||||||
|
- Current page link has `aria-current="page"`.
|
||||||
|
- Site name is an `<a href="/">` — always links home.
|
||||||
|
- All links have minimum touch target height of 44px (achieved via padding on the nav container and line-height on links).
|
||||||
|
- Skip-to-content link: A visually hidden `<a href="#main-content">Skip to content</a>` as the first child of `<body>`, appearing on focus. Styled: `var(--surface-card)` background, `var(--text-primary)` text, `var(--space-1)` padding, absolutely positioned at top-left on focus.
|
||||||
|
|
||||||
|
### Why this design
|
||||||
|
|
||||||
|
The nav earns its restraint from the aesthetic guide's "not personal brand" directive. No logo, no color accent in the name, no visual weight competing with the prose below. The score line at the bottom is the thinnest possible assertion: "navigation is above this line, content is below." Three words, three links, one line. Done.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Component Patterns
|
||||||
|
|
||||||
|
### The Label System
|
||||||
|
|
||||||
|
Three components share an identical label treatment — article-card badge, callout label, and post-header badge:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.label {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-xs); /* 12px */
|
||||||
|
font-weight: 600; /* semibold */
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
line-height: var(--leading-normal); /* 1.5 */
|
||||||
|
/* Color varies by context */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is intentional. One system for labeling content types. Recognize it once, understand it everywhere.
|
||||||
|
|
||||||
|
### The Inset Surface Grammar
|
||||||
|
|
||||||
|
Three components use the "different material" pattern — code-block, callout, and audio-player:
|
||||||
|
|
||||||
|
| Component | Surface | Left accent | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| code-block | `var(--surface-code)` | `3px var(--accent-primary)` | "This is evidence" |
|
||||||
|
| callout | `var(--surface-inset)` | `3px` varies by type | "This is an aside" |
|
||||||
|
| audio-player | `var(--surface-inset)` | none | "This is a tool" |
|
||||||
|
|
||||||
|
All three are inset (no shadow), all three use a background shift to signal "different material," and the code-block and callout share the left-accent pattern. The audio player omits the accent because it's interactive, not annotative.
|
||||||
|
|
||||||
|
### The Depth Vocabulary (Complete)
|
||||||
|
|
||||||
|
The entire system uses exactly these depth levels:
|
||||||
|
|
||||||
|
| Level | Shadow | Surface | Used by |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Page** | none | `var(--surface-base)` | Page background, nav-bar, post-header |
|
||||||
|
| **Card** | `var(--shadow-sm)` | `var(--surface-card)` | article-card |
|
||||||
|
| **Card (hover)** | `var(--shadow-md)` | `var(--surface-card)` | article-card on hover |
|
||||||
|
| **Inset** | none | `var(--surface-inset)` | callout, audio-player |
|
||||||
|
| **Inset (code)** | none | `var(--surface-code)` | code-block |
|
||||||
|
|
||||||
|
Nothing floats. The maximum depth in the system is `shadow-md` on a hovered card. That's it.
|
||||||
|
|
||||||
|
### Motion Budget
|
||||||
|
|
||||||
|
| Transition | Duration | Easing | Property |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Link/button color changes | `var(--duration-fast)` (100ms) | `var(--ease-default)` | `color` |
|
||||||
|
| Card shadow on hover | `var(--duration-fast)` (100ms) | `var(--ease-default)` | `box-shadow` |
|
||||||
|
| Card press | `var(--duration-fast)` (100ms) | `var(--ease-default)` | `transform` |
|
||||||
|
| Audio player expand/collapse | `var(--duration-slow)` (250ms) | `var(--ease-in-out)` | `height`, `border-radius` |
|
||||||
|
| Audio progress bar scrubber appear | `var(--duration-normal)` (150ms) | `var(--ease-default)` | `opacity` |
|
||||||
|
|
||||||
|
All set to `0ms` under `prefers-reduced-motion: reduce`. Non-negotiable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
Recommended build order based on dependencies and page coverage:
|
||||||
|
|
||||||
|
1. **nav-bar** — present on every page, frames everything else
|
||||||
|
2. **post-header** — top of every article, depends on the label system
|
||||||
|
3. **code-block** — the evidence container, used in every technical post
|
||||||
|
4. **callout** — aside blocks within prose, shares grammar with code-block
|
||||||
|
5. **article-card** — index pages, the only elevated surface in the system
|
||||||
|
6. **audio-player** — optional per-post feature, most complex interactive states
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- **Token traceability:** Every color, size, space, radius, shadow, and duration in the implemented CSS traces to a named custom property. Zero magic numbers.
|
||||||
|
- **Aesthetic coherence:** A page using all six components should feel like one material surface with variations in weight and purpose — not six different "components."
|
||||||
|
- **Quiet confidence:** If someone inspects the page and thinks "there's not much design here," the design succeeded. The craft is in what's NOT there.
|
||||||
|
- **Five-year test:** Nothing in these specs will look dated in 2031. Paper, ink, and careful spacing don't go out of style.
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
---
|
||||||
|
feature: DesignTokenSystem
|
||||||
|
date: 2026-05-26
|
||||||
|
status: planned
|
||||||
|
project: crash-test-dev
|
||||||
|
tags: [tokens, system, color, typography, spacing, layout]
|
||||||
|
related: [AESTHETIC-GUIDE.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Design Token System — Crash Test Dev
|
||||||
|
|
||||||
|
## User's Spark (preserved)
|
||||||
|
|
||||||
|
> A well-used workshop. Not a pristine showroom — the one where someone actually builds things.
|
||||||
|
> Tools are organized because disorganization wastes time. The wood has warmth.
|
||||||
|
> The light is good because you need to see details. There's a coffee cup somewhere.
|
||||||
|
|
||||||
|
**Metaphor:** The workbench, not the gallery.
|
||||||
|
**Materials vocabulary:** Paper, ink, wood, stone, iron.
|
||||||
|
**Personality:** Dry warmth. Quiet confidence. Evidence over performance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Token System
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"colors": {
|
||||||
|
"_meta": {
|
||||||
|
"temperature": "warm across all values — no blue-grays, no cool neutrals",
|
||||||
|
"philosophy": "Aged paper, brown-black ink, fired earth, tarnished brass. Every color drawn from the workbench vocabulary: materials that have been used, heated, and cooled."
|
||||||
|
},
|
||||||
|
|
||||||
|
"surface": {
|
||||||
|
"base": { "value": "#F5F0E8", "description": "Page background. Heavy unbleached paper stock — warm off-white, not pure white, not yellow." },
|
||||||
|
"card": { "value": "#FEFCF7", "description": "Reading surface. White card stock laid on the paper base — for article bodies, elevated panels." },
|
||||||
|
"code": { "value": "#EEEAE2", "description": "Code block background. Heavier paper weight — subtle warm-gray shift from prose background." },
|
||||||
|
"inset": { "value": "#E8E3DA", "description": "Callout/aside background. Slightly recessed surface, like a different stock weight." }
|
||||||
|
},
|
||||||
|
|
||||||
|
"text": {
|
||||||
|
"primary": { "value": "#2B2421", "description": "Body text. Warm near-black — brown-black ink, NOT blue-black digital default.", "contrastOnBase": "13.44:1 (AAA)", "contrastOnCard": "14.87:1 (AAA)" },
|
||||||
|
"secondary": { "value": "#6B5F55", "description": "Metadata, captions, bylines. Warm mid-gray with stone undertone.", "contrastOnBase": "5.46:1 (AA)", "contrastOnCard": "6.04:1 (AA)" },
|
||||||
|
"tertiary": { "value": "#948880", "description": "Timestamps, tags, disabled text. Lighter warm gray.", "contrastOnBase": "3.04:1 (AA-UI)", "contrastOnCard": "3.36:1 (AA-UI)" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"accent": {
|
||||||
|
"primary": { "value": "#A0522D", "description": "Links, emphasis, code block left-edge accent. Muted terracotta/sienna — fired earth, not bright red.", "contrastOnBase": "4.95:1 (AA)", "contrastOnCard": "5.48:1 (AA)" },
|
||||||
|
"primaryHover": { "value": "#8B4726", "description": "Link hover state. Darker rust — the terracotta after more firing.", "contrastOnBase": "6.12:1 (AA)" },
|
||||||
|
"secondary": { "value": "#7D6C2F", "description": "Special callouts, highlights. Tarnished brass/dark gold — aged metal.", "contrastOnBase": "4.56:1 (AA)", "contrastOnCard": "5.05:1 (AA)" },
|
||||||
|
"secondaryHover": { "value": "#6B5C28", "description": "Brass hover state. Darker, more patinated." }
|
||||||
|
},
|
||||||
|
|
||||||
|
"code": {
|
||||||
|
"text": { "value": "#3D342E", "description": "Code text. Warm dark brown, slightly lighter than body text for the different register.", "contrastOnCode": "10.12:1 (AAA)" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"border": {
|
||||||
|
"subtle": { "value": "#DDD7CD", "description": "Decorative score lines, faint dividers. Barely there — like a fold in paper. No contrast requirement (decorative)." },
|
||||||
|
"default": { "value": "#C2BAA9", "description": "General-purpose separators, card edges. Visible but quiet." },
|
||||||
|
"strong": { "value": "#8E857A", "description": "Functional UI borders (form inputs, focused elements). Meets 3:1 on both base and card.", "contrastOnBase": "3.20:1 (AA-UI)", "contrastOnCard": "3.54:1 (AA-UI)" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"focus": {
|
||||||
|
"ring": { "value": "#A0522D", "description": "Keyboard focus indicator. Terracotta — visible, warm, consistent with accent.", "contrastOnBase": "4.95:1 (AA-UI)" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"state": {
|
||||||
|
"success": {
|
||||||
|
"bg": { "value": "#E8EDDF", "description": "Warm sage green tint." },
|
||||||
|
"text": { "value": "#4A5E3A", "description": "Dark moss green.", "contrastOnBg": "5.96:1 (AA)" },
|
||||||
|
"border": { "value": "#B5C4A0", "description": "Muted sage border." }
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"bg": { "value": "#F2E8D0", "description": "Warm amber tint." },
|
||||||
|
"text": { "value": "#7A5C1F", "description": "Dark warm amber.", "contrastOnBg": "5.10:1 (AA)" },
|
||||||
|
"border": { "value": "#D4BC7C", "description": "Muted gold border." }
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"bg": { "value": "#F2DED6", "description": "Warm rose/clay tint." },
|
||||||
|
"text": { "value": "#8B3A2A", "description": "Dark terracotta.", "contrastOnBg": "5.92:1 (AA)" },
|
||||||
|
"border": { "value": "#CFA090", "description": "Muted clay border." }
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"bg": { "value": "#E5E2DA", "description": "Warm neutral tint." },
|
||||||
|
"text": { "value": "#4A4540", "description": "Warm dark gray.", "contrastOnBg": "7.32:1 (AAA)" },
|
||||||
|
"border": { "value": "#B8B2A8", "description": "Warm mid gray border." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"shadow": {
|
||||||
|
"color": { "value": "rgba(43, 36, 33, 0.06)", "description": "Warm-toned shadow base. Derived from text-primary. Barely-there desk shadow." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"typography": {
|
||||||
|
"_meta": {
|
||||||
|
"philosophy": "Three voices, three jobs. Serif for prose (Ken's voice). Sans for structure (navigation, headings). Mono for evidence (code, proof). The reader always knows which voice is speaking.",
|
||||||
|
"scaleRatio": 1.25,
|
||||||
|
"scaleName": "Major Third",
|
||||||
|
"scaleBase": "18px"
|
||||||
|
},
|
||||||
|
|
||||||
|
"families": {
|
||||||
|
"prose": { "value": "'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif", "role": "Reading voice — body text, blockquotes, long-form prose" },
|
||||||
|
"structure": { "value": "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", "role": "Navigation voice — headings, UI labels, metadata" },
|
||||||
|
"evidence": { "value": "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace", "role": "Proof voice — code blocks, terminal output, file paths" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"scale": {
|
||||||
|
"xs": { "value": "0.75rem", "px": 12, "step": -2, "use": "footnotes, legal text" },
|
||||||
|
"sm": { "value": "0.875rem", "px": 14, "step": -1, "use": "captions, meta, timestamps, tags" },
|
||||||
|
"base": { "value": "1.125rem", "px": 18, "step": 0, "use": "body prose" },
|
||||||
|
"lg": { "value": "1.375rem", "px": 22, "step": 1, "use": "h4 headings" },
|
||||||
|
"xl": { "value": "1.75rem", "px": 28, "step": 2, "use": "h3 headings" },
|
||||||
|
"2xl": { "value": "2.25rem", "px": 36, "step": 3, "use": "h2 headings — section titles" },
|
||||||
|
"3xl": { "value": "2.75rem", "px": 44, "step": 4, "use": "h1 headings — article title" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"lineHeight": {
|
||||||
|
"tight": { "value": 1.15, "use": "display headings (h1)" },
|
||||||
|
"snug": { "value": 1.25, "use": "headings (h2, h3)" },
|
||||||
|
"heading": { "value": 1.35, "use": "smaller headings (h4)" },
|
||||||
|
"normal": { "value": 1.5, "use": "UI text, captions" },
|
||||||
|
"relaxed": { "value": 1.6, "use": "code blocks, blockquotes" },
|
||||||
|
"prose": { "value": 1.7, "use": "body prose — generous for dense technical narrative" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"weight": {
|
||||||
|
"regular": { "value": 400, "use": "body prose, code" },
|
||||||
|
"medium": { "value": 500, "use": "UI labels, caption emphasis" },
|
||||||
|
"semibold": { "value": 600, "use": "headings, inline emphasis in prose" },
|
||||||
|
"bold": { "value": 700, "use": "h1 title, strong emphasis" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"styles": {
|
||||||
|
"body": {
|
||||||
|
"fontFamily": "prose",
|
||||||
|
"fontSize": "1.125rem",
|
||||||
|
"fontWeight": 400,
|
||||||
|
"lineHeight": 1.7,
|
||||||
|
"letterSpacing": "normal",
|
||||||
|
"description": "Ken's voice on the page. 18px Source Serif 4 at generous line-height for sustained technical reading."
|
||||||
|
},
|
||||||
|
"bodySmall": {
|
||||||
|
"fontFamily": "prose",
|
||||||
|
"fontSize": "0.875rem",
|
||||||
|
"fontWeight": 400,
|
||||||
|
"lineHeight": 1.6,
|
||||||
|
"letterSpacing": "0.005em",
|
||||||
|
"description": "Footnotes, secondary prose passages. Still serif — still Ken's voice, just quieter."
|
||||||
|
},
|
||||||
|
"h1": {
|
||||||
|
"fontFamily": "structure",
|
||||||
|
"fontSize": "2.75rem",
|
||||||
|
"fontWeight": 700,
|
||||||
|
"lineHeight": 1.15,
|
||||||
|
"letterSpacing": "-0.02em",
|
||||||
|
"description": "Article title. 44px Inter Bold — confident, unhurried. Negative tracking tightens at display size."
|
||||||
|
},
|
||||||
|
"h2": {
|
||||||
|
"fontFamily": "structure",
|
||||||
|
"fontSize": "2.25rem",
|
||||||
|
"fontWeight": 600,
|
||||||
|
"lineHeight": 1.25,
|
||||||
|
"letterSpacing": "-0.015em",
|
||||||
|
"description": "Section heading. 36px Inter Semibold — chapter breaks in the narrative."
|
||||||
|
},
|
||||||
|
"h3": {
|
||||||
|
"fontFamily": "structure",
|
||||||
|
"fontSize": "1.75rem",
|
||||||
|
"fontWeight": 600,
|
||||||
|
"lineHeight": 1.3,
|
||||||
|
"letterSpacing": "-0.01em",
|
||||||
|
"description": "Subsection heading. 28px Inter Semibold — organizes without competing with prose."
|
||||||
|
},
|
||||||
|
"h4": {
|
||||||
|
"fontFamily": "structure",
|
||||||
|
"fontSize": "1.375rem",
|
||||||
|
"fontWeight": 600,
|
||||||
|
"lineHeight": 1.35,
|
||||||
|
"letterSpacing": "-0.005em",
|
||||||
|
"description": "Minor heading. 22px Inter Semibold — just enough hierarchy shift to signal structure."
|
||||||
|
},
|
||||||
|
"blockquote": {
|
||||||
|
"fontFamily": "prose",
|
||||||
|
"fontSize": "1.25rem",
|
||||||
|
"fontWeight": 400,
|
||||||
|
"lineHeight": 1.6,
|
||||||
|
"letterSpacing": "normal",
|
||||||
|
"fontStyle": "italic",
|
||||||
|
"description": "Pull quotes, cited text. 20px Source Serif 4 italic — a gentle size step up signals 'someone else's words' without shouting."
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"fontFamily": "structure",
|
||||||
|
"fontSize": "0.875rem",
|
||||||
|
"fontWeight": 500,
|
||||||
|
"lineHeight": 1.5,
|
||||||
|
"letterSpacing": "0.01em",
|
||||||
|
"description": "Image captions, metadata labels, timestamps, tags. 14px Inter Medium — the structure voice at small scale."
|
||||||
|
},
|
||||||
|
"codeInline": {
|
||||||
|
"fontFamily": "evidence",
|
||||||
|
"fontSize": "0.9em",
|
||||||
|
"fontWeight": 400,
|
||||||
|
"lineHeight": "inherit",
|
||||||
|
"letterSpacing": "normal",
|
||||||
|
"description": "Inline code within prose. 0.9em relative to parent — the evidence voice embedded in narrative."
|
||||||
|
},
|
||||||
|
"codeBlock": {
|
||||||
|
"fontFamily": "evidence",
|
||||||
|
"fontSize": "1rem",
|
||||||
|
"fontWeight": 400,
|
||||||
|
"lineHeight": 1.6,
|
||||||
|
"letterSpacing": "normal",
|
||||||
|
"description": "Code blocks. 16px JetBrains Mono — factual, verifiable, this-is-what-actually-happened."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"spacing": {
|
||||||
|
"_meta": {
|
||||||
|
"base": "8px",
|
||||||
|
"philosophy": "The page breathes like a well-set book. Generous vertical rhythm for dense technical narrative. Space above H2 is a full breath — section breaks are chapter breaks."
|
||||||
|
},
|
||||||
|
|
||||||
|
"scale": {
|
||||||
|
"0": { "value": "0", "px": 0 },
|
||||||
|
"px": { "value": "1px", "px": 1 },
|
||||||
|
"0.5":{ "value": "0.25rem", "px": 4, "gridMultiple": "0.5x" },
|
||||||
|
"1": { "value": "0.5rem", "px": 8, "gridMultiple": "1x" },
|
||||||
|
"1.5":{ "value": "0.75rem", "px": 12, "gridMultiple": "1.5x" },
|
||||||
|
"2": { "value": "1rem", "px": 16, "gridMultiple": "2x" },
|
||||||
|
"2.5":{ "value": "1.25rem", "px": 20, "gridMultiple": "2.5x" },
|
||||||
|
"3": { "value": "1.5rem", "px": 24, "gridMultiple": "3x" },
|
||||||
|
"4": { "value": "2rem", "px": 32, "gridMultiple": "4x" },
|
||||||
|
"5": { "value": "2.5rem", "px": 40, "gridMultiple": "5x" },
|
||||||
|
"6": { "value": "3rem", "px": 48, "gridMultiple": "6x" },
|
||||||
|
"8": { "value": "4rem", "px": 64, "gridMultiple": "8x" },
|
||||||
|
"10": { "value": "5rem", "px": 80, "gridMultiple": "10x" },
|
||||||
|
"12": { "value": "6rem", "px": 96, "gridMultiple": "12x" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"semantic": {
|
||||||
|
"paragraphGap": { "value": "1.5rem", "px": 24, "note": "~1.33em at 18px body. Generous — lets ideas land." },
|
||||||
|
"headingAboveH2": { "value": "4rem", "px": 64, "note": "Section breaks are chapter breaks. Full breath between sections." },
|
||||||
|
"headingBelowH2": { "value": "1rem", "px": 16, "note": "Tight — the heading belongs to its content, not the space above." },
|
||||||
|
"headingAboveH3": { "value": "2.5rem", "px": 40, "note": "Sub-section breath. Less than H2, still generous." },
|
||||||
|
"headingBelowH3": { "value": "0.75rem", "px": 12, "note": "Snug — content follows immediately." },
|
||||||
|
"headingAboveH4": { "value": "2rem", "px": 32, "note": "Minor heading spacing." },
|
||||||
|
"headingBelowH4": { "value": "0.5rem", "px": 8, "note": "Very tight — H4 is almost inline with its content." },
|
||||||
|
"codeBlockVertical": { "value": "2.5rem", "px": 40, "note": "Code blocks as exhibited artifacts — generous vertical clearance." },
|
||||||
|
"codeBlockPadding": { "value": "1.5rem", "px": 24, "note": "Internal padding of code blocks." },
|
||||||
|
"codeBlockAccentWidth": { "value": "3px", "note": "Left-edge accent line on code blocks." },
|
||||||
|
"listItemGap": { "value": "0.5rem", "px": 8, "note": "Breathing room between list items." },
|
||||||
|
"blockquoteIndent": { "value": "1.5rem", "px": 24, "note": "Left indent/border offset for blockquotes." },
|
||||||
|
"sectionDivider": { "value": "3rem", "px": 48, "note": "Vertical space when using a horizontal rule." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"rounded": {
|
||||||
|
"_meta": {
|
||||||
|
"philosophy": "Workshop surfaces have worn edges, not machined radii. Just enough softness to not feel sharp, not enough to feel 'designed.'"
|
||||||
|
},
|
||||||
|
"none": { "value": "0" },
|
||||||
|
"sm": { "value": "2px", "use": "inline code, small tags, badges" },
|
||||||
|
"md": { "value": "4px", "use": "code blocks, buttons, small elements" },
|
||||||
|
"lg": { "value": "6px", "use": "cards, callout boxes" },
|
||||||
|
"xl": { "value": "8px", "use": "larger panels, modals (if ever needed)" },
|
||||||
|
"full": { "value": "9999px", "use": "pills, avatars, toggle tracks" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"layout": {
|
||||||
|
"_meta": {
|
||||||
|
"philosophy": "Prose lives in a reading column. Code breaks wider. The width shift signals the register change from narrative to evidence."
|
||||||
|
},
|
||||||
|
"prose": { "value": "45rem", "px": 720, "approxCh": "~65ch at 18px serif", "description": "Reading column max-width. Sweet spot for sustained reading." },
|
||||||
|
"codeBreak": { "value": "52.5rem", "px": 840, "description": "Code block max-width. Wider breakout creates narrow-wide-narrow rhythm." },
|
||||||
|
"container": { "value": "68.75rem","px": 1100, "description": "Outer container max-width. Provides margin for the prose column." },
|
||||||
|
"sideGutter": { "value": "1.5rem", "px": 24, "description": "Minimum horizontal padding on small screens." }
|
||||||
|
},
|
||||||
|
|
||||||
|
"shadows": {
|
||||||
|
"_meta": {
|
||||||
|
"philosophy": "Nothing gleams. Nothing floats. If something casts a shadow, it's the small soft shadow of a card sitting on a desk — barely there."
|
||||||
|
},
|
||||||
|
"sm": { "value": "0 1px 3px rgba(43, 36, 33, 0.04)", "use": "Subtle lift — barely perceptible. Cards at rest." },
|
||||||
|
"md": { "value": "0 2px 8px rgba(43, 36, 33, 0.06)", "use": "Card on a desk — the maximum depth in this system." },
|
||||||
|
"none": { "value": "none", "use": "Explicit no-shadow override." }
|
||||||
|
},
|
||||||
|
|
||||||
|
"motion": {
|
||||||
|
"_meta": {
|
||||||
|
"philosophy": "Fast, barely-there. The confidence of something that doesn't need to announce itself. Reduce to zero for prefers-reduced-motion."
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"instant": { "value": "0ms", "use": "Reduced motion fallback." },
|
||||||
|
"fast": { "value": "100ms", "use": "Color transitions (hover, focus)." },
|
||||||
|
"normal": { "value": "150ms", "use": "Standard interactions — the site's default." },
|
||||||
|
"slow": { "value": "250ms", "use": "Layout shifts, expand/collapse (rare)." }
|
||||||
|
},
|
||||||
|
"easing": {
|
||||||
|
"default": { "value": "cubic-bezier(0.25, 0.1, 0.25, 1.0)", "use": "Standard ease — smooth, natural deceleration." },
|
||||||
|
"inOut": { "value": "cubic-bezier(0.45, 0.05, 0.55, 0.95)", "use": "Symmetric transitions." }
|
||||||
|
},
|
||||||
|
"reducedMotion": {
|
||||||
|
"rule": "@media (prefers-reduced-motion: reduce)",
|
||||||
|
"behavior": "All durations set to 0ms. All transitions disabled. Non-negotiable."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"rationale": {
|
||||||
|
"colorPhilosophy": {
|
||||||
|
"title": "Why these specific colors",
|
||||||
|
"explanation": [
|
||||||
|
"The surface-base (#F5F0E8) was tuned to sit precisely between 'warm' and 'yellow.' Its RGB channels (245, 240, 232) taper from R→G→B, placing the warmth in the red channel without letting it become cream or beige. It reads as 'good light on heavy paper' — the brief's exact intent.",
|
||||||
|
"Text-primary (#2B2421) avoids the blue-black of browser defaults (which typically live around #1a1a2e or #333). The brown-black comes from keeping R > G > B at the dark end: (43, 36, 33). This is ink with warmth, not ink with coldness.",
|
||||||
|
"The accent-primary (#A0522D, CSS 'sienna') is a historically grounded earth pigment — literally fired earth. At 4.95:1 contrast on base, it passes AA for body text and works as link color without being 'red' or 'alarming.' The hover state (#8B4726) darkens toward the earth rather than toward black.",
|
||||||
|
"Accent-secondary (#7D6C2F) captures tarnished brass — the green-gold of aged copper alloy, not the bright gold of decoration. It needed darkening from the first candidate to meet AA (4.56:1), but the constraint improved the color: real tarnished brass IS darker than you'd first guess.",
|
||||||
|
"State colors (success, warning, error, info) were designed within the warm palette. No saturated primaries. Success uses sage-moss (#4A5E3A on #E8EDDF), warning uses amber (#7A5C1F on #F2E8D0), error uses deeper terracotta (#8B3A2A on #F2DED6). Even the info state avoids blue entirely — it's warm neutral (#4A4540 on #E5E2DA)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"contrastStrategy": {
|
||||||
|
"title": "How we exceeded WCAG requirements",
|
||||||
|
"explanation": [
|
||||||
|
"The brief demanded WCAG AAA (7:1) for body text on base. We achieved 13.44:1 — nearly double the requirement. This provides comfortable reading even in imperfect lighting conditions or on devices with lower contrast ratios.",
|
||||||
|
"Secondary text at 5.46:1 (AA requires 4.5:1) gives comfortable headroom. Tertiary text at 3.04:1 meets AA-UI (3:1) — appropriate since it's used only for supplementary information like timestamps and tags, never for essential content.",
|
||||||
|
"Code text (#3D342E) on the code surface (#EEEAE2) achieves 10.12:1 (AAA). Code is evidence — it must be maximally readable.",
|
||||||
|
"Border tokens are split into three tiers: subtle (decorative, no contrast requirement — the 'score line'), default (separators), and strong (#8E857A at 3.20:1, for functional UI elements). This acknowledges that a publishing site's dividers are decorative, not interactive, while still providing accessible borders when needed."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"typographyRationale": {
|
||||||
|
"title": "Three voices, mathematically grounded",
|
||||||
|
"explanation": [
|
||||||
|
"The Major Third ratio (1.25) was chosen because the brief explicitly asked for conservative hierarchy. At 1.25, the jump from body (18px) to H4 (22px) is perceptible but not dramatic — the family change from serif to sans does most of the work. Weight and family, not size, create the hierarchy.",
|
||||||
|
"Heading sizes round to clean values: 22, 28, 36, 44px. Each pair's ratio stays within 1.17-1.29 of true Major Third — close enough for mathematical coherence, rounded enough for pixel clarity.",
|
||||||
|
"The 18px body base was set by the brief, and it's correct for sustained technical reading in a serif face. At 1.125rem, it inherits browser zoom gracefully.",
|
||||||
|
"Code at 16px (1rem / 0.9em of body) respects the 'different register' described in the aesthetic guide. JetBrains Mono at 16px with 1.6 line-height gives comfortable code reading without competing with the prose voice.",
|
||||||
|
"Letter-spacing uses negative tracking for headings (-0.02em at h1, tapering to -0.005em at h4) because Inter at display sizes needs tightening to feel intentional. Body prose and code use normal tracking — the fonts were designed for these sizes.",
|
||||||
|
"Blockquote at 20px italic is the only off-scale value — sitting between base (18) and lg (22). This is deliberate: a pull quote should feel like a different register, not a heading. The italic + size shift + left border treatment (defined in spacing) makes the voice unmistakable."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"spacingRationale": {
|
||||||
|
"title": "Book typography meets code breakout",
|
||||||
|
"explanation": [
|
||||||
|
"The 8px grid provides the full range from fine-grain (4px component padding) to macro (96px section spacing). All semantic tokens map onto grid multiples.",
|
||||||
|
"H2 above-spacing (64px / 4rem) creates the 'chapter break' the aesthetic guide describes. At nearly 4x the paragraph gap, it creates a decisive pause in the scroll. H2 below-spacing (16px / 1rem) is deliberately tight — the heading belongs to what follows, not what precedes it.",
|
||||||
|
"Paragraph gap (24px) is ~1.33em at 18px base — within the 1.2-1.5em range specified, and landing on the 8px grid (3x base).",
|
||||||
|
"Code block vertical margin (40px / 2.5rem) positions code as 'exhibited artifacts' — clearly separated from prose without creating visual gaps. The 3px left accent line in terracotta signals the register change from narrative to evidence.",
|
||||||
|
"The prose column at 720px (45rem) produces ~65 characters per line at 18px Source Serif 4 — the typographic sweet spot for sustained reading. Code breaks to 840px (52.5rem) creating the narrow-wide-narrow rhythm described in the aesthetic guide."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"borderRadiusRationale": {
|
||||||
|
"title": "Worn edges, not machined radii",
|
||||||
|
"explanation": [
|
||||||
|
"Four meaningful stops: 2px (inline code), 4px (code blocks, buttons), 6px (cards), 8px (larger panels). The progression feels like increasingly worn edges on workshop materials — slight softening, never roundness.",
|
||||||
|
"No radius exceeds 8px except 'full' (pills/avatars). Nothing on this site should look like a mobile app button or a glassmorphism card."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"motionRationale": {
|
||||||
|
"title": "Fast, barely-there, respectful",
|
||||||
|
"explanation": [
|
||||||
|
"150ms is the site's default transition — fast enough to feel instant, slow enough to not be jarring. Color transitions (hover, focus) at 100ms. Layout transitions at 250ms (rare — this is a reading site, not an app).",
|
||||||
|
"prefers-reduced-motion reduces all durations to 0ms, no exceptions. This is non-negotiable.",
|
||||||
|
"Only GPU-accelerated properties (transform, opacity) should be animated. Color transitions are the exception — they're so fast (100ms) that the CPU cost is negligible."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"whatWeDidNotDo": {
|
||||||
|
"title": "Intentional omissions",
|
||||||
|
"explanation": [
|
||||||
|
"No dark mode tokens. The aesthetic guide explicitly says 'Ken's work happens in daylight.' Dark mode can be added later if needed — the semantic naming makes it straightforward.",
|
||||||
|
"No elevation system. The brief says 'nothing floats.' Two shadow tokens (sm, md) are the maximum depth vocabulary.",
|
||||||
|
"No animation choreography. A publishing site doesn't need entrance animations, scroll-triggered effects, or page transitions. Links change color. That's it.",
|
||||||
|
"No breakpoint tokens. The layout is a single column that narrows gracefully. The prose column (720px) and code breakout (840px) handle their own responsive behavior within the 1100px container.",
|
||||||
|
"No z-index scale. Nothing overlaps. The page is flat paper with ink on it."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contrast Audit Results
|
||||||
|
|
||||||
|
Every text/background pairing was tested computationally. Full results:
|
||||||
|
|
||||||
|
### Body Text (WCAG AAA — 7:1 required)
|
||||||
|
|
||||||
|
| Pairing | Ratio | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `text-primary` (#2B2421) on `surface-base` (#F5F0E8) | **13.44:1** | PASS |
|
||||||
|
| `text-primary` (#2B2421) on `surface-card` (#FEFCF7) | **14.87:1** | PASS |
|
||||||
|
| `text-primary` (#2B2421) on `surface-code` (#EEEAE2) | **12.71:1** | PASS |
|
||||||
|
| `text-primary` (#2B2421) on `surface-inset` (#E8E3DA) | **11.93:1** | PASS |
|
||||||
|
|
||||||
|
### Secondary Text (WCAG AA — 4.5:1 required)
|
||||||
|
|
||||||
|
| Pairing | Ratio | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `text-secondary` (#6B5F55) on `surface-base` (#F5F0E8) | **5.46:1** | PASS |
|
||||||
|
| `text-secondary` (#6B5F55) on `surface-card` (#FEFCF7) | **6.04:1** | PASS |
|
||||||
|
| `text-secondary` (#6B5F55) on `surface-code` (#EEEAE2) | **5.16:1** | PASS |
|
||||||
|
|
||||||
|
### Tertiary Text (WCAG AA UI — 3:1 required)
|
||||||
|
|
||||||
|
| Pairing | Ratio | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `text-tertiary` (#948880) on `surface-base` (#F5F0E8) | **3.04:1** | PASS |
|
||||||
|
| `text-tertiary` (#948880) on `surface-card` (#FEFCF7) | **3.36:1** | PASS |
|
||||||
|
|
||||||
|
### Accent Links (WCAG AA — 4.5:1 required)
|
||||||
|
|
||||||
|
| Pairing | Ratio | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `accent-primary` (#A0522D) on `surface-base` (#F5F0E8) | **4.95:1** | PASS |
|
||||||
|
| `accent-primary` (#A0522D) on `surface-card` (#FEFCF7) | **5.48:1** | PASS |
|
||||||
|
| `accent-primary` (#A0522D) on `surface-code` (#EEEAE2) | **4.68:1** | PASS |
|
||||||
|
| `accent-primaryHover` (#8B4726) on `surface-base` (#F5F0E8) | **6.12:1** | PASS |
|
||||||
|
| `accent-secondary` (#7D6C2F) on `surface-base` (#F5F0E8) | **4.56:1** | PASS |
|
||||||
|
| `accent-secondary` (#7D6C2F) on `surface-card` (#FEFCF7) | **5.05:1** | PASS |
|
||||||
|
|
||||||
|
### Code Text (WCAG AAA — 7:1 required)
|
||||||
|
|
||||||
|
| Pairing | Ratio | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `code-text` (#3D342E) on `surface-code` (#EEEAE2) | **10.12:1** | PASS |
|
||||||
|
|
||||||
|
### State Colors (WCAG AA — 4.5:1 required)
|
||||||
|
|
||||||
|
| Pairing | Ratio | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `success-text` (#4A5E3A) on `success-bg` (#E8EDDF) | **5.96:1** | PASS |
|
||||||
|
| `warning-text` (#7A5C1F) on `warning-bg` (#F2E8D0) | **5.10:1** | PASS |
|
||||||
|
| `error-text` (#8B3A2A) on `error-bg` (#F2DED6) | **5.92:1** | PASS |
|
||||||
|
| `info-text` (#4A4540) on `info-bg` (#E5E2DA) | **7.32:1** | PASS |
|
||||||
|
|
||||||
|
### Functional Borders (WCAG AA UI — 3:1 required)
|
||||||
|
|
||||||
|
| Pairing | Ratio | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `border-strong` (#8E857A) on `surface-base` (#F5F0E8) | **3.20:1** | PASS |
|
||||||
|
| `border-strong` (#8E857A) on `surface-card` (#FEFCF7) | **3.54:1** | PASS |
|
||||||
|
|
||||||
|
**Note:** `border-subtle` and `border-default` are decorative (score lines, separators) and do not carry WCAG contrast requirements per [WCAG 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html) — decorative borders that don't convey information are exempt. `border-strong` is provided for any functional UI borders that DO need 3:1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### CSS Custom Properties (recommended structure)
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Surfaces */
|
||||||
|
--surface-base: #F5F0E8;
|
||||||
|
--surface-card: #FEFCF7;
|
||||||
|
--surface-code: #EEEAE2;
|
||||||
|
--surface-inset: #E8E3DA;
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--text-primary: #2B2421;
|
||||||
|
--text-secondary: #6B5F55;
|
||||||
|
--text-tertiary: #948880;
|
||||||
|
|
||||||
|
/* Accents */
|
||||||
|
--accent-primary: #A0522D;
|
||||||
|
--accent-primary-hover: #8B4726;
|
||||||
|
--accent-secondary: #7D6C2F;
|
||||||
|
--accent-secondary-hover: #6B5C28;
|
||||||
|
|
||||||
|
/* Code */
|
||||||
|
--code-text: #3D342E;
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--border-subtle: #DDD7CD;
|
||||||
|
--border-default: #C2BAA9;
|
||||||
|
--border-strong: #8E857A;
|
||||||
|
|
||||||
|
/* Focus */
|
||||||
|
--focus-ring: #A0522D;
|
||||||
|
|
||||||
|
/* State: Success */
|
||||||
|
--success-bg: #E8EDDF;
|
||||||
|
--success-text: #4A5E3A;
|
||||||
|
--success-border: #B5C4A0;
|
||||||
|
|
||||||
|
/* State: Warning */
|
||||||
|
--warning-bg: #F2E8D0;
|
||||||
|
--warning-text: #7A5C1F;
|
||||||
|
--warning-border: #D4BC7C;
|
||||||
|
|
||||||
|
/* State: Error */
|
||||||
|
--error-bg: #F2DED6;
|
||||||
|
--error-text: #8B3A2A;
|
||||||
|
--error-border: #CFA090;
|
||||||
|
|
||||||
|
/* State: Info */
|
||||||
|
--info-bg: #E5E2DA;
|
||||||
|
--info-text: #4A4540;
|
||||||
|
--info-border: #B8B2A8;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--shadow-sm: 0 1px 3px rgba(43, 36, 33, 0.04);
|
||||||
|
--shadow-md: 0 2px 8px rgba(43, 36, 33, 0.06);
|
||||||
|
|
||||||
|
/* Typography families */
|
||||||
|
--font-prose: 'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif;
|
||||||
|
--font-structure: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-evidence: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace;
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--space-0: 0;
|
||||||
|
--space-px: 1px;
|
||||||
|
--space-0-5: 0.25rem;
|
||||||
|
--space-1: 0.5rem;
|
||||||
|
--space-1-5: 0.75rem;
|
||||||
|
--space-2: 1rem;
|
||||||
|
--space-2-5: 1.25rem;
|
||||||
|
--space-3: 1.5rem;
|
||||||
|
--space-4: 2rem;
|
||||||
|
--space-5: 2.5rem;
|
||||||
|
--space-6: 3rem;
|
||||||
|
--space-8: 4rem;
|
||||||
|
--space-10: 5rem;
|
||||||
|
--space-12: 6rem;
|
||||||
|
|
||||||
|
/* Border radius */
|
||||||
|
--rounded-none: 0;
|
||||||
|
--rounded-sm: 2px;
|
||||||
|
--rounded-md: 4px;
|
||||||
|
--rounded-lg: 6px;
|
||||||
|
--rounded-xl: 8px;
|
||||||
|
--rounded-full: 9999px;
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
--width-prose: 45rem;
|
||||||
|
--width-code: 52.5rem;
|
||||||
|
--width-container: 68.75rem;
|
||||||
|
--gutter: 1.5rem;
|
||||||
|
|
||||||
|
/* Motion */
|
||||||
|
--duration-fast: 100ms;
|
||||||
|
--duration-normal: 150ms;
|
||||||
|
--duration-slow: 250ms;
|
||||||
|
--ease-default: cubic-bezier(0.25, 0.1, 0.25, 1.0);
|
||||||
|
--ease-in-out: cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
:root {
|
||||||
|
--duration-fast: 0ms;
|
||||||
|
--duration-normal: 0ms;
|
||||||
|
--duration-slow: 0ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Font Loading (recommended)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Preconnect -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|
||||||
|
<!-- Fonts — load only weights used -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,wght@0,400;0,600;1,400&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- **Functional:** All tokens resolve to concrete CSS values. Zero undefined variables.
|
||||||
|
- **Aesthetic:** Every color reads warm. The page feels like paper and ink under good light.
|
||||||
|
- **Accessibility:** AAA body text (13.44:1), AA secondary (5.46:1), AA accent links (4.95:1). All verified.
|
||||||
|
- **Performance:** Three fonts, minimal weights. No heavy shadow/blur calculations.
|
||||||
|
- **Longevity:** Nothing here will look dated in 5 years. Paper, ink, and careful spacing don't go out of style.
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
---
|
||||||
|
name: Crash Test Dev
|
||||||
|
description: >
|
||||||
|
A personal technical publishing site. Workbench aesthetic --
|
||||||
|
warm, grounded, un-digital. Paper, ink, wood, stone, iron.
|
||||||
|
Three typographic voices: serif for prose, sans for structure,
|
||||||
|
mono for evidence.
|
||||||
|
|
||||||
|
colors:
|
||||||
|
primary: "#2B2421"
|
||||||
|
secondary: "#6B5F55"
|
||||||
|
tertiary: "#A0522D"
|
||||||
|
neutral: "#F5F0E8"
|
||||||
|
on-primary: "#F5F0E8"
|
||||||
|
on-tertiary: "#FEFCF7"
|
||||||
|
surface: "#FEFCF7"
|
||||||
|
surface-code: "#EEEAE2"
|
||||||
|
surface-inset: "#E8E3DA"
|
||||||
|
accent-hover: "#8B4726"
|
||||||
|
accent-secondary: "#7D6C2F"
|
||||||
|
accent-secondary-hover: "#6B5C28"
|
||||||
|
code-text: "#3D342E"
|
||||||
|
border-subtle: "#DDD7CD"
|
||||||
|
border-default: "#C2BAA9"
|
||||||
|
border-strong: "#8E857A"
|
||||||
|
success-bg: "#E8EDDF"
|
||||||
|
success-text: "#4A5E3A"
|
||||||
|
warning-bg: "#F2E8D0"
|
||||||
|
warning-text: "#7A5C1F"
|
||||||
|
error-bg: "#F2DED6"
|
||||||
|
error-text: "#8B3A2A"
|
||||||
|
|
||||||
|
typography:
|
||||||
|
h1:
|
||||||
|
fontFamily: Inter
|
||||||
|
fontSize: 2.75rem
|
||||||
|
fontWeight: 700
|
||||||
|
lineHeight: 1.15
|
||||||
|
letterSpacing: -0.02em
|
||||||
|
h2:
|
||||||
|
fontFamily: Inter
|
||||||
|
fontSize: 2.25rem
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.25
|
||||||
|
letterSpacing: -0.015em
|
||||||
|
h3:
|
||||||
|
fontFamily: Inter
|
||||||
|
fontSize: 1.75rem
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.3
|
||||||
|
letterSpacing: -0.01em
|
||||||
|
h4:
|
||||||
|
fontFamily: Inter
|
||||||
|
fontSize: 1.375rem
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.35
|
||||||
|
letterSpacing: -0.005em
|
||||||
|
body-md:
|
||||||
|
fontFamily: Source Serif 4
|
||||||
|
fontSize: 1.125rem
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.7
|
||||||
|
body-sm:
|
||||||
|
fontFamily: Source Serif 4
|
||||||
|
fontSize: 0.875rem
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.6
|
||||||
|
letterSpacing: 0.005em
|
||||||
|
blockquote:
|
||||||
|
fontFamily: Source Serif 4
|
||||||
|
fontSize: 1.25rem
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.6
|
||||||
|
caption:
|
||||||
|
fontFamily: Inter
|
||||||
|
fontSize: 0.875rem
|
||||||
|
fontWeight: 500
|
||||||
|
lineHeight: 1.5
|
||||||
|
letterSpacing: 0.01em
|
||||||
|
label:
|
||||||
|
fontFamily: Inter
|
||||||
|
fontSize: 0.75rem
|
||||||
|
fontWeight: 600
|
||||||
|
lineHeight: 1.5
|
||||||
|
letterSpacing: 0.05em
|
||||||
|
code-inline:
|
||||||
|
fontFamily: JetBrains Mono
|
||||||
|
fontSize: 0.9em
|
||||||
|
fontWeight: 400
|
||||||
|
code-block:
|
||||||
|
fontFamily: JetBrains Mono
|
||||||
|
fontSize: 1rem
|
||||||
|
fontWeight: 400
|
||||||
|
lineHeight: 1.6
|
||||||
|
|
||||||
|
rounded:
|
||||||
|
sm: 2px
|
||||||
|
md: 4px
|
||||||
|
lg: 6px
|
||||||
|
xl: 8px
|
||||||
|
full: 9999px
|
||||||
|
|
||||||
|
spacing:
|
||||||
|
xs: 4px
|
||||||
|
sm: 8px
|
||||||
|
md: 16px
|
||||||
|
lg: 24px
|
||||||
|
xl: 32px
|
||||||
|
2xl: 40px
|
||||||
|
3xl: 48px
|
||||||
|
4xl: 64px
|
||||||
|
|
||||||
|
components:
|
||||||
|
article-card:
|
||||||
|
backgroundColor: "{colors.surface}"
|
||||||
|
textColor: "{colors.primary}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 24px
|
||||||
|
article-card-hover:
|
||||||
|
backgroundColor: "{colors.surface}"
|
||||||
|
textColor: "{colors.tertiary}"
|
||||||
|
code-block:
|
||||||
|
backgroundColor: "{colors.surface-code}"
|
||||||
|
textColor: "{colors.code-text}"
|
||||||
|
rounded: "{rounded.md}"
|
||||||
|
padding: 24px
|
||||||
|
callout-pattern:
|
||||||
|
backgroundColor: "{colors.surface-inset}"
|
||||||
|
textColor: "{colors.primary}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 24px
|
||||||
|
callout-antipattern:
|
||||||
|
backgroundColor: "{colors.surface-inset}"
|
||||||
|
textColor: "{colors.primary}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 24px
|
||||||
|
audio-player:
|
||||||
|
backgroundColor: "{colors.surface-inset}"
|
||||||
|
textColor: "{colors.secondary}"
|
||||||
|
rounded: "{rounded.full}"
|
||||||
|
padding: 16px
|
||||||
|
audio-player-expanded:
|
||||||
|
backgroundColor: "{colors.surface-inset}"
|
||||||
|
textColor: "{colors.secondary}"
|
||||||
|
rounded: "{rounded.lg}"
|
||||||
|
padding: 16px
|
||||||
|
nav-bar:
|
||||||
|
backgroundColor: "{colors.neutral}"
|
||||||
|
textColor: "{colors.secondary}"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Crash Test Dev is a personal technical publishing site built on a single metaphor: **the workbench, not the gallery.** The design draws from a materials vocabulary of paper, ink, wood, stone, and iron -- surfaces that have been used, heated, cooled. Nothing gleams. Nothing floats. Every element earns its presence through function.
|
||||||
|
|
||||||
|
The aesthetic is quiet confidence with warmth. Warm off-white backgrounds like heavy unbleached paper stock. Brown-black text like good ink. A muted terracotta accent drawn from fired earth -- the site's only color with intent. Three typographic voices work together: a serif for prose (Ken's voice on the page), a sans for structure (headings, navigation, labels), and a monospace for evidence (code, proof, receipts).
|
||||||
|
|
||||||
|
The site serves a personal audience hierarchy: the author first (this is a reference for his own thinking), his children someday, peers in engineering second, and other travelers last. The craft level is high because craft is how you show respect for your own thinking -- not because someone is performing quality. If someone inspects the page and thinks "there's not much design here," the design succeeded.
|
||||||
|
|
||||||
|
This is not a corporate dev blog. Not a portfolio. Not a "personal brand." It's a thinking journal published with the care of a printed essay. The writing is the hero. The design gets out of the way and makes the reading experience exceptional.
|
||||||
|
|
||||||
|
## Colors
|
||||||
|
|
||||||
|
The palette is warm across all values. No blue-grays, no cool neutrals. The temperature comes from keeping red channel values higher than green and blue at every level of the scale, creating visual comfort even at strong contrast ratios.
|
||||||
|
|
||||||
|
- **Primary (#2B2421):** Brown-black ink. The warmth comes from RGB (43, 36, 33) -- red leads. Body text, headlines, anything that demands full attention. 13.44:1 contrast on the neutral surface (AAA).
|
||||||
|
- **Secondary (#6B5F55):** Warm stone gray. Metadata, captions, bylines, navigation links at rest. 5.46:1 on neutral (AA).
|
||||||
|
- **Tertiary (#A0522D):** Muted terracotta, CSS "sienna." The sole accent color -- literally fired earth. Links, emphasis, code block left-edge accents. 4.95:1 on neutral (AA). The hover state (#8B4726) darkens toward earth, not toward black.
|
||||||
|
- **Neutral (#F5F0E8):** Heavy unbleached paper stock. Page background. RGB (245, 240, 232) tapers R > G > B, placing the warmth in the red channel without becoming cream or beige.
|
||||||
|
- **Surface (#FEFCF7):** White card stock laid on the paper base. Reading areas, article cards.
|
||||||
|
- **Surface-code (#EEEAE2):** Heavier paper weight. Code blocks. The material shift signals "different register."
|
||||||
|
- **Surface-inset (#E8E3DA):** Recessed surface for callouts, asides, and the audio player.
|
||||||
|
- **Accent-secondary (#7D6C2F):** Tarnished brass. The green-gold of aged copper alloy. For tips, special highlights.
|
||||||
|
|
||||||
|
State colors stay within the warm palette: sage green for success (#4A5E3A on #E8EDDF), warm amber for warnings (#7A5C1F on #F2E8D0), deeper terracotta for errors (#8B3A2A on #F2DED6). No saturated primaries anywhere.
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
Three voices, three jobs. The reader always knows which voice is speaking.
|
||||||
|
|
||||||
|
**Source Serif 4** is the prose voice -- Ken's voice on the page. A transitional serif at 18px (1.125rem) with 1.7 line-height, generous enough for dense technical narrative. Weight 400 for body, 600 for inline emphasis. The serif 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. Blockquotes step up to 20px italic -- a different register, clearly quoted words.
|
||||||
|
|
||||||
|
**Inter** is the structure voice. Headings, UI labels, metadata, timestamps, navigation. It organizes without competing with the prose. Semi-bold (600) for headings creates contrast with the lighter serif body without shouting. Negative letter-spacing at display sizes (-0.02em for h1, tapering to -0.005em for h4) because Inter at large sizes needs tightening to feel intentional.
|
||||||
|
|
||||||
|
**JetBrains Mono** is the evidence voice. Code blocks, terminal output, file paths, inline code. This is where receipts are shown. Slightly smaller than prose (0.9em inline, 1rem in blocks) with 1.6 line-height. Clear character differentiation. Factual, verifiable.
|
||||||
|
|
||||||
|
The type scale uses a Major Third ratio (1.25) from the 18px base: 12, 14, 18, 22, 28, 36, 44px. Conservative -- hierarchy comes from weight and family contrast, not dramatic size jumps.
|
||||||
|
|
||||||
|
A system-wide label treatment (12px Inter semibold, uppercase, 0.05em tracking) appears identically on article cards, callout headers, and post headers. Recognize it once, understand it everywhere.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
The page breathes like a well-set book with one key deviation: code breaks the column.
|
||||||
|
|
||||||
|
Prose lives in a reading column at 720px (45rem, approximately 65 characters per line at 18px serif). This is the typographic sweet spot for sustained reading. Code blocks break wider to 840px (52.5rem), creating a narrow-wide-narrow rhythm as you scroll. The width shift signals the register change from narrative to evidence -- your eye physically moves to a wider surface when the voice changes from serif prose to mono proof.
|
||||||
|
|
||||||
|
Vertical rhythm follows the content's natural structure. H2 headings get 64px above them -- a full breath between sections, because section breaks are chapter breaks. H2-to-content gets just 16px -- the heading belongs to what follows. H3 gets 40px above, H4 gets 32px. Paragraph spacing is 24px. Code blocks get 40px above and below -- exhibited artifacts, not inline interruptions.
|
||||||
|
|
||||||
|
The outer container is 1100px. Side gutters are 24px minimum. On narrow viewports, the prose column narrows gracefully. No breakpoint-driven layout shifts -- it's a single column that adapts.
|
||||||
|
|
||||||
|
Spacing follows an 8px base grid: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96px.
|
||||||
|
|
||||||
|
## Elevation & Depth
|
||||||
|
|
||||||
|
Nothing floats. The maximum depth in the system is a hovered article card.
|
||||||
|
|
||||||
|
The entire depth vocabulary: page background (no shadow, neutral surface), cards at rest (shadow-sm: barely perceptible desk shadow), cards on hover (shadow-md: the maximum), and inset surfaces (no shadow, background shift only). That's it.
|
||||||
|
|
||||||
|
Shadows use warm-toned RGBA derived from the primary ink color: `rgba(43, 36, 33, 0.04)` for sm, `rgba(43, 36, 33, 0.06)` for md. No dramatic elevation. No layered depth system. If something casts a shadow, it's the small soft shadow of a card sitting on a desk.
|
||||||
|
|
||||||
|
Code blocks and callouts feel inset -- a background shift plus a 3px left-edge accent line in terracotta. The accent line is a consistent grammar: "pay attention, this is a different voice." Different material embedded in the page, not floating above it.
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
Workshop surfaces have worn edges, not machined radii. Four meaningful stops:
|
||||||
|
|
||||||
|
- **sm (2px):** Inline code spans, small tags and badges. Barely softened.
|
||||||
|
- **md (4px):** Code blocks, buttons, small interactive elements. Just enough to not feel sharp.
|
||||||
|
- **lg (6px):** Article cards, callout boxes. Slightly more wear.
|
||||||
|
- **xl (8px):** Larger panels if ever needed. Still not "round."
|
||||||
|
- **full (9999px):** Pills only -- the audio player collapsed state, avatars.
|
||||||
|
|
||||||
|
No radius exceeds 8px except full. Nothing on this site should look like a mobile app button or a glassmorphism card.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
Six core components define the visual vocabulary.
|
||||||
|
|
||||||
|
**article-card** -- An index card laid on the desk. White surface (`{colors.surface}`) with the system's only use of shadow (sm at rest, md on hover). Content type badge in terracotta uppercase label, title in 28px Inter semibold, summary in serif, date in tertiary. The entire card is a single link -- touch target is the full surface. On hover, the shadow deepens and the title shifts to terracotta.
|
||||||
|
|
||||||
|
**code-block** -- The evidence container. Breaks wider than prose (840px vs 720px). Heavier paper background (`{colors.surface-code}`) with a 3px terracotta left border accent. No shadow -- it's inset. Optional filename label in 12px Inter at top-right. Syntax highlighting stays warm: terracotta for keywords, tarnished brass for strings, no neon anywhere. Warm-styled scrollbar for horizontal overflow.
|
||||||
|
|
||||||
|
**audio-player** -- Starts as a pill (offer, not demand): play icon, "Listen," duration. Expands to full transport controls within the prose column: play/pause, progress bar with terracotta fill, time in mono (because time is a fact), speed selector cycling 1x through 2x. The one layout animation in the system (250ms expand/collapse) is justified because the shape change communicates a mode shift.
|
||||||
|
|
||||||
|
**callout** -- A sticky note on the workbench. Inset surface with a 3px left accent, color varying by type: terracotta for Pattern, clay-red for Anti-pattern, amber for Warning, tarnished brass for Tip, neutral for Note. Same uppercase label treatment as article cards. Body in serif at full reading weight. Static -- callouts are read, not clicked.
|
||||||
|
|
||||||
|
**post-header** -- Typography IS the hero. Centered stack on the base surface: content type badge, title at 44px Inter bold, meta line (date and reading time in tertiary), optional audio pill. No container, no border, no shadow. 64px breathing room above and below. Title steps down the type scale on narrow viewports (44 to 36 to 28px) but the hierarchy holds.
|
||||||
|
|
||||||
|
**nav-bar** -- The quietest component. Same surface as the page (no separate background). Site name at 18px Inter semibold left, three content type links right (Posts, Patterns, Lessons). Active page gets a 2px terracotta underline offset 6px below. One subtle border-bottom score line. Static positioning because persistent nav steals reading space. No hamburger for three links.
|
||||||
|
|
||||||
|
## Do's and Don'ts
|
||||||
|
|
||||||
|
**Do** use serif (Source Serif 4) for all long-form prose. The serif is the primary voice.
|
||||||
|
|
||||||
|
**Do** use the terracotta accent (#A0522D) sparingly -- for links, emphasis, and left-edge accents on code blocks and callouts. It's the site's only intentional color.
|
||||||
|
|
||||||
|
**Do** let code blocks break wider than prose. The 720px-to-840px rhythm is the core visual pattern.
|
||||||
|
|
||||||
|
**Do** maintain the warm temperature across everything. Every surface, every text color, every border should feel warm. If a color has a blue or cool undertone, it doesn't belong here.
|
||||||
|
|
||||||
|
**Do** keep shadows minimal and warm-toned. shadow-md on a hovered card is the maximum depth anywhere.
|
||||||
|
|
||||||
|
**Don't** use dark mode, neon syntax highlighting, or saturated primary colors. Ken's work happens in daylight.
|
||||||
|
|
||||||
|
**Don't** add hero images, gradient banners, or decorative illustrations. The writing is the hero. Typography is the visual weight.
|
||||||
|
|
||||||
|
**Don't** use animation beyond color transitions (100ms) and the audio player expand (250ms). This is a reading site, not an app.
|
||||||
|
|
||||||
|
**Don't** add sidebar content, newsletter CTAs, social media icons, or "hire me" elements. This is a library, not a landing page.
|
||||||
|
|
||||||
|
**Don't** use glassmorphism, blur effects, or any visual treatment that will look dated in two years. Paper, ink, and careful spacing don't go out of style.
|
||||||
|
|
||||||
|
**Don't** underline links in body prose except on hover. The terracotta color already signals "this is a link." Underline appears on hover as confirmation, and as the active-state indicator in the nav.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
bundle:
|
||||||
|
name: crashtestdev
|
||||||
|
version: 1.0.0
|
||||||
|
description: >
|
||||||
|
Crash test dev site with DESIGN.md portable format support.
|
||||||
|
Includes design-intelligence agents and DESIGN.md generation,
|
||||||
|
import, and validation capabilities.
|
||||||
|
|
||||||
|
includes:
|
||||||
|
- bundle: git+https://github.com/kenotron-ms/amplifier-bundle-design-md@master
|
||||||
|
---
|
||||||
File diff suppressed because it is too large
Load Diff
Executable
+162
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lint-voice.sh -- Anti-slop linter for Crash Test Dev posts
|
||||||
|
# Usage: ./scripts/lint-voice.sh src/content/posts/my-post.md
|
||||||
|
#
|
||||||
|
# Checks for AI writing tells and voice mismatches.
|
||||||
|
# Exit code 0 = clean, 1 = issues found.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 <markdown-file>"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
FILE="$1"
|
||||||
|
if [[ ! -f "$FILE" ]]; then
|
||||||
|
echo "File not found: $FILE"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip frontmatter (everything between first and second ---)
|
||||||
|
BODY=$(sed -n '/^---$/,/^---$/!p' "$FILE" | tail -n +1)
|
||||||
|
WORD_COUNT=$(echo "$BODY" | wc -w)
|
||||||
|
ISSUES=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
echo "=== Voice Lint: $(basename "$FILE") ==="
|
||||||
|
echo " Words: $WORD_COUNT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# --- CHECK 1: Em-dash density ---
|
||||||
|
EMDASH_COUNT=$(echo "$BODY" | grep -o '—' | wc -l)
|
||||||
|
EMDASH_DOUBLE=$(echo "$BODY" | grep -oP '(?<!\-)--(?!\-)' | wc -l)
|
||||||
|
TOTAL_DASHES=$((EMDASH_COUNT + EMDASH_DOUBLE))
|
||||||
|
if [[ $WORD_COUNT -gt 0 ]]; then
|
||||||
|
DENSITY=$(echo "scale=1; $TOTAL_DASHES * 1000 / $WORD_COUNT" | bc 2>/dev/null || echo "0")
|
||||||
|
else
|
||||||
|
DENSITY="0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $TOTAL_DASHES -gt 5 ]]; then
|
||||||
|
echo "FAIL Em-dashes: $TOTAL_DASHES found ($DENSITY per 1000 words)"
|
||||||
|
echo " Normal: 1-3 per 1000 words. Yours: $DENSITY"
|
||||||
|
echo " Lines:"
|
||||||
|
grep -n '—\|[^-]--[^-]' "$FILE" | head -20 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
ISSUES=$((ISSUES + 1))
|
||||||
|
elif [[ $TOTAL_DASHES -gt 3 ]]; then
|
||||||
|
echo "WARN Em-dashes: $TOTAL_DASHES found ($DENSITY per 1000 words)"
|
||||||
|
echo " Getting close to AI territory. Consider replacing 1-2."
|
||||||
|
echo ""
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "OK Em-dashes: $TOTAL_DASHES ($DENSITY per 1000 words)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- CHECK 2: AI trigger words ---
|
||||||
|
TRIGGER_WORDS="delve|tapestry|multifaceted|nuanced|landscape|comprehensive|pivotal|leverage[sd]?|leveraging|robust|utilize[sd]?|utilizing|seamless|cutting-edge|game.changer|paradigm|synergy|holistic|streamline"
|
||||||
|
TRIGGERS=$(echo "$BODY" | grep -oiP "\b($TRIGGER_WORDS)\b" | sort | uniq -c | sort -rn)
|
||||||
|
if [[ -n "$TRIGGERS" ]]; then
|
||||||
|
echo "FAIL AI trigger words found:"
|
||||||
|
echo "$TRIGGERS" | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
ISSUES=$((ISSUES + 1))
|
||||||
|
else
|
||||||
|
echo "OK No AI trigger words"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- CHECK 3: Hedging phrases ---
|
||||||
|
HEDGE_PATTERNS="it.s worth noting|it is worth noting|it.s important to note|one might argue|it could be said|in today.s digital|in today.s rapidly|in the realm of|it goes without saying|at the end of the day|in an era where|it is important to understand"
|
||||||
|
HEDGES=$(echo "$BODY" | grep -oiP "$HEDGE_PATTERNS" | sort | uniq -c | sort -rn)
|
||||||
|
if [[ -n "$HEDGES" ]]; then
|
||||||
|
echo "FAIL Hedging phrases found:"
|
||||||
|
echo "$HEDGES" | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
ISSUES=$((ISSUES + 1))
|
||||||
|
else
|
||||||
|
echo "OK No hedging phrases"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- CHECK 4: Filler transitions ---
|
||||||
|
FILLER_PATTERNS="in conclusion|moving on|with that said|that being said|to summarize|in summary|as we.ve seen|as mentioned earlier|without further ado|let.s dive in|let.s delve"
|
||||||
|
FILLERS=$(echo "$BODY" | grep -oiP "$FILLER_PATTERNS" | sort | uniq -c | sort -rn)
|
||||||
|
if [[ -n "$FILLERS" ]]; then
|
||||||
|
echo "FAIL Filler transitions found:"
|
||||||
|
echo "$FILLERS" | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
ISSUES=$((ISSUES + 1))
|
||||||
|
else
|
||||||
|
echo "OK No filler transitions"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- CHECK 5: Resolution closers ---
|
||||||
|
CLOSER_PATTERNS="the key takeaway|the takeaway here|at the end of the day|what this means is|the bottom line|the real question is"
|
||||||
|
CLOSERS=$(echo "$BODY" | grep -oiP "$CLOSER_PATTERNS" | sort | uniq -c | sort -rn)
|
||||||
|
if [[ -n "$CLOSERS" ]]; then
|
||||||
|
echo "WARN Resolution closer phrases found:"
|
||||||
|
echo "$CLOSERS" | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "OK No resolution closers"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- CHECK 6: Receipts check (links and code blocks) ---
|
||||||
|
LINK_COUNT=$(echo "$BODY" | grep -oP '\[.*?\]\(http[^)]+\)' | wc -l)
|
||||||
|
URL_COUNT=$(echo "$BODY" | grep -oP 'https?://[^\s\)]+' | wc -l)
|
||||||
|
CODE_BLOCK_COUNT=$(echo "$BODY" | grep -c '^```' || true)
|
||||||
|
CODE_BLOCK_COUNT=$((CODE_BLOCK_COUNT / 2))
|
||||||
|
|
||||||
|
TOTAL_EVIDENCE=$((LINK_COUNT + URL_COUNT + CODE_BLOCK_COUNT))
|
||||||
|
if [[ $TOTAL_EVIDENCE -eq 0 && $WORD_COUNT -gt 500 ]]; then
|
||||||
|
echo "WARN No receipts: 0 links, 0 URLs, 0 code blocks in $WORD_COUNT words"
|
||||||
|
echo " Ken's voice rule: \"Show receipts. Always. Non-negotiable.\""
|
||||||
|
echo " Consider: Is this a manifesto (OK without receipts) or a war story (needs them)?"
|
||||||
|
echo ""
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "OK Evidence: $LINK_COUNT links, $URL_COUNT URLs, $CODE_BLOCK_COUNT code blocks"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- CHECK 7: Sentence length variation ---
|
||||||
|
# Count sentences (rough: split on . ! ? followed by space or newline)
|
||||||
|
SENTENCES=$(echo "$BODY" | grep -oP '[^.!?]+[.!?]' | wc -l)
|
||||||
|
if [[ $SENTENCES -gt 5 ]]; then
|
||||||
|
# Get word counts per sentence
|
||||||
|
LENGTHS=$(echo "$BODY" | grep -oP '[^.!?]+[.!?]' | while read -r sent; do echo "$sent" | wc -w; done)
|
||||||
|
AVG=$(echo "$LENGTHS" | awk '{sum+=$1} END {printf "%.0f", sum/NR}')
|
||||||
|
# Count sentences within 5 words of average
|
||||||
|
UNIFORM=$(echo "$LENGTHS" | awk -v avg="$AVG" '{if ($1 >= avg-5 && $1 <= avg+5) count++} END {printf "%.0f", (count/NR)*100}')
|
||||||
|
if [[ $UNIFORM -gt 70 ]]; then
|
||||||
|
echo "WARN Sentence uniformity: ${UNIFORM}% within 5 words of average ($AVG words)"
|
||||||
|
echo " AI benchmark: >70% uniform. Human writing varies wildly."
|
||||||
|
echo " Try: Add some 5-word punches. Break up medium sentences."
|
||||||
|
echo ""
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "OK Sentence variation: ${UNIFORM}% uniform (avg $AVG words/sentence)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SUMMARY ---
|
||||||
|
echo "=== Summary ==="
|
||||||
|
if [[ $ISSUES -eq 0 && $WARNINGS -eq 0 ]]; then
|
||||||
|
echo "CLEAN -- No issues found"
|
||||||
|
exit 0
|
||||||
|
elif [[ $ISSUES -eq 0 ]]; then
|
||||||
|
echo "$WARNINGS warning(s), 0 failures"
|
||||||
|
echo "Review warnings but publishable."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "$ISSUES FAILURE(s), $WARNINGS warning(s)"
|
||||||
|
echo "Fix failures before publishing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -8,23 +8,23 @@ summary: "AI raises the floor dramatically. It does not flatten the ceiling. The
|
|||||||
|
|
||||||
## The Core Argument
|
## The Core Argument
|
||||||
|
|
||||||
Real professional engineers will still be needed for excellence — not because AI can't write code, but because excellence is qualitatively different from functional, and the gap between the two requires judgment, taste, and accountability that no model improvement can close.
|
Real professional engineers will still be needed for excellence. Not because AI can't write code, but because excellence is qualitatively different from functional. The gap between the two requires judgment, taste, and accountability that no model improvement can close.
|
||||||
|
|
||||||
AI raises the floor dramatically. It does not flatten the ceiling.
|
AI raises the floor dramatically. It does not flatten the ceiling.
|
||||||
|
|
||||||
## The Dignity Problem (Most Underappreciated)
|
## The Dignity Problem (Most Underappreciated)
|
||||||
|
|
||||||
The threat to engineers isn't only economic. When expert judgment stops being consulted, something human is lost — the social recognition that hard-won expertise deserves. A doctor who loses clinical judgment to AI doesn't just lose income; they lose the acknowledgment that fifteen years of intuition was worth something.
|
The threat to engineers isn't only economic. When expert judgment stops being consulted, something human is lost: the social recognition that hard-won expertise deserves. A doctor who loses clinical judgment to AI doesn't just lose income. They lose the acknowledgment that fifteen years of intuition was worth something.
|
||||||
|
|
||||||
This is a different kind of harm than job displacement and deserves its own conversation.
|
This is a different kind of harm than job displacement and deserves its own conversation.
|
||||||
|
|
||||||
## "Judgment + AI > Professionals" Is Weaponized Dunning-Kruger
|
## "Judgment + AI > Professionals" Is Weaponized Dunning-Kruger
|
||||||
|
|
||||||
Non-engineers with AI tools get just enough coherent output to *look* competent. This is more dangerous than obvious incompetence — it fools everyone longer, including the person doing it.
|
Non-engineers with AI tools get just enough coherent output to *look* competent. This is more dangerous than obvious incompetence. It fools everyone longer, including the person doing it.
|
||||||
|
|
||||||
The professional sees second-order failures coming. The amateur with AI doesn't know there is a second order.
|
The professional sees second-order failures coming. The amateur with AI doesn't know there is a second order.
|
||||||
|
|
||||||
## The Tradesman Analogy — Where It Holds and Where It Breaks
|
## The Tradesman Analogy: Where It Holds and Where It Breaks
|
||||||
|
|
||||||
**What actually protects trades:** physical reality checks (the fridge either gets cold or it doesn't), special tool access, licensing for hazardous materials, liability, code inspections.
|
**What actually protects trades:** physical reality checks (the fridge either gets cold or it doesn't), special tool access, licensing for hazardous materials, liability, code inspections.
|
||||||
|
|
||||||
@@ -34,47 +34,44 @@ The professional sees second-order failures coming. The amateur with AI doesn't
|
|||||||
- Complex systems integration (AI-assisted apps work at 1,000 users; fail catastrophically at 100,000)
|
- Complex systems integration (AI-assisted apps work at 1,000 users; fail catastrophically at 100,000)
|
||||||
- Security and adversarial reasoning (not "write code that works" but "what does a motivated attacker do with this?")
|
- Security and adversarial reasoning (not "write code that works" but "what does a motivated attacker do with this?")
|
||||||
- Production debugging of distributed systems (race conditions, cascading failures)
|
- Production debugging of distributed systems (race conditions, cascading failures)
|
||||||
- Professional accountability — someone has to sign off, and their reputation is on the line
|
- Professional accountability: someone has to sign off, and their reputation is on the line
|
||||||
|
|
||||||
The protection isn't tool access. It's *complexity that bites back* — systems that reward deep knowledge by visibly punishing its absence.
|
The protection isn't tool access. It's *complexity that bites back*. Systems that reward deep knowledge by visibly punishing its absence.
|
||||||
|
|
||||||
## Excellence vs. Functional — A Qualitative Distinction
|
## Excellence vs. Functional: A Qualitative Distinction
|
||||||
|
|
||||||
A better model raises the ceiling on "good enough." It doesn't touch excellence. Excellence requires:
|
A better model raises the ceiling on "good enough." It doesn't touch excellence. Excellence requires:
|
||||||
|
|
||||||
- **Knowing when requirements are wrong.** AI executes on requirements. It doesn't challenge them.
|
- **Knowing when requirements are wrong.** AI executes on requirements. It doesn't challenge them.
|
||||||
- **Taste about what not to build.** Knowing which feature request is a symptom of a deeper problem.
|
- **Taste about what not to build.** Knowing which feature request is a symptom of a deeper problem.
|
||||||
- **Conceptual integrity over time.** Holding the whole system in your head and noticing when new pieces violate its coherence.
|
- **Conceptual integrity over time.** Holding the whole system in your head and noticing when new pieces violate its coherence.
|
||||||
- **The 3am judgment call.** Partial information, pressure, three competing hypotheses — picking which risk to take and owning it.
|
- **The 3am judgment call.** Partial information, pressure, three competing hypotheses. You pick which risk to take and you own it.
|
||||||
- **Scar tissue.** AI has no memory of the deployment that took down prod because of a race condition that looked fine in testing. The professional does.
|
- **Scar tissue.** AI has no memory of the deployment that took down prod because of a race condition that looked fine in testing. The professional does.
|
||||||
|
|
||||||
## The CEO Incentive Problem
|
## The CEO Incentive Problem
|
||||||
|
|
||||||
CEOs optimize for what they can measure: headcount costs, features shipped, time to market. AI makes the visible part of engineering look cheap.
|
CEOs optimize for what they can measure: headcount costs, features shipped, time to market. AI makes the visible part of engineering look cheap.
|
||||||
|
|
||||||
What isn't on any dashboard:
|
What isn't on any dashboard: the judgment call that prevented a disaster nobody even knows about. An architectural decision from three years ago that's the reason the system still works at scale. Taste. The product feels like one thing instead of six things glued together, and nobody can point to a line item that made it so.
|
||||||
- The judgment call that prevented a disaster
|
|
||||||
- The architectural decision that kept the system coherent at scale
|
|
||||||
- The taste that made the product feel like one thing
|
|
||||||
|
|
||||||
The failure mode of cutting engineering judgment is **lagged** — 12-18 months to show up, and by then it's attributed to market conditions or leadership changes, not the original decision.
|
The failure mode of cutting engineering judgment is **lagged**. It takes 12-18 months to show up, and by then it's attributed to market conditions or leadership changes, not the original decision.
|
||||||
|
|
||||||
This is structurally identical to cutting cybersecurity. You don't see the attacks that didn't happen.
|
This is structurally identical to cutting cybersecurity. You don't see the attacks that didn't happen.
|
||||||
|
|
||||||
**What could change the incentive structure:**
|
**What could change the incentive structure:**
|
||||||
- Liability — if organizations faced professional accountability for AI-generated software failures the way they face accountability for accounting fraud, you'd need engineers the way you need licensed accountants
|
- Liability: if organizations faced professional accountability for AI-generated software failures the way they face accountability for accounting fraud, you'd need engineers the way you need licensed accountants
|
||||||
- Attributed failures — high-profile disasters clearly traced to "they fired engineers and replaced with AI"
|
- Attributed failures: high-profile disasters clearly traced to "they fired engineers and replaced with AI"
|
||||||
- Credentialing and certification in safety-critical domains (medical, aviation, finance)
|
- Credentialing and certification in safety-critical domains (medical, aviation, finance)
|
||||||
|
|
||||||
Arguments don't change incentive structures. Consequences do.
|
Arguments don't change incentive structures. Consequences do.
|
||||||
|
|
||||||
## The Pricing Gatekeeping Layer
|
## The Pricing Gatekeeping Layer
|
||||||
|
|
||||||
Model providers have inserted themselves as a new kind of gatekeeper. The non-engineer thinks they've broken free from professional gatekeeping — they've actually just changed gatekeepers to ones who charge by the token, can change pricing overnight, can deprecate models, and have no professional obligation to anyone.
|
Model providers have inserted themselves as a new kind of gatekeeper. The non-engineer thinks they've broken free from professional gatekeeping. They haven't. They've changed gatekeepers to ones who charge by the token, can change pricing overnight, can deprecate models, and have no professional obligation to anyone.
|
||||||
|
|
||||||
**The sustainability tension:**
|
**The sustainability tension:**
|
||||||
- Major labs (Anthropic, OpenAI) are burning enormous capital; pricing reflects "keep the lights on while we figure this out"
|
- Major labs (Anthropic, OpenAI) are burning enormous capital; pricing reflects "keep the lights on while we figure this out"
|
||||||
- Inference costs have dropped ~100x in under three years; open source is closing the capability gap
|
- Inference costs have dropped ~[100x](https://epochai.org/data/notable-ai-models) in under three years; open source is closing the capability gap
|
||||||
- If prices collapse and open source wins → AI becomes nearly free, *strengthening* the "judgment + AI beats professionals" narrative
|
- If prices collapse and open source wins → AI becomes nearly free, *strengthening* the "judgment + AI beats professionals" narrative
|
||||||
- If prices stay high or providers fail → workflows built on top become hostages
|
- If prices stay high or providers fail → workflows built on top become hostages
|
||||||
|
|
||||||
@@ -82,21 +79,12 @@ The accountability that can't be priced by the token: when the AI-assisted decis
|
|||||||
|
|
||||||
## How to Actually Help
|
## How to Actually Help
|
||||||
|
|
||||||
**In immediate conversations:**
|
Start with the conversation you're already in. Make the specific argument, not the vague one. Not "we need engineers" but "here's the failure that happened because judgment was absent." Concrete and attributed. Name what the human contributed when AI does something well. Normalize crediting judgment, not just output.
|
||||||
Make the specific argument, not the vague one. Not "we need engineers" but "here's the failure that happened because judgment was absent." Concrete and attributed. Also: name what the human contributed when AI does something — normalize crediting judgment, not just output.
|
|
||||||
|
|
||||||
**In mentorship:**
|
In mentorship, the skills most worth transferring aren't coding skills. They're judgment skills: how to challenge a requirement, how to read a system for hidden brittleness, how to develop taste. These were assumed to come with seniority and were never made explicit. Make them explicit now.
|
||||||
The skills most worth transferring aren't coding skills. They're judgment skills — how to challenge a requirement, how to read a system for hidden brittleness, how to develop taste. These were assumed to come with seniority and were never made explicit. Make them explicit now.
|
|
||||||
|
|
||||||
**At the organizational level:**
|
At the organizational level, push to measure what actually matters: system health, architectural coherence, incident prevention. If these aren't measured, they'll keep being cut because they're invisible.
|
||||||
Push to measure what actually matters: system health, architectural coherence, incident prevention. If these aren't measured, they'll keep being cut because they're invisible.
|
|
||||||
|
|
||||||
**In public:**
|
|
||||||
The counter-narrative that's true and needs more voices: *AI commoditizes features, but judgment compounds.* People who understand this need to say it plainly, with examples, to audiences who influence the decisions.
|
The counter-narrative that's true and needs more voices: *AI commoditizes features, but judgment compounds.* People who understand this need to say it plainly, with examples, to audiences who influence the decisions.
|
||||||
|
|
||||||
**The structural work:**
|
The structural work (liability frameworks, credentialing, regulation in safety-critical domains) is slow and requires collective action. But the individual can support and accelerate those conversations.
|
||||||
Liability frameworks, credentialing, regulation in safety-critical domains — slow and requiring collective action, but the individual can support and accelerate those conversations.
|
|
||||||
|
|
||||||
## The One-Sentence Version
|
|
||||||
|
|
||||||
The profession that survives is the one hardest to demo without, not just hardest to do without — and the things that are hardest to demo without are exactly the ones that don't show up in any headcount review.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user