Color
Color
Components consume semantic colors named by purpose. Those colors map to different primitives in light and dark themes, so switching themes changes values without changing class names.
Interface and surfaces
--color-bgLight gray-50 · Dark gray-950
--color-surfaceLight white · Dark gray-900
--color-surface-hoverLight gray-100 · Dark gray-800
--color-trackLight gray-200 · Dark gray-950
The groove behind segmented controls (Tabs solid, Segmented). It is defined by a relationship rather than a fixed grey: always one step deeper than the card surface, and in both themes the raised pill must read as closer to the viewer. Do not substitute the hovered surface — in light mode it is only 3.3% lighter than white, and in dark mode it is lighter than the pill, which inverts the elevation.
--color-subtleLight gray-100 · Dark gray-800
Static area backgrounds: grouping containers, board columns, swim lanes, note strips. It currently resolves to the same value as the hovered surface but means something different — writing hover: for a permanent background is a semantic mismatch.
--color-mutedLight gray-100 · Dark gray-800
Same value and meaning as the subtle background; the name exists to match the shadcn/ui vocabulary, where --muted is a weak background. Before 0.28.0 this name held the secondary text colour — the exact opposite of the ecosystem — and it has since been renamed to the secondary text token below.
--color-borderLight gray-200 · Dark gray-800
--color-hairlineLight transparent · Dark gray-800 (same as border)
Use only with border-* on elevated surfaces. It is transparent in light mode; text-, bg-, or fill- would become invisible, so use border or muted-foreground for fill and text.
Text
--color-foregroundLight gray-900 · Dark gray-50
--color-muted-foregroundLight gray-600 · Dark gray-400
Descriptive copy, timestamps, placeholders, disabled labels. Named --color-muted before 0.28.0.
Brand and emphasis
--color-primaryLight brand-700 · Dark brand-400
The matching text colour is -foreground, but it only matches this solid fill: in light mode it is plain white. Painting a tint of your own (-subtle, or /12) and reaching for text-x-foreground by naming intuition gives you white on white and the text disappears — while dark mode looks right, so whoever develops in dark mode never sees it. On a tint the text colour is the semantic colour itself: solid = bg-x + text-x-foreground, soft = bg-x-subtle + text-x, outline = border-x + text-x.
--color-primary-hoverLight brand-500 · Dark brand-500
Every semantic hover token picks the step between the light and dark values, so it moves one step towards lower contrast in both themes.
--color-primary-subtleLight brand-50 · Dark surface + 16% brand-400
Tinted backgrounds: notice bars, selected rows, Tag and Badge fills, the active sidebar item. The direction flips in dark mode — instead of getting lighter, the semantic colour is mixed into the surface, so the fill stays deeper than the surface and only picks up the hue. Do not derive these by mixing the accent towards white; that is wrong in dark mode.
--color-primary-borderLight brand-200 · Dark surface + 34% brand-400
--color-ringLight brand-500 · Dark brand-400
Status
--color-dangerLight danger-700 · Dark danger-400
The matching text colour is -foreground, but it only matches this solid fill: in light mode it is plain white. Painting a tint of your own (-subtle, or /12) and reaching for text-x-foreground by naming intuition gives you white on white and the text disappears — while dark mode looks right, so whoever develops in dark mode never sees it. On a tint the text colour is the semantic colour itself: solid = bg-x + text-x-foreground, soft = bg-x-subtle + text-x, outline = border-x + text-x.
--color-danger-hoverLight danger-500 · Dark danger-500
--color-danger-subtleLight danger-50 · Dark surface + 16% danger-400
Tinted backgrounds: notice bars, selected rows, Tag and Badge fills, the active sidebar item. The direction flips in dark mode — instead of getting lighter, the semantic colour is mixed into the surface, so the fill stays deeper than the surface and only picks up the hue. Do not derive these by mixing the accent towards white; that is wrong in dark mode.
--color-danger-borderLight danger-200 · Dark surface + 34% danger-400
--color-successLight success-700 · Dark success-500
The matching text colour is -foreground, but it only matches this solid fill: in light mode it is plain white. Painting a tint of your own (-subtle, or /12) and reaching for text-x-foreground by naming intuition gives you white on white and the text disappears — while dark mode looks right, so whoever develops in dark mode never sees it. On a tint the text colour is the semantic colour itself: solid = bg-x + text-x-foreground, soft = bg-x-subtle + text-x, outline = border-x + text-x.
--color-success-hoverLight success-600 · Dark success-600
--color-success-subtleLight success-50 · Dark surface + 16% success-500
Tinted backgrounds: notice bars, selected rows, Tag and Badge fills, the active sidebar item. The direction flips in dark mode — instead of getting lighter, the semantic colour is mixed into the surface, so the fill stays deeper than the surface and only picks up the hue. Do not derive these by mixing the accent towards white; that is wrong in dark mode.
--color-success-borderLight success-200 · Dark surface + 34% success-500
--color-warningLight warning-700 · Dark warning-500
The matching text colour is -foreground, but it only matches this solid fill: in light mode it is plain white. Painting a tint of your own (-subtle, or /12) and reaching for text-x-foreground by naming intuition gives you white on white and the text disappears — while dark mode looks right, so whoever develops in dark mode never sees it. On a tint the text colour is the semantic colour itself: solid = bg-x + text-x-foreground, soft = bg-x-subtle + text-x, outline = border-x + text-x.
--color-warning-hoverLight warning-600 · Dark warning-600
--color-warning-subtleLight warning-50 · Dark surface + 16% warning-500
Tinted backgrounds: notice bars, selected rows, Tag and Badge fills, the active sidebar item. The direction flips in dark mode — instead of getting lighter, the semantic colour is mixed into the surface, so the fill stays deeper than the surface and only picks up the hue. Do not derive these by mixing the accent towards white; that is wrong in dark mode.
--color-warning-borderLight warning-200 · Dark surface + 34% warning-500
--color-infoLight info-700 · Dark info-400
Hints, explanations and neutral notices, deliberately separate from the primary colour: primary means “this is actionable”, info means “this is an explanation”. Both are blues but 30° apart in hue — borrowing the primary colour for notices spends the scarcest attention budget on the page on descriptive copy. The matching text colour is -foreground, but it only matches this solid fill: in light mode it is plain white. Painting a tint of your own (-subtle, or /12) and reaching for text-x-foreground by naming intuition gives you white on white and the text disappears — while dark mode looks right, so whoever develops in dark mode never sees it. On a tint the text colour is the semantic colour itself: solid = bg-x + text-x-foreground, soft = bg-x-subtle + text-x, outline = border-x + text-x.
--color-info-hoverLight info-500 · Dark info-500
--color-info-subtleLight info-50 · Dark surface + 16% info-400
Tinted backgrounds: notice bars, selected rows, Tag and Badge fills, the active sidebar item. The direction flips in dark mode — instead of getting lighter, the semantic colour is mixed into the surface, so the fill stays deeper than the surface and only picks up the hue. Do not derive these by mixing the accent towards white; that is wrong in dark mode.
--color-info-borderLight info-200 · Dark surface + 34% info-400
Chart colors
Colors for data series, consumed directly through var(). Light and dark themes tune luminance independently for contrast.
Semantic color ramps
Steps 50-300 are the tints used for notice bars, selected rows and Tag fills; 400-700 are the accent steps. The tints are hand-tuned in OKLCH rather than derived with mix(): sRGB interpolation shifts the hue, and every project picks a different percentage — yet tints cover the largest area of an admin UI.
Primitive gray scale
The shared palette in primitives.css does not change with the theme. Semantic colors are derived from it.