Dreamweaver · style guide
The twilight hour
Dreamweaver is read at bedtime, so it lives in the half-hour after sunset: deep indigo and violet for the sky, warm parchment for the words, and one gold thread — lamplight — for the thing you should touch next. Everything on this page is drawn with the real tokens and utilities, so this is both the guide and the proof.
Foundations
Four rules. Everything else on this page follows from them.
One theme, dark: no light mode, no prefers-color-scheme, no light-dark() — color-scheme: dark is set once so native controls and scrollbars follow.
Components use semantic tokens only — bg-surface, text-ink-muted. Tailwind's stock palette is cleared, so there is no bg-slate-900 to reach for. Need a colour? Add a role.
The accent goes on the primary action, the selected chip, the live caret, the thread and one title. If two gold things compete on a screen, one of them is wrong.
The whimsy is in the sky and the titles; the controls stay Apple-plain: 44pt targets, 16px control text, visible focus, reduced motion honoured.
Colour
Two tiers. Primitives are raw OKLCH ramps with no meaning; roles say what a colour is for. Only roles reach components.
Primitives
plain CSS vars · no utilities · don't use directly/* tokens.css — primitives live on :root, in OKLCH (perceptual, wide-gamut) */
:root {
--twilight-950: oklch(0.165 0.055 282); /* the canvas */
--gold-400: oklch(0.81 0.15 79); /* the accent */
--mist-50: oklch(0.955 0.012 85); /* the ink */
}Roles
hex is measured live, composited over the canvasCanvas & surfaces
Stacked from the sky up. Surfaces are translucent so the stars show through — never paint an opaque hex behind content.
- …opaque--color-canvas·bg-canvas
The page. Set once on <html>; the sky draws over it.
- …opaque--color-surface·bg-surface
Cards, lists, the book's pages — anything content sits on.
- …opaque--color-surface-raised·bg-surface-raised
A surface on a surface: hover rows, menus, the details block.
- …opaque--color-surface-overlay·bg-surface-overlay
Sheets, dialogs, toasts. The top of the stack.
- …opaque--color-surface-sunken·bg-surface-sunken
Wells inside a surface: inputs at rest, code blocks.
Ink
Warm white text on a cool sky. Three steps, no more: if you need a fourth, you need a different layout.
- …opaque--color-ink·text-ink
Body text, titles, icons.
- …opaque--color-ink-muted·text-ink-muted
Secondary text: labels, metadata, helper copy.
- …opaque--color-ink-faint·text-ink-faint
Placeholders, disabled text, decorative numerals.
- …opaque--color-ink-on-accent·text-ink-on-accent
Text on a gold fill (the primary button).
Lines
Hairlines are mist at low alpha, so they sit on any surface without a second token.
- …opaque--color-line·bg-line
Dividers, card borders.
- …opaque--color-line-strong·bg-line-strong
Control borders, the thread at rest.
Accent — gold
The one warm thing in a cool world. Spend it on: the primary action, the selected chip, the live caret, the thread, one gilded title. Not on decoration.
- …opaque--color-accent·text-accent
Primary fills and strokes; the thread.
- …opaque--color-accent-bright·text-accent-bright
Highlights: selected-chip text, gradient tops.
- …opaque--color-accent-deep·bg-accent-deep
Gradient bottoms, pressed states.
- …opaque--color-accent-soft·bg-accent-soft
Tinted fills behind a selected or hovered thing.
- …opaque--color-accent-glow·bg-accent-glow
Focus rings, halos, the pulse. Alpha on purpose.
Atmosphere
The sky's glows. Background only — never text, never a fill on a control.
- …opaque--color-glow-indigo·bg-glow-indigo
The big glow, top-left.
- …opaque--color-glow-violet·bg-glow-violet
The dusk glow, bottom-right.
- …opaque--color-glow-gold·bg-glow-gold
A faint lamplight low on the horizon.
Signals
Status colours, each with a soft fill for the message block behind it. Green means finished, red means the loom stopped.
- …opaque--color-ok·text-ok
Finished. Dot, text.
- …opaque--color-ok-soft·bg-ok-soft
Background for a finished notice.
- …opaque--color-danger·text-danger
Failed / error text and dots.
- …opaque--color-danger-soft·bg-danger-soft
Background for an error block.
- …opaque--color-focus·bg-focus
Keyboard focus ring — always visible, always gold.
/* tokens.css — roles are Tailwind theme variables, so they are utilities too */
@theme {
--color-*: initial; /* no stock palette: roles only */
--color-canvas: var(--twilight-950);
--color-surface: color-mix(in oklch, var(--twilight-900) 84%, transparent);
--color-ink: var(--mist-50);
--color-accent: var(--gold-400);
--color-accent-glow: color-mix(in oklch, var(--gold-400) 38%, transparent);
}
/* a component */
<section className="card">…</section> // bg-surface + line + shadow
<p className="text-ink-muted">…</p>
<span className="bg-accent-soft text-accent-bright">…</span>
<div style={{ boxShadow: "var(--shadow-glow)" }} /> // or: shadow-glowSurfaces
Depth comes from stacking translucent layers over the sky, not from grey drop shadows. Four levels, in order.
card · bg-surfaceCards, the library list, the book's pages. 84% opaque — stars glimmer faintly behind.
bg-surface-raisedHovered rows, the prompt disclosure, popovers.
bg-surface-sunkenWells: inputs at rest, code blocks.
- Do
- Use
cardfor anything that holds content; it carries surface, line and shadow together. - Nest at most one level: surface → raised. Overlay is for things that float.
- Let the sky do the atmosphere — a card on the canvas is already interesting.
- Don't
- Don't paint an opaque colour behind content; it cuts a hole in the sky.
- Don't add
backdrop-filter: blurto surfaces — the sky animates behind them and phones would repaint every frame. - Don't invent a fifth level. If the hierarchy needs more, use spacing or type.
Typography
Four faces, each with one job. The personality is in the titles; the rest stays out of the way.
opsz 144 · SOFT 100 · WONK 1 — the storybook voice. Titles only, via .display.
Made for long reads on screens. Every sentence a child will hear is set in it.
Labels, buttons, helper text. Neutral on purpose — it is the part that should feel like a phone.
Status words, timestamps, page numbers, the prompt. Anything the machine said.
Scale
named by role · size + leading travel togethertext-display-2xlPage title — one per page, usually gildedWeave a booktext-display-xlBook title on its pageThe Lighthouse at the Edge of the Maptext-display-lgSection title inside a page (settings preview)A Door in the Fogtext-display-mdList row titleMila and the Paper Moontext-readingReading text (Literata)The sea was quiet that night, and the lamp kept its slow, patient turning.text-bodyUI body textPick who it’s for and how it should feel.text-labelField labels, nav linksWhere does it happen?text-captionMetadata, helper text, timestampsFinished · 2h agotext-eyebrowSpaced uppercase label above a sectionFrom the author’s notes
.display.gildedGold gradient clipped to the letterforms, with a soft drop-shadow glow behind. One per page — the page title or the book title, never both.
.eyebrowSmall, spaced, uppercase, muted. It labels what comes next; it is never the thing itself.
<h1 className="display gilded text-display-2xl">Weave a book</h1>
<p className="prose-book text-ink-muted">…</p> // reading text
<span className="eyebrow">From the author's notes</span>
<time className="font-mono text-caption text-ink-muted">2h ago</time>Controls
One vocabulary for everything touchable, sitting on the HIG floors: 44pt hit targets, 16px control text (so iOS never zooms), a visible gold focus ring.
controlText input, textarea, select. Sunken at rest, lifts to surface and glows on focus.
chip · chip-onA radio option drawn as a pill. The selected one borrows the accent: gold border, soft fill, a halo.
btn · btn-quietOne primary action per screen, gold, 50pt tall, full width on a phone. Everything else is quiet.
tap · linkText links get a full-height hit target with `tap`; links inside prose use `link` (gold underline, ink text).
SignalsStatus dots and the soft blocks behind messages. The live dot pulses in gold.
The loom stopped: the model returned no pages.
It won't try again on its own.
<input className="control" />
<span className="chip peer-checked:chip-on">Cozy</span>
<button className="btn sm:w-auto">Weave my book</button>
<button className="btn-quiet">Weave another</button>
<Link className="tap px-3 text-ink-muted hover:text-ink">Library</Link>
<a className="link">Weave the first book</a>Motion
Motion tells you the loom is working. Every animation maps to a real state; none are decoration. All stop under prefers-reduced-motion.
.threadTen segments, one per page. Breathing while thinking, weaving on the active page, lit gold when done.
.caret · .riseThe caret blinks at the end of text that is still arriving. `rise` lifts a finished thing into place once.
and the lamp turned, and turned, and
.shimmerA placeholder that is being painted — the cover, while Gemini works.
EasingTwo curves, both tokens.
- --ease-rise cubic-bezier(.2,.7,.2,1) — things arriving
- --ease-drift cubic-bezier(.4,0,.6,1) — the sky
The sky
The one signature. A fixed layer behind every page: a twilight gradient, three slow glows, a seeded field of stars that twinkle, and — every eighteen seconds — one that falls.
.sky gradient → .sky-glow ×3 (indigo, violet, gold; blurred, drifting 70–110s) → .sky-stars (110 SVG circles) → .sky-fall.
Stars come from makeStars(seed, n), a seeded PRNG in lib/sky.ts, so the server and the browser draw the same sky and nothing hydrates.
Only opacity and transform animate — compositor work, no layout, no paint. Under prefers-reduced-motion it all holds still.
// app/layout.tsx
<body>
<Sky /> {/* position: fixed; z-index: -1; aria-hidden */}
<header>…</header>
<main>…</main>
</body>
/* content overlays it on translucent surfaces */
<section className="card">…</section>Using the tokens
Tailwind v4 theme variables are the system. Utilities reference the role variable, so a subtree can re-skin itself by redefining a role.
<li className="bg-surface hover:bg-surface-raised
border-line text-ink">
<span className="text-ink-muted">…</span>
</li>.thread > i[data-state="done"] {
background: var(--color-accent);
box-shadow: 0 0 8px var(--color-accent-glow);
}/* the book page, later: warmer pages */
.book-page {
--color-surface: color-mix(in oklch,
var(--twilight-900) 70%, var(--gold-700));
}
/* every bg-surface inside follows *//* tokens.css → @theme */
--color-warning: var(--gold-300);
--color-warning-soft: color-mix(in oklch,
var(--gold-700) 60%, transparent);
/* then add a row in styleguide/tokens.ts */- Do
- Reach for a role utility first (
bg-surface,text-ink-muted,border-line,shadow-glow). - Use opacity modifiers on roles when you need a tint:
border-danger/30. - Put new shapes in tokens.css as
--radius-*,--shadow-*,--text-*; they become utilities.
- Don't
- Don't reference a primitive (
--gold-400) outside tokens.css. - Don't add a
prefers-color-schemeblock anywhere. There is one theme. - Don't write
#hexoroklch()in a component; it is a missing role.