Space & shape
A 4px spatial base, real control sizes, one stacking vocabulary, a radius scale, three elevations, and motion.
Spacing — 4px base
All spacing is a multiple of 4px, and the base is a token — --spacing: 0.25rem — so
Tailwind's spacing utilities (p-4, gap-2, …) derive from Lumen, not from a coincidence.
The common rungs:
4 · 8 · 12 · 16 · 24 · 32 · 48 · 64
Size — controls & the tap floor
The dimensional decisions the kit actually ships, as tokens (use h-(--size-control-md) etc.):
| Token | Value | Use |
|---|---|---|
--size-control-sm | 32px | Compact rows (Button sm) |
--size-control-md | 36px | The default control (Button md, Input) |
--size-control-lg | 44px | Hero / touch-first (Button lg) |
--size-tap-min | 24px | The hard floor — no pressable target below this (WCAG 2.5.8) |
Layers — one stacking vocabulary
Z-index is a scale, not an arms race. Use z-(--layer-modal) etc.; never a bare number.
| Token | Value | What lives there |
|---|---|---|
--layer-raised | 10 | Lifted cards, sticky affordances inside content |
--layer-sticky | 20 | Sticky headers/rails |
--layer-overlay | 40 | Scrims (the --overlay wash) |
--layer-modal | 50 | Sheets, dialogs |
--layer-popover | 60 | Menus, selects, popovers — may open above a modal |
--layer-toast | 70 | Toasts — outlive the modal that spawned them |
--layer-tooltip | 80 | Tooltips — beat everything |
Radius
The rounded-* utilities are bound to this scale (not Tailwind's defaults — Tailwind's md
is 6px, Lumen's is 12px). Each step has one job.
| Token | Value | Use |
|---|---|---|
sm | 8px | Chips and small controls |
md | 12px | Inputs and buttons |
lg | 20px | Cards |
xl | 28px | Sheets and dialogs |
full | 999px | Pills, dots, avatars, the tab bar |
Strokes
| Token | Value | Use |
|---|---|---|
--stroke-hairline | 1px | Dividers inside a card, and the edge of a control |
--stroke-emphasis | 2px | Focus rings, invalid borders |
Elevation
Depth comes from fill and shadow, not from lines. A card stands apart from the ground by being one step
lighter and carrying shadow-sm; it has no border. Three warm-ink shadows, plus the one scrim.
shadow-sm / shadow-lg / shadow-xl utilities resolve to these. At Night shadows barely read, so
surfaces also step lighter as they rise (--surface-raised).
Each shadow lists its wide layer first. On iOS and Android, NativeWind draws only a shadow's first
layer, so that layer must carry the lift. A view that clips its content, with overflow-hidden, loses its
shadow on iOS, so a clipped card keeps its line instead. So does a card that sits inside a clipped region,
such as Halo's panel, where the region would cut the shadow at its edges. A horizontal strip of cards pads
its content so the shadow fits inside the scroll view.
| Token | Use |
|---|---|
--shadow-sm | A card on the ground — the resting surface |
--shadow-lg | Floating chrome: the tab bar, popovers, the Ask bar |
--shadow-xl | A sheet rising over content |
--overlay | The wash behind sheets/dialogs — warm ink by Day, deeper by Night; never pure black |
Motion
Purposeful and quiet. The living quality comes from one shared rhythm, not from movement everywhere.
| Token | Value | Use |
|---|---|---|
--duration-quick | 120ms | A press, a toggle, a chip selecting |
--duration-standard | 220ms | A card or row changing state; content swapping in place |
--duration-gentle | 320ms | Sheets rising, screens entering |
--vital-tempo | 1.25s | The heartbeat loader and the breathing mark |
--ease-standard | cubic-bezier(0.2, 0, 0, 1) | Things that arrive and settle |
--ease-exit | cubic-bezier(0.3, 0, 1, 1) | Things that leave |
Under reduced motion, movement becomes an opacity fade of --duration-quick or less, and loops stop.
All of the numeric scales above are mirrored as data in the package (radius, spacing,
sizes, layers, icons from lumen) and machine-checked against the CSS by the
truth script — drift fails the build.