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.

--bglight
#fffefa
Page / canvas. The pipelines dot grid paints on this.
--panel-bglight
#fcfbf7
Panels, popovers, mid-column lists. Half-step darker than --bg.
--rail-bglight
#fcfbf7
Left navbar. Shares --panel-bg in light.
--bgdark
#2e2e35
Body / canvas. Lifted above the rail.
--panel-bgdark
#2e2e35
Flattens onto --bg. Lift via hairline + shadow.
--rail-bgdark
#2b2b31
A step deeper than --bg. Navbar recedes.

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.

TokenLightDarkUsed on
--ink#1a1a1a#ecececBody copy, titles, ledger rows, kbd glyphs
--muted#6b6b6b#8a8a8aEyebrows, 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.

active · running#23aaff
also: --accent, primary CTAs
pipelinesrunningtransformrunning edge
timetravelclassifyuploadmainrc
ok · source#1f8f4a
also: passed, complete
pipelinesoksourceok edge
timetravelnormalizewebhookingestdraftadd
model · merge · human#7c5bd9
also: code / notebook authorship
pipelinesmodel kind
timetravelmergeprpublictag op
stale · scheduled#c0922e
also: warning, needs review
pipelinesfilterstalestalled edge
timetravelfilterschedulemodify
error · sink · quarantine#c8513b
also: destructive actions
pipelinessinkerrorerror edge
timetravelpatchincidentremove
idle · queued · muted#9c907a
also: placeholder rows, neutral chips
pipelinesidle edge
timetravelmanualtag-onlyforkqueued

When adding a new concept

  1. Open pipelines-canvas.jsx. PIPE_KIND_COLOR, PIPE_STATUS, and PIPE_FLOW_LABEL are the canonical mappings. The legend (PipeLegend) is what users learn the palette from.
  2. 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.
  3. Never invent a near-miss hex. #3a7bd5, #c08a2a, #5a8fbf were 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.
  4. tokens.css is for surface chrome, not domain coloring. --accent happens to equal #23aaff because the active/running color is also the UI accent. Don't add --ok / --error to tokens.css; keep the six hex values inline at use sites so grep tells 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 *.jsx should 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.

h1 · sectioncard titles, view headers
Recent runs across this branch
Inter Tight 600 · 22 / 27 · −0.012em
h2 · subsectiongroups inside a panel
Schedule and triggers
Inter Tight 600 · 16 / 22
h3 · panel headinspector inner heads
Last successful run
Inter Tight 600 · 13 / 18
bodyprose, descriptions, button copy
A pipeline emits one column per run. Filters operate on those columns; sinks subscribe to them.
Inter Tight 400 · 13 / 20
row · ledgerzebra lists, ID columns, paths
classify · acme/prod · 4m 12s · ok
Fira Code 500 · 12 / 18
meta · timestampsecondary cell text in rows
2026-04-12 · 14:23 · run_8a14e2
Fira Code 500 · 10 / 14 · +0.04em
eyebrowsmall caps section labels
Sources · live now
Fira Code 600 · 9.5 · +0.12em · UPPER
taginline glyph metadata
v1.4.2 · 3 columns · 2 sinks
Fira Code 500 · 9 · +0.04em
kbdkeyboard chips inline in copy
Kopens command palette
Fira Code 600 · 10 · 5% ink fill

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

space-2
2px
space-4
4px
space-6
6px
space-8
8px
space-10
10px
space-12
12px
space-14
14px
space-16
16px
space-20
20px
space-28
28px
space-40
40px

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.

css
.page {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr) 240px;
  max-width: 1320px;
  margin-inline: auto;
}

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.

navigation
arrow-up
arrow-down
arrow-left
arrow-right
chevron-down
chevron-right
panel-left-close
panel-left-open
files & data
folder
folder-open
file
file-text
database
hash
book-marked
actions
plus
trash-2
pencil
copy
check
x
more-horizontal
layout & views
list
list-tree
columns-3
layout-grid
rows-3
status
circle
circle-check
circle-x
circle-alert
clock
loader
domain
route
git-branch
history
tag
merge

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.

wherecustom kindlucide replacement
WireIconpanel-hidepanel-left-close
WireIconpanel-showpanel-left-open
WireIcontree-viewlist-tree
WireIconcol-viewcolumns-3
WireIcongrid-viewlayout-grid
WireIconnotefile-text
WireIconprojectfolder
WireIconbindrbook-marked (or domain glyph hash)
pipelines-viewinline pipeline SVGroute
timetravel-viewinline history SVGhistory

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.

tint-1
.06 / .45
Panel base shadow.
tint-2
.10 / .60
Lifted panel · floating popover.
tint-3
.16 / .75
Modal · vim-help overlay.
inset
.08 / .50
Pressed states, focused inputs.

Z-index layers

A small fixed set of layers — never invent ad-hoc z-indices.

Layerz-indexWhat sits there
Page content0 (auto)Body, prose, specimens — the default plane
Sticky in-page chrome[3]Sticky <H2> headings inside the prose
Search backdrop40Dim/blur layer behind the cmd+K palette when open
Search panel[49]The cmd+K results card surface
Page chrome (topbar)50The 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).

3px
kbd · tag
Inline glyph elements only.
6px
badge
Inline kbd-like chips and version badges.
8px
card
Cards, state tables, and lab tiles.
10px--radius
panel · input
Panels, popovers, search input, palette buttons.
12px
list row
Zebra-list rows at rest, hover, and selection-run wrappers.

Hairlines & dividers

Every structural border on the page is 1px --faintrgba(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.

--faint
.08 light · .09 dark
Structural borders. Panels, gridlines, dividers.
--faint-dashed
.22 / .22
Annotated underlines for "incomplete" boundaries and metadata.
--chip-bg
.04 / .05
Inline code, tag chips, kbd backgrounds.

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. Uses cubic-bezier(.4, .7, .3, 1) — a soft ease-out with a settled tail.

Tier 3 — spring (420ms)

  • 420ms — theme-toggle puck. Uses cubic-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:

selected  >  hover  >  zebra (odd)  >  base (even)

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.

state
light
dark
base · even
#fffefa
#2b2b31
zebra · odd
#f3f2ee
#36363c
hover · selectable
#d9e6f7
#3d4856
hover · warm
#f3e6cc
#3d4856
selected
#2174d9 · fg #fff
#2f86e6 · fg #fff
focus-visible
--accent · inset 2px
--accent · inset 2px

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.