diff --git a/.claude/skills/dispatches-design/SKILL.md b/.claude/skills/dispatches-design/SKILL.md
new file mode 100644
index 0000000..247ab43
--- /dev/null
+++ b/.claude/skills/dispatches-design/SKILL.md
@@ -0,0 +1,67 @@
+---
+name: dispatches-design
+description: Use this skill to generate well-branded interfaces and assets for Dispatches, Paul Berman's personal technical blog (plx.github.io) — either production code in this Astro repo or throwaway prototypes/mocks. Contains the brand's colors, type, fonts, assets, and component patterns (the "Twilight" design system).
+user-invocable: true
+---
+
+# Dispatches Design Skill
+
+The site runs on the **Twilight** design system (a.k.a. "Twilight 3b Deeper"):
+a single indigo-night-sky family for surfaces and text, with deep plum as the
+**only** chromatic accent. Quiet, plain, serif-on-paper.
+
+## Start here
+
+- **`docs/DESIGN_SYSTEM.md`** — the repo-specific quick reference: the token
+ table, the token → Tailwind-utility mapping, dos & don'ts, where everything
+ lives, how dark mode works, and the opt-in/deferred patterns. **Read it first
+ for any visual change.**
+- **`docs/design-system/README.md`** — the full brand narrative (voice, tone,
+ iconography, OG/favicon/wordmark specs, "what to avoid").
+- **`docs/design-system/colors_and_type.css`** — the canonical token *reference*
+ (CSS variables). The **live** wiring is `src/styles/tokens.css` (tokens +
+ `@font-face`) and `tailwind.config.mjs` (utilities). Edit those, not the
+ reference copy.
+
+## If you're working on production code (this repo)
+
+The live patterns to mirror:
+
+- **Tokens** are CSS variables in `src/styles/tokens.css`, exposed as Tailwind
+ utilities (`bg-bg`, `text-fg`, `text-fg-strong`, `text-muted`, `border-border`,
+ `bg-bg-hover`, `text-accent`, `decoration-accent`, `bg-accent-soft`,
+ `ring-accent`/`outline-accent`). They **auto-flip in dark mode** — do not add
+ `dark:` color variants; use the semantic token.
+- **Type:** Inter (UI/headings, via `@fontsource/inter`) + Source Serif 4 (prose
+ body, local woff2 in `public/fonts/`, `font-serif`). Monospace = Expressive
+ Code default. Do **not** reintroduce Lora or add a third family.
+- **Components:** `src/components/ExcerptEntry.astro` (the editorial listing),
+ `ContentCard.astro` (projects), `Link.astro`, `Header/Footer.astro`. Global
+ prose + editorial-feed + article-kind styles live in `src/styles/global.css`.
+- **Don't change the type or color direction without checking** — the
+ low-contrast body / strong-heading hierarchy is load-bearing for the brand.
+
+## If you're making a throwaway visual artifact (slides, mocks, one-pagers)
+
+1. Copy `docs/design-system/colors_and_type.css` and any needed
+ `docs/design-system/assets/` into a single self-contained HTML file.
+2. Stay on-brand: Inter + Source Serif 4, the Twilight palette (paper / night /
+ ink / moon + plum accent), **no shadows, no gradients, no second accent,
+ no emoji**, 300ms hover/focus transitions and **no entrance animation**.
+ See the "What to avoid" list in `docs/design-system/README.md`.
+
+## Hard rules (see docs/DESIGN_SYSTEM.md for the full list)
+
+- Plum is the only chroma. No green/red/second accent.
+- No gradients, no shadows, no decorative SVG/mascots/sparkles.
+- No emoji as iconography (and none in body copy).
+- Layout caps: 640px (briefs/default), 540+280 gutter (Tufte blog), never >880px.
+- **Motion is interaction feedback only** — 300ms hover/focus cross-fades; there
+ is **no** page-load fade-up. (The bundled `_ds_manifest.json`/`README` SKILL
+ text mentioning a "700ms fade-up" is stale; the README §Motion is authoritative.)
+
+## If invoked with no other guidance
+
+Ask what to build, ask a few clarifying questions (audience, format, length,
+options vs. a single direction), then act as an expert designer producing either
+an HTML artifact or production code as appropriate.
diff --git a/.conductor/settings.toml b/.conductor/settings.toml
new file mode 100644
index 0000000..3729a84
--- /dev/null
+++ b/.conductor/settings.toml
@@ -0,0 +1,18 @@
+"$schema" = "https://conductor.build/schemas/settings.repo.schema.json"
+
+[scripts]
+setup = "npm ci && npx playwright install chromium firefox webkit"
+run_mode = "concurrent"
+
+[scripts.run.dev]
+command = "npm run dev -- --host 127.0.0.1 --port $CONDUCTOR_PORT"
+default = true
+icon = "play"
+
+[scripts.run.preview]
+command = "npm run build && npm run preview -- --host 127.0.0.1 --port $CONDUCTOR_PORT"
+icon = "globe"
+
+[scripts.run.unit]
+command = "npm run test:unit"
+icon = "test-tube"
diff --git a/.gitignore b/.gitignore
index a44363d..f57a402 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,8 @@ dist/
.astro/
# conductor build artifacts
-.conductor/
+.conductor/*
+!.conductor/settings.toml
# dependencies
node_modules/
@@ -32,3 +33,7 @@ pnpm-debug.log*
/playwright-report/
/blob-report/
/playwright/.cache/
+.playwright-mcp/
+
+# Design-system handoff archive (unpacked form lives in docs/design-system/)
+Dispatches Design System-handoff.zip
diff --git a/CLAUDE.md b/CLAUDE.md
index 11a75e9..00f1f81 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -35,6 +35,29 @@ Some key commands are:
Additionally, we aim to have *reasonable* accessibility support throughout the site.
+## Design System
+
+The site's visual design is the **Twilight** design system: a single
+indigo-night-sky surface/text family with deep **plum** as the only accent;
+serif prose (Source Serif 4) on sans UI (Inter); flat-with-border, no shadows,
+no gradients, no emoji. **Before making any visual change, read
+[`docs/DESIGN_SYSTEM.md`](docs/DESIGN_SYSTEM.md)** (token table, the
+token→Tailwind-utility mapping, dos & don'ts, dark-mode mechanics, and the
+opt-in Tufte / deferred patterns). The `/dispatches-design` skill and the full
+brand narrative in `docs/design-system/README.md` are also available.
+
+Key things to know:
+
+- **Tokens** are CSS variables in `src/styles/tokens.css`, exposed as Tailwind
+ utilities in `tailwind.config.mjs` (`bg-bg`, `text-fg`, `text-fg-strong`,
+ `text-muted`, `border-border`, `bg-bg-hover`, `text-accent`, `bg-accent-soft`,
+ `ring-accent`). They **auto-flip in dark mode** — use the semantic token, don't
+ add `dark:` color variants.
+- **Served brand assets** (`public/favicon.svg`, `og-image*.png`, etc.) are
+ generated from `docs/design-system/` — don't hand-edit the PNGs.
+- **Motion is interaction feedback only** (300ms hover/focus); there is no
+ page-load animation.
+
## Content Structure
The site's content is organized into three main collections:
diff --git a/astro.config.mjs b/astro.config.mjs
index 0ca0b10..2ff45b9 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -7,15 +7,22 @@ import react from "@astrojs/react";
import expressiveCode from "astro-expressive-code";
+import rehypeSidenotes from "./src/lib/rehype-sidenotes.mjs";
+
export default defineConfig({
site: "https://plx.github.io",
+ markdown: {
+ // Tufte sidenotes for blog posts that opt in via `sidenotes: true`.
+ // No-ops on every other page (see src/lib/rehype-sidenotes.mjs).
+ rehypePlugins: [rehypeSidenotes],
+ },
integrations: [
- sitemap(),
- tailwind(),
- react(),
+ sitemap(),
+ tailwind(),
+ react(),
expressiveCode({
themes: ["github-dark", "github-light"]
- }),
+ }),
mdx()
],
});
diff --git a/cspell.json b/cspell.json
index 3a250a4..57c3ce8 100644
--- a/cspell.json
+++ b/cspell.json
@@ -23,6 +23,7 @@
"devcontainer",
"doesn",
"expressibility",
+ "fnref",
"frontmatter",
"genericity",
"HDXLXC",
@@ -31,7 +32,9 @@
"instancetype",
"justfile",
"kindof",
+ "lede",
"linter",
+ "marginalia",
"MDX",
"meso",
"Meso",
@@ -45,18 +48,25 @@
"nonnull",
"NSURL",
"objc",
+ "opengraph",
"plx",
"Playwright",
"prb",
"pretraining",
"ripgrep",
+ "seeall",
+ "sidenote",
+ "sidenotes",
"sitemap",
"speedbump",
+ "stipple",
"subclassing",
"Tailgraph",
"Tailwindcss",
"textlint",
"todolist",
+ "Tufte",
+ "Twilight",
"Typesafe",
"uncategorized",
"Uncategorized",
diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md
new file mode 100644
index 0000000..62d343f
--- /dev/null
+++ b/docs/DESIGN_SYSTEM.md
@@ -0,0 +1,184 @@
+# Dispatches Design System — Repo Reference
+
+The site's visual system is **Twilight** ("Twilight 3b Deeper"). The vibe:
+quiet, plain, serif-on-paper — but the paper has cooled down. A single
+indigo-night-sky family runs every surface and text color; deep **plum** is the
+only chromatic accent. No illustrations, no gradients, no shadows.
+
+This file is the working reference for anyone (human or agent) making visual
+changes. For the full brand narrative — voice, tone, iconography, OG/favicon
+specs — read [`design-system/README.md`](./design-system/README.md). For
+prototypes, invoke the `/dispatches-design` skill.
+
+---
+
+## 1. How the system is wired in this repo
+
+| Concern | Lives in |
+| --- | --- |
+| Design tokens (CSS vars) + `@font-face` | `src/styles/tokens.css` |
+| Tailwind utilities backed by tokens | `tailwind.config.mjs` |
+| Global element/prose/editorial/article styles | `src/styles/global.css` |
+| Fonts (served) | `public/fonts/source-serif-4-latin*.woff2` (Inter via `@fontsource/inter`) |
+| Brand assets (served) | `public/favicon.svg`, `public/favicon-*.png`, `public/apple-touch-icon.png`, `public/android-chrome-192x192.png`, `public/og-image.png`, `public/og-image-dark.png` |
+| Off-page brand assets (not served) | `docs/design-system/assets/` (dark favicons, OG `.svg` sources, OG bare motifs, wordmark lockups) + `docs/design-system/fonts/` (Source Serif 4 TTF sources) |
+| Token reference (not built) | `docs/design-system/colors_and_type.css` |
+
+**Served assets are generated from the bundle — don't hand-edit the PNGs.** To
+regenerate, work from the `.svg` sources in `docs/design-system/assets/`. The
+woff2 fonts were latin-subset from the TTFs in `docs/design-system/fonts/` via
+`pyftsubset` (keeping the `wght` + `opsz` axes).
+
+---
+
+## 2. Tokens
+
+Defined in `src/styles/tokens.css` as CSS variables on `:root`, flipped under
+`html.dark`. Body text is intentionally **low-contrast** (`--fg`); only
+headings, `strong`, inline `code`, hovered links, and focus rings hit
+`--fg-strong` / the accent.
+
+| Semantic token | Light | Dark | Role |
+| --- | --- | --- | --- |
+| `--bg` | `#f6f6fa` (paper) | `#0f1126` (night) | page surface |
+| `--bg-elevated` | `rgba(22 24 48 / .04)` | `rgba(228 230 248 / .05)` | skip-link / subtle raised surface |
+| `--bg-frost` | `rgba(246 246 250 / .75)` | `rgba(15 17 38 / .68)` | header frosted-glass veil (opaque-ish so backdrop colors don't bleed/flash) |
+| `--bg-hover` | `rgba(22 24 48 / .09)` | `rgba(228 230 248 / .11)` | card/button hover |
+| `--fg` | `rgba(22 24 48 / .62)` | `rgba(228 230 248 / .76)` | body text (soft) |
+| `--fg-strong` | `#161830` (ink) | `#eff1fb` (moon) | headings, strong, code, focus |
+| `--fg-muted` | `rgba(22 24 48 / .61)` | `rgba(228 230 248 / .48)` | meta, nav dividers, footer |
+| `--border` | `rgba(22 24 48 / .13)` | `rgba(228 230 248 / .16)` | 1px outlines |
+| `--border-hover` | `rgba(22 24 48 / .25)` | `rgba(228 230 248 / .40)` | |
+| `--decoration` | `rgba(22 24 48 / .15)` | `rgba(228 230 248 / .22)` | prose link underline |
+| `--decoration-hover` | `rgba(22 24 48 / .25)` | `rgba(228 230 248 / .45)` | |
+| `--accent` / `--focus-ring` | `#762263` (plum-700) | `#d088b3` (plum-300) | the only chroma |
+| `--accent-soft` | `rgba(118 34 99 / .09)` | `rgba(208 136 179 / .13)` | inline-code chip |
+
+Raw anchors: `--ink #161830`, `--night #0f1126`, `--paper #f6f6fa`,
+`--moon #eff1fb`, `--plum-700 #762263`, `--plum-300 #d088b3`. The accent is also
+published as channel triplets (`--accent-rgb`) so `accent` opacity modifiers work.
+
+**Type:** `--font-sans` Inter (UI/headings, 400/600); `--font-serif` Source
+Serif 4 (prose `
`/`
`, variable 200–900 + italic); `--font-mono` system
+mono (Expressive Code). Scale `--text-xs`…`--text-4xl` (12→36px), prose leading
+`--leading-loose 1.75`. **Radii:** `sm .25rem` / `DEFAULT .375rem` / `lg .5rem` /
+`full`. Layout: `--container 640px`, `--page-py 8rem`. Motion: `--dur-base 300ms`,
+`--ease-in-out cubic-bezier(.65,0,.35,1)`.
+
+---
+
+## 3. Token → Tailwind-utility mapping
+
+`tailwind.config.mjs` maps the semantic tokens to color utilities that **auto-flip
+in dark mode**. Use the semantic utility; **do not add `dark:` color variants.**
+
+| Utility | Token | Notes |
+| --- | --- | --- |
+| `bg-bg`, `bg-bg-elevated`, `bg-bg-hover`, `bg-frost` | `--bg` / `--bg-elevated` / `--bg-hover` / `--bg-frost` | surfaces; `bg-frost` is the header glass veil |
+| `text-fg`, `text-fg-strong`, `text-muted` | `--fg` / `--fg-strong` / `--fg-muted` | text |
+| `border-border`, `hover:border-border-hover` | `--border` / `--border-hover` | bare `border` is also brand-colored |
+| `decoration-decoration`, `hover:decoration-decoration-hover` | `--decoration` / `--decoration-hover` | link underlines |
+| `text-accent`, `decoration-accent`, `ring-accent`, `outline-accent`, `bg-accent/NN` | `--accent` (channel-triplet) | supports opacity modifiers |
+| `bg-accent-soft` | `--accent-soft` | inline-code chip (baked alpha) |
+| `font-serif` | Source Serif 4 | prose body |
+
+> **Gotcha:** every token except `accent` bakes its own alpha, so opacity
+> modifiers like `text-fg/50` **silently produce invalid CSS** — use the whole
+> token. Only `accent` is channel-triplet form, so `bg-accent/10` etc. work.
+
+**Link hover behavior** (two distinct kinds):
+- *Prose* links (inside ``): hover flips text **and** underline to the
+ **accent** (`global.css` `@layer utilities`).
+- *UI* links (`Link.astro`): hover flips text to `fg-strong`, underline to accent.
+
+---
+
+## 4. Dos & Don'ts
+
+**Do:** serif prose body + sans headings/UI; low-contrast body with strong
+headings; flat-with-border cards; 300ms hover/focus cross-fades; static chevrons;
+plum focus ring; honor `prefers-reduced-motion` (handled in `tokens.css`).
+
+**Don't:**
+- ❌ Gradients (the flat indigo+plum pairing is the system; never blend them).
+- ❌ A second accent — plum is the only chroma. No green/red/blue.
+- ❌ Shadows / elevation (the frosted header backdrop is the only layering signal).
+- ❌ Emoji as iconography (and none in body copy).
+- ❌ Multi-column or content wider than **880px** (640 default/briefs; 540+280
+ gutter for Tufte blog posts).
+- ❌ Rounded cards with a colored left-border accent stripe; decorative SVGs,
+ mascots, "AI sparkle" gradients.
+- ❌ Raw hex / px / non-DS fonts in styles — use tokens. (The bundle ships
+ `design-system/_adherence.oxlintrc.json` as the canonical rule statement; it is
+ reference-only, not wired into this repo's ESLint.)
+
+---
+
+## 5. Dark mode
+
+Class-based (`darkMode: ["class"]`): the `` element gets `.dark`, which
+flips every `--token`, so token-backed utilities re-theme automatically. The
+toggle (light / dark / system, persisted to `localStorage`, transitions disabled
+for one frame on flip) lives in the inline script in `src/components/Head.astro`;
+the footer buttons drive it (`data-theme-button`, `aria-pressed`).
+
+---
+
+## 6. Listings — the editorial feed
+
+Home, blog index, and briefs index use **`ExcerptEntry.astro`** (the editorial
+feed): a `DATE · KIND · category` meta line, a large Source-Serif title, a 3-line
+excerpt, and a "Continue reading →" tail. Styles are the `.ex-*` / `.feed-*` /
+`.intro-lede` blocks in `global.css`. The whole entry is one link with an explicit
+`aria-label`. The **Projects** list and brief-category pages keep
+`ContentCard.astro` (bordered card — reads well for repo-link rows). The excerpt
+is currently the entry's `description`; body-derived excerpts are a future option
+(see §8). Helpers: `getBlogEntryProps` / `getBriefEntryProps` in
+`src/lib/contentCardHelpers.ts`.
+
+---
+
+## 7. Tufte sidenotes (opt-in per article)
+
+Blog posts can opt into a Tufte layout — a wider column (540 body + 280 gutter)
+with numbered footnotes floated into the right margin as sidenotes (collapsing
+inline below ~880px). **Existing posts keep the default** 640px column with
+footnotes collected at the bottom (brand-styled).
+
+**To opt a post in:** set `sidenotes: true` in its frontmatter (blog collection
+only — see `src/content/config.ts`). The author still writes standard markdown
+footnotes (`[^1]`); the `src/lib/rehype-sidenotes.mjs` plugin relocates each
+footnote inline next to its reference (as `` + `.sidenote`) and
+removes the bottom section. The `.post-blog` / `.sidenote` / `.marginalia` styles
+live in `global.css`. `.marginalia` (unanchored italic margin notes) is wired in
+CSS but not yet emitted — a hook for future use.
+
+---
+
+## 8. Deferred / future patterns
+
+- **Steel alternate palette** — a per-article aubergine-on-steel theme planned for
+ a forthcoming AI-and-artificial-light essay (whose color figures would fight the
+ plum). Not wired in. Activation: a `theme: "steel"` frontmatter field swapping a
+ class on the article root + a Steel token block. See `design-system/README.md`
+ §"Alternate theme".
+- **Body-derived excerpts** — replace the `description` excerpt in `ExcerptEntry`
+ with the first rendered paragraph (via a remark step or `render()`); the helper
+ boundary in `contentCardHelpers.ts` is where this would change.
+- **Per-post OG generator** — the static brand OG art is served sitewide; a
+ dynamic per-post card generator (on-brand stipple, not the old Tailgraph
+ gradient) could return later. Source art: `design-system/assets/og-image*.svg`.
+- **`.marginalia`** — unanchored margin notes for Tufte posts (CSS ready).
+
+---
+
+## 9. Resolved bundle contradiction (motion)
+
+The handoff's `README.md` §Motion is **authoritative**: the page-load fade-up
+(700ms) and 150ms stagger are **removed** — content paints immediately; only
+300ms hover/focus cross-fades remain and chevrons are static. The bundle's
+`_ds_manifest.json` "motion" card and the original `SKILL.md` still describe the
+old "700ms fade-up · 150ms stagger" — that text is **stale**. This repo follows
+the README: no entrance animation. The legacy `.animate` / `.show` CSS, old
+`Head.astro` `animate()` script, and existing `class="animate"` call sites were
+removed during migration.
diff --git a/docs/design-system/README.md b/docs/design-system/README.md
new file mode 100644
index 0000000..10121c0
--- /dev/null
+++ b/docs/design-system/README.md
@@ -0,0 +1,490 @@
+# Dispatches — Design System
+
+The brand+visual system for **Dispatches**, the personal technical blog at
+[plx.github.io](https://plx.github.io) by Paul Berman.
+
+The live site is an Astro Nano template, customized — what's captured here
+is the *current* visual direction (carefully read out of the codebase) plus
+the room to refine and explore alternatives the author wants.
+
+---
+
+## 1. Brand Context
+
+| | |
+| --- | --- |
+| **Site name** | Dispatches |
+| **Author** | Paul Berman |
+| **Tagline** | *Technical writing on topics of personal interest.* |
+| **URL** | https://plx.github.io |
+| **Stack** | Astro 5 + MDX + Tailwind + React (theme: [Astro Nano](https://github.com/markhorn-dev/astro-nano)) |
+| **Audience** | Working software engineers — heavy Swift, iOS, and recently-coding-agent communities |
+| **Type** | Static personal blog (GitHub Pages) |
+
+### What it is
+
+A long-running, low-frequency technical journal. The site distinguishes
+**three** kinds of content:
+
+| Section | What it's for | Length | Examples |
+| --- | --- | --- | --- |
+| **Blog** | Longer-form essays | 1.5k–10k+ words | "Generic Testing", "Meso-Optimization" |
+| **Briefs** | Short notes filed under categories (Swift Warts, Claude Code, Objective-C, Testing) | 100–800 words | "Lazy Sequences Decay Easily", "Claude Code's Subscription Pricing vs GPT-5" |
+| **Projects** | Long-lived write-ups of personal OSS tools | 500–4k words | `trop`, `hdxl-xctest-retrofit`, `agentic-navigation-guide` |
+
+There's also an **About** page in the nav.
+
+### Sources used
+
+- **Repo:** [`plx/plx.github.io`](https://github.com/plx/plx.github.io) — `main` branch.
+ Files read while building this system are listed in `SOURCES.md`.
+- **Live site:** https://plx.github.io
+- **Template lineage:** [Astro Nano](https://github.com/markhorn-dev/astro-nano)
+ (Tailwind, MIT). Several files (`BlockQuote`, `CallToAction`, `ExternalLink`)
+ are still leftover boilerplate from an earlier template (`accessible-astro-starter`)
+ and are *not* active on the live site — they're noted but not modeled.
+
+---
+
+## 2. Index — what's in this folder
+
+```
+.
+├── README.md ← you are here
+├── SKILL.md ← Agent Skill entry point (Claude Code-compatible)
+├── SOURCES.md ← exact files read from the upstream repo
+├── colors_and_type.css ← CSS variables: colors, type, motion, shape
+│
+├── assets/ ← logos, favicons, OG image
+│ ├── logo.jpeg ← the brand "logo" (a photograph of icy water)
+│ ├── logo-144.jpeg ← square crop @ 144px, used in OG image
+│ ├── favicon-32.png
+│ ├── favicon-48.png
+│ ├── apple-touch-icon.png
+│ └── android-chrome-192.png
+│
+├── preview/ ← cards that populate the Design System tab
+│ └── *.html
+│
+└── ui_kits/
+ └── blog/ ← clickable recreation of the live site
+ ├── README.md
+ ├── index.html ← runnable, scrollable, theme-toggleable
+ └── *.jsx ← Header, Footer, ContentCard, ArticleBody, …
+```
+
+---
+
+## 3. Visual Foundations
+
+> **The vibe.** Quiet, plain, serif-on-paper — but the paper has cooled
+> down. The system runs on a single indigo-night-sky family with deep
+> plum as the only chromatic accent. No illustrations, no gradients, no
+> wordmark on the live page itself (one was extracted as a standalone
+> asset in `assets/wordmark*.svg` for off-site reuse); the only
+> decoration on the page is the page-load fade-up and the
+> arrow that draws itself on card hover.
+
+### Palette — Twilight 3b “Deeper”
+
+Four anchor colors plus a two-step plum accent. Everything else is one
+of the anchors at an alpha.
+
+| Token | Hex | Role |
+| --- | --- | --- |
+| `--paper` | `#f6f6fa` | Light-mode background — cool desaturated near-white |
+| `--night` | `#0f1126` | Dark-mode background — indigo, a step toward midnight |
+| `--ink` | `#161830` | Light-mode strong text (and the source color for body text at 62% alpha) |
+| `--moon` | `#eff1fb` | Dark-mode strong text (and the source color for body text at 76% alpha) |
+| `--plum-700` | `#762263` | Accent — light mode. Focus ring, hovered prose link, code-chip tint, current-page underline. |
+| `--plum-300` | `#d088b3` | Accent — dark mode. Same roles, lifted for contrast on `--night`. |
+
+**Why low-contrast body text?** Same as before: it pushes hierarchy.
+Headings, hovered links, and `` are the only things that hit full
+`--ink` / `--moon`, so the eye walks the page along those rails. The
+narrow column (640 / 540 in the Tufte layout) and generous leading
+(1.7–1.75 on prose) make this comfortable for long reads.
+
+**Where the accent shows up.**
+- Focus ring (replaces the previous blue-600 / blue-400)
+- Hovered prose link — text and underline color both flip to accent
+- `` chip backdrop — `accent-soft` (the accent at ≈9% / 13% alpha)
+- Current-page underline in the header nav
+- Marker color in numbered footnotes / sidenotes (subtle, on hover)
+
+**Where the accent does *not* show up.** Filled buttons, banners,
+gradients, illustrations. The system has none of those.
+
+### Alternate theme (planned, for one specific essay)
+
+A forthcoming essay draws an analogy between AI and artificial light
+and will lean heavily on color-theory illustrations (spectrograms,
+high-vs-low CRI samples, single-wavelength examples). The plum-on-indigo
+Twilight palette would fight with those figures. The **Steel** palette
+from `explorations/05-colors.html` is held as the per-article alternate:
+
+| Token | Steel light | Steel dark |
+| --- | --- | --- |
+| `--bg` | `#f1f2f4` | `#1c2026` |
+| `--fg-strong` | `#1c2026` | `#eef0f3` |
+| `--accent` | `#5a1e4a` (aubergine) | `#b78097` |
+
+Not wired into the live site yet. When the essay is closer, we'll add a
+`theme: "steel"` frontmatter field that swaps a class on the article
+root, leaving the rest of the site on Twilight.
+
+### Typography
+
+| Use | Family | Weight |
+| --- | --- | --- |
+| Headings, nav, UI | **Inter** | 600 (semibold) |
+| Prose body (``, `
` inside ``) | **Source Serif 4** | 400 / 400 italic *(variable; whole range available)* |
+| Code (inline + blocks) | Monospace (Expressive Code default) | 400/500 |
+
+Inter loads at 400 + 600. Source Serif 4 ships locally as two variable
+font files (regular + italic) and is loaded via `@font-face` from
+`fonts/`. **No third family** is used. JetBrains Mono is included here
+as a reasonable code stand-in for static mocks.
+
+> **Heads up: this is a change from the live site.** The Astro template
+> ships Lora as the prose serif; the uploaded font files replace it with
+> Source Serif 4. The two have meaningfully different feels — Source
+> Serif 4 is taller, more spacious, and slightly more "editorial /
+> scholarly"; Lora is rounder and a bit more contemporary. If you want
+> the live site to follow this design system, you'll need to update the
+> Astro project to load Source Serif 4 instead of `@fontsource/lora`.
+
+The serif-for-prose / sans-for-everything-else split is the strongest
+typographic move on the site. It's worth preserving in any new direction —
+it makes long-form posts feel like a quiet book, not a SaaS landing page.
+
+### Spacing & shape
+
+- Container: **640px** (`max-w-screen-sm`). Single column, centered.
+- Header: fixed top, `py-5`, frosted glass via `backdrop-blur-sm` over an
+ opaque-ish veil (`--bg-frost`: paper at 75% light / night at 68% dark). No
+ `saturate()` — an earlier `saturate-200` amplified saturated content scrolling
+ behind the bar (e.g. a list title mid-hover flipping to plum) into a flash.
+- Main content: `py-32` — *very* generous top/bottom padding.
+- Cards: `rounded-lg` (8px), thin border, no shadow.
+- Buttons: `rounded` (6px) or `rounded-full` for icon buttons.
+- **No shadow system** on the live site. None. Elevation is communicated
+ with the frosted header backdrop only.
+
+### Background, imagery & illustration
+
+The site has **no decorative illustrations, no full-bleed photography, no
+gradients, no repeating patterns, no textures.** The body is a flat single
+cool-paper or indigo-night-sky surface — *except* for one piece of brand
+imagery, the OG image.
+
+**`assets/og-image.{svg,png}`** — the brand image and Open Graph card.
+A monochrome ink drawing of a starry sky in the engraving tradition:
+dense plum-ink stippling (Bold tier, ~21k dots on a 280×155 grid) on
+the cool `--paper` surface, with stars left as **reserves** where the
+paper shows through. Density falls off linearly from ~100% at the top
+edge to ~0% at the horizon (78% of the way down), so the bottom of the
+frame matches the rest of the brand's flat-paper feel. Wordmark sits
+on the lower-left in Inter Semibold with the tagline in Source Serif 4
+italic — typography is *inside* the SVG, so it travels with any export.
+
+A bare variant without the wordmark (`assets/og-image-bare.svg/.png`)
+is available for use as a recurring background motif on essay landings.
+
+A dark-mode OG variant lives at **`assets/og-image-dark.svg/.png`**
+(and `-dark-bare.svg/.png`) — same plum-300 ink stippling on the
+indigo `--night` surface, identical geometry. It ships as a brand
+asset but isn't wired into the page metadata: `og:image:dark` is not
+part of the Open Graph protocol and no major platform honors it, so
+social cards use the single light `og-image.png` until a supported
+mechanism exists.
+
+### Favicon
+
+Same engraving family, tuned for tiny rendering. `assets/favicon.svg`
+is the canonical adaptive favicon — it embeds a `prefers-color-scheme`
+media query inside the SVG, so a single file flips between modes.
+
+Specific PNG sizes are also shipped because (a) some browsers ignore
+SVG color-scheme switching and (b) at small sizes a downsampled SVG
+goes muddy. Each PNG is rendered **natively** at its target size with
+disproportionately larger dots and added jaggedness — at 16px the dot
+radius is **7×** the canonical 144 size, the grid is **1.5× denser**,
+and the density floor is raised by **+0.45** so the image stays
+saturated. Roughly **10% of dots in the top band flip to moon color**
+(`#f0eafa` on dark, `#3a0e30` on light) as "bright stars" — these
+break up the monotone field and survive aggressive downscaling.
+
+Files in `assets/`:
+- `favicon.svg` — adaptive primary
+- `favicon-light-{16,32,48,180,192}.png` and `favicon-dark-*` — raster fallbacks
+- `apple-touch-icon.png` — light variant at 180px (iOS rarely honors dark)
+- `android-chrome-192.png` — light variant at 192px
+
+The previous icy-water photo was retained for one revision; it has now
+been removed.
+
+### Motion
+
+Interaction feedback only. No entrance animation, no decorative motion.
+
+- **Hover transitions.** Color, background, border, and text-decoration
+ changes are **300ms ease-in-out** (`--dur-base` / `--ease-in-out`).
+ Everything is on a transition; nothing snaps.
+- **No page-load fade-up.** Content paints immediately. The legacy
+ `.animate` / `.show` pattern, its 700ms timing, and the 150ms index
+ stagger have all been removed. `` in the UI kit is now a
+ no-op passthrough kept only so old call sites compile.
+- **No arrow-draw choreography.** The `ContentCard` right-chevron and
+ `BackToPrev` left-chevron are now static glyphs — visible at rest,
+ no transform on hover. The arrow still indicates affordance; it just
+ doesn't perform.
+- **No bounces, no springs, no rotation, no scale-on-press.**
+- **Reduced motion respected.** A `@media (prefers-reduced-motion: reduce)`
+ block collapses every transition/animation to ~one frame, so users who
+ ask their OS to reduce motion get instant state changes, not missing
+ feedback. Lives in `colors_and_type.css` (and mirrored in the blog kit's
+ `styles.css`).
+- **Theme toggle disables all transitions** for one frame (a `
+
+
+
+
+
+
light · plum on paper
+
+
+
+
dark · plum-300 on night
+
+
+
+
+
+
+