Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .claude/skills/dispatches-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions .conductor/settings.toml
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ dist/
.astro/

# conductor build artifacts
.conductor/
.conductor/*
!.conductor/settings.toml

# dependencies
node_modules/
Expand All @@ -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
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 11 additions & 4 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
],
});
10 changes: 10 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"devcontainer",
"doesn",
"expressibility",
"fnref",
"frontmatter",
"genericity",
"HDXLXC",
Expand All @@ -31,7 +32,9 @@
"instancetype",
"justfile",
"kindof",
"lede",
"linter",
"marginalia",
"MDX",
"meso",
"Meso",
Expand All @@ -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",
Expand Down
184 changes: 184 additions & 0 deletions docs/DESIGN_SYSTEM.md
Original file line number Diff line number Diff line change
@@ -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 `<p>`/`<li>`, 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 `<article>`): 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 `<html>` 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 `<sup class="fnref">` + `.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.
Loading
Loading