Style Guide
The complete design contract: surfaces and ink, the six-hue semantic palette, the type pairing, the spacing scale and page grid, the icon catalog, the shadow + z-index ladder, the radius scale and hairline weight, the motion vocabulary, and the zebra-list four-band model. Every DreamLake page reads from this contract.
Color
Color splits across three concerns: the surface stack (where things sit), the ink pair (what users read), and a six-color semantic palette (what's happening). No primary/secondary distinction — semantics come first, and the palette stays at six forever.
Surfaces
Three surface tokens — --bg, --panel-bg, --rail-bg — relate differently in light
vs. dark. Light separates --bg from --panel-bg (subtle cream-on-cream lift) and
shares --rail-bg with --panel-bg. Dark collapses --panel-bg onto --bg; panels
lift via 1px --faint + shadow alone, and --rail-bg drops below --bg so the rail
recedes into a deeper plane.
Ink
Two text colors carry every glyph on the page: --ink for primary content, --muted
for secondary. There is no third gray — anything paler is achieved with opacity on
--ink, not by introducing another token. Both flip in dark mode against the inverted
surface stack.
| Token | Light | Dark | Used on |
|---|---|---|---|
--ink | #1a1a1a | #ececec | Body copy, titles, ledger rows, kbd glyphs |
--muted | #6b6b6b | #8a8a8a | Eyebrows, meta cells, placeholder text, neutral status |
Semantic palette
All domain coloring — pipeline kinds, run statuses, transform types, event types,
branch identities, release kinds, diff ops — pulls from one shared 6-color set. Every
other page maps its concepts onto these six. dreamlake-pipelines is the canonical
source.
When adding a new concept
- Open
pipelines-canvas.jsx.PIPE_KIND_COLOR,PIPE_STATUS, andPIPE_FLOW_LABELare the canonical mappings. The legend (PipeLegend) is what users learn the palette from. - Map by function, not vibe. "Ingest" is a successful inflow → green
(
#1f8f4a), not blue. "RC release" is an active candidate → blue (#23aaff), not amber. "Draft release" is pre-success → green. The semantic bucket trumps any aesthetic preference. - Never invent a near-miss hex.
#3a7bd5,#c08a2a,#5a8fbfwere all bugs in earlier timetravel work — they tried to be "a slightly different blue/amber" and just looked broken next to pipelines. Distinguish two same-bucket concepts via glyph + label, not a third color. tokens.cssis for surface chrome, not domain coloring.--accenthappens to equal#23aaffbecause the active/running color is also the UI accent. Don't add--ok/--errortotokens.css; keep the six hex values inline at use sites sogreptells the truth.
Why six and not more
- Six is the ceiling for a legend. The pipelines flow legend is already 5 rows tall
(
running/queued/stalled/error/ok); a 7th hue pushes it past the threshold where users can hold the mapping in working memory. - Each hue does double duty. Every color carries a status meaning and a kind meaning and a domain-concept meaning. That collision is the point — if "ingest" and "ok" both mean data flowed in successfully, they should look the same.
- Outliers get caught at review. A
grep '#[0-9a-f]\{6\}'across*.jsxshould return only these six values plus surface tokens. Anything else is either a typo, a leftover from prototyping, or a missed alignment.
Typography
Two families. Mono carries identity (IDs, paths, hashes, durations, eyebrows, ledger rows, kbd, status). UI carries narrative (titles, body, nav labels, button copy). The pairing is fixed.
The OpenType feature settings ss01 and cv11 are turned on globally — they are the
two stylistic sets Inter Tight ships that affect punctuation density and the ampersand
shape. Set once on <body> in the library's styles.css; no per-component opt-in.
Spacing & layout
Eleven spacing steps from 2 to 40px. There's no token — values are used inline. Most padding lives between 6–18px; the 2px row gap is sacred. The big jumps at the top (20 → 28 → 40) are for section breathing room, not row rhythm.
Spacing scale
Page grid
The docs site itself is the canonical example: a 3-column grid at 1320px max width —
240px left rail, 1fr content, 240px right rail — with 0 gaps between
columns. Everything inside lays out against this frame.
Breakpoints
Two breakpoints in current use, both for the docs shell:
max-[1100px]— drop the right rail (TOC). Content widens to fill the remaining space; the left nav stays at 240px.max-[880px]— drop the left nav too. Single-column layout; chrome collapses.
No "mobile-first" set of named breakpoints. Everything is desktop-first; the two overrides are the only places the layout changes.
Iconography
Every icon across dreamlake, pipelines, timetravel, profile, and notebook is
sourced from lucide.dev. Render via lucide-react so the path
data matches upstream byte-for-byte — no hand-copied SVG. Stroke 1.4–1.6 at small
sizes (12–16px); 1.1–1.25 at large (24–28px).
Categories
A representative cut across the six domain categories the design-guide lists: navigation, files & data, actions, layout & views, status, and DreamLake-specific domain glyphs.
Migration backlog
A handful of legacy custom SVGs still ship inline in source apps (wireframe-shared.jsx,
timetravel-view.jsx). They read like Lucide but aren't — replace with the canonical
name on next touch.
| where | custom kind | lucide replacement |
|---|---|---|
| WireIcon | panel-hide | panel-left-close |
| WireIcon | panel-show | panel-left-open |
| WireIcon | tree-view | list-tree |
| WireIcon | col-view | columns-3 |
| WireIcon | grid-view | layout-grid |
| WireIcon | note | file-text |
| WireIcon | project | folder |
| WireIcon | bindr | book-marked (or domain glyph hash) |
| pipelines-view | inline pipeline SVG | route |
| timetravel-view | inline history SVG | history |
Elevation & depth
Four shadow tints layered onto the panel surface. Light mode uses ink-tinted black at low opacity; dark mode uses pure black at higher opacity (the same custom property tracks both themes).
Shadow scale
tint-1 is the resting panel base; tint-2 lifts a popover off the page; tint-3
floats a modal or vim-help overlay. The inset variant marks pressed states and focused
inputs.
Z-index layers
A small fixed set of layers — never invent ad-hoc z-indices.
| Layer | z-index | What sits there |
|---|---|---|
| Page content | 0 (auto) | Body, prose, specimens — the default plane |
| Sticky in-page chrome | [3] | Sticky <H2> headings inside the prose |
| Search backdrop | 40 | Dim/blur layer behind the cmd+K palette when open |
| Search panel | [49] | The cmd+K results card surface |
| Page chrome (topbar) | 50 | The sticky doc topbar |
| Search input (open) | [71] | The search input portals out of the topbar when the palette is open, sitting above every other layer so it's the only thing receiving keystrokes |
Component-internal stacking (the ThemeToggle puck, the ZebraRunSpecimen
selection ring) uses small ints (z-0 / z-10 / z-[1] / z-[2]) inside its own
isolate boundary. Those don't compose with the page ladder above — they're private to
the component.
Border & radius
Five named radii smallest to largest, plus a single hairline weight that draws every structural border on the page.
Radius scale
Five named roles, smallest to largest: 3 inline glyphs, 6 badges, 8 cards, 10 panels +
inputs, 12 list rows. --radius (10px) is the canonical panel + input radius — also
the resting search bar and palette button. List rows step up to 12 (rest, hover, and
the selection-run ring all share that radius).
Hairlines & dividers
Every structural border on the page is 1px --faint — rgba(0,0,0,.08) in light,
rgba(255,255,255,.09) in dark. The contract is that panels, gridlines, and dividers
share this single hairline weight; "incomplete" or annotated boundaries get the dashed
variant --faint-dashed instead. The chip background --chip-bg is the same ink-tint
family at lower opacity, used for inline code, tag chips, and kbd glyphs.
Motion
Three tiers, two custom easings, one prohibition.
Durations & easings
Tier 1 — feedback (100–150ms · linear ease)
100ms— search palette result rows on hover.120ms— nav-rail rows on hover and selection.150ms— Tailwind's default; everything else color/border transitions through here (prose links, copy/anchor buttons, palette inner borders).
Tier 2 — chrome morph (180–250ms)
180ms— search input grows wider when the cmd+K palette opens.220ms— palette backdrop fades + blurs in.250ms— topbar merged-state crossfade as the H1 scrolls past, brand → breadcrumb swap, palette panel slide. Usescubic-bezier(.4, .7, .3, 1)— a soft ease-out with a settled tail.
Tier 3 — spring (420ms)
420ms— theme-toggle puck. Usescubic-bezier(.34, 1.45, .55, 1)for an overshoot that reads as a button-press, not a crossfade. The only animation in the kit that doesn't return-to-rest by 250ms.
When not to animate
Never on box-shadow — except the focus-visible outline. Animating shadow
composites every frame and reads as "the panel is breathing"; the resting shadow is
meant to be invisible after first paint.
Honor prefers-reduced-motion everywhere. The theme toggle's spring puck and any
slide-in chrome collapse to instant transitions when the user has reduced-motion set;
nothing else moves under that media query.
Zebra lists
Every list row is in exactly one of four bands at any moment: base, zebra, hover, selected. Each band picks a different hue axis so they never collide.
The four-band model
Zebra is structural — a cool ink-step that carries scan rhythm. Hover is
transient — a soft tint of the active blue that previews the eventual selection.
Selected is committed — saturated blue with white text. Base is "this row is
here," equal to the panel --bg.
Rows are not flush. A gap: 2px between every row lets the panel --bg show through,
which is what makes the alternating zebra fills tile rather than smear and lets a
hovered or selected row read as a discrete tile lifting out of the list — without any
per-row border.
Per-state colors
Precedence is strict — only one rule wins per render:
A row that is both odd-parity and hovered paints with --color-row-hover, not the
zebra fill — hover overrides zebra. A selected row paints --color-row-selected
regardless of parity or hover state. Text colors do not change between bands; the one
exception is bright-blue selected, which forces every span inside the row to
--color-row-selected-fg (white) so a saturated selection reads as one identity.
Hover is bg-only. No inset border, no ring. The 2px row gap already detaches the hovered tile from its neighbours.
Multi-row runs
When two or more rows are selected, consecutive selected rows merge into one visual block. The two ways to draw the block are not interchangeable; pick by what the selection means:
- Ring — a 2px accent border around the run, rows underneath keep their own zebra parity showing through. Use for transient selection (bulk-delete, bulk-tag, bulk-move). Loud and short-lived.
- Fill — every selected row paints
--color-row-selected, optionally with a left rail. Use for persistent selection that drives downstream UI (compare panel, side-by-side charts, batch editor). Commits visually.
The specimen below renders the ring form, mirroring the design-guide source. The
wrapper is transparent — its ::after pseudo-element draws the 2px accent border,
sitting at z:2 above the rows so the stroke isn't occluded by row backgrounds. Rows
inside the run keep their zebra parity uninterrupted; the ring is purely an outer
indicator.