Skip to content

Design & motion

The system is calm on purpose: elevation from shadow, one accent, a compressed scale, and motion that carries information rather than decoration.

Calm by construction

The look is near-monochrome and dark-first. The canvas is a cool charcoal; surfaces and text are neutral. The only chromatic color in the whole interface is a single accent — Constructive blue — and it earns its place by being rare: a focus ring, the active nav row, a link, a “new” dot. When one color means “pay attention,” attention is cheap to direct.

What you won’t find is decoration standing in for hierarchy. No gradients, no glow, no second accent competing for the eye. Light mode exists and stays functional, but the system was tuned in the dark, where restraint shows up fastest.

Elevation is shadow, not chrome

Depth is carried by shadow, not by outlines. The system defines an eight-step surface ladder paired one-to-one with a shadow ladder: bg-surface-3 goes with shadow-surface-3, and so on up. A card reads as lifted because of the soft, layered shadow beneath it — not because a heavy border boxes it in.

Borders still exist, but as hairlines. A border is the foreground color mixed roughly twelve percent into transparency, used faint — a quiet seam between regions, not a frame around them. The result is surfaces that feel placed in space rather than drawn on a page.

A compressed scale

Type runs small and dense. 13px is the workhorse — navigation, body copy, table cells, inputs all live there. Page titles step up to 28px, section headings to 16px, and captions down to 12 and 11 for badges. It’s all Geist; weight and size do the hierarchy, never a third typeface.

Small type rewards small habits. Headings balance so they don’t leave a lonely last word; body wraps pretty; numbers in tables use tabular figures so columns line up. Tight density and generous whitespace aren’t opposites here — the type is compact so the space around it can be open.

Motion is information

Motion follows one rule: every animation has to answer why. Spatial continuity, a state change, feedback for an action, or softening a jarring jump — those are reasons. “It looks cool” is not, and on a frequent element it’s a cost. So the test comes before the animation: an action you take dozens of times a day, or trigger from the keyboard, gets no motion; a modal or drawer you open occasionally can afford a little.

The mechanics fall out of that. Animate only transform and opacity, never layout properties — those are smooth and cheap; width and margin are neither. Entrances ease out; interaction feedback stays under 200ms so the interface never feels like it’s waiting on itself. And reduced-motion is honored as the framework means it: fewer and gentler, not zero — opacity and color stay, movement drops.

Tokens, not magic numbers

Consistency here isn’t willpower, it’s tokens. Durations and easing curves are CSS variables, and every component consumes them — var(--ease-out), duration-[var(--dur)] — instead of hand-picking a number. Three duration tiers and a small set of strong curves cover the whole interface, so motion is reviewable in one place rather than scattered across files.

globals.css
/* Strong curves — built-in easings are too weak; never ease-in on UI. */
--ease-out:    cubic-bezier(0.23, 1, 0.32, 1);    /* enter / exit */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);   /* on-screen move / morph */
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);    /* drawers / sheets */

/* "Bigger thing moves, slower spring" — three tiers, nothing in between. */
--dur-fast: 120ms;  /* hover, focus, taps */
--dur:      160ms;  /* dropdowns, tabs, accordion */
--dur-slow: 240ms;  /* dialogs, drawers, stepped flows */

Calm isn’t the absence of design — it’s the discipline of it. The same restraint shapes the blocks you install: they inherit these tokens, so a surface dropped into your app reads the way the rest of it does. Retune those tokens for your brand and every block follows — that recipe is Theme to your brand. For why those blocks carry their data path with them, see why blocks.