Tokens
Tokens
:root. 39 are documented here across 8 groups. Consume them through the Tailwind preset — never by copying values, which is exactly how they drifted between repos before.Using them
Two imports. The preset supplies the Tailwind theme; the stylesheet supplies the custom properties the theme points at. You need both — the preset alone produces utilities that resolve to nothing.
// tailwind.config.ts — in any BTS repo
import btsPreset from "bts-storybook/tailwind-preset";
export default {
presets: [btsPreset],
content: ["./src/**/*.{ts,tsx}"],
};// The tokens themselves live in the stylesheet, not the config.
// Import it once, at your application root.
import "bts-storybook/styles.css";Surfaces
Five levels of background, from the true black the app sits on up to the hover fill. Depth in BTS comes from these steps and a hairline border — not from drop shadows.
--background0 0% 0%bg-backgroundThe page. True black, always.
--background-secondary0 0% 4%bg-background-secondarySidebars and the first lift off the page.
--background-tertiary0 0% 10%bg-background-tertiaryInset wells, code blocks, the tertiary button fill.
--background-hover0 0% 15%bg-background-hoverHover fill on tertiary surfaces.
--background-input0 0% 10%bg-background-inputField interiors.
--surface0 0% 7.5%bg-surfaceElevated chrome — modals, headers, toolbars, cards.
--card0 0% 4%bg-cardCard fill.
--popover0 0% 7.5%bg-popoverFloating panels — menus, tooltips, popovers.
Text
Four steps of emphasis. Body copy is `foreground-secondary`; `foreground` is reserved for headings, active states and anything that needs to win.
--foreground0 0% 100%text-foregroundHeadings, active items, primary emphasis.
--foreground-secondary0 0% 63%text-foreground-secondaryBody copy and inactive navigation.
--foreground-tertiary0 0% 42%text-foreground-tertiaryMetadata, placeholders, column headers.
--foreground-disabled0 0% 29%text-foreground-disabledUnavailable text.
Interactive
BTS has no brand hue. Primary is white, and emphasis is carried by the convex fill on `.btn-white` rather than by colour.
--primary0 0% 100%bg-primaryPrimary action fill.
--primary-foreground0 0% 0%text-primary-foregroundText on a primary fill.
--secondary0 0% 10%bg-secondarySecondary action fill.
--muted0 0% 10%bg-mutedDe-emphasised fill.
--muted-foreground0 0% 63%text-muted-foregroundText on a muted fill.
--accent0 0% 15%bg-accentHover fill for ghost and outline actions.
Status
The only saturated colour in the system. Reserve it for meaning — never for decoration.
--success112 100% 55%text-success / bg-successPositive deltas and confirmations. A deliberately neon green.
--warning38 92% 50%bg-warningAttention without failure. Also the hue Lantern's own glow is built from.
--destructive0 84% 60%bg-destructiveDeletion and irreversible actions.
Caveat `.btn-destructive` hard-codes hue 0 70% and does not derive from this token — changing the token will not move the button (audit gap #9).
--like0 84% 60%Reactions.
Caveat Dead. Identical to --destructive, zero references, no Tailwind mapping (audit gap #12).
Borders and ring
Two weights. `border-subtle` is the resting state for cards and rows; `border` is what they move to on hover.
--border0 0% 17%border-borderHover and active edges, dividers that need to read.
--border-subtle0 0% 12%border-border-subtleResting edges on cards, rows and containers.
--input0 0% 17%border-inputField borders.
--ring0 0% 100%ring-ringThe focus ring. White, offset by two pixels against the background.
Radius
One base value, with Tailwind deriving `sm` and `md` from it.
--radius0.5remrounded-lgThe base. Cards, dialogs, buttons.
--radius-xl1remrounded-xlLarge containers.
--radius-sm0.25remDeclared but never read.
Caveat Tailwind's `rounded-sm` is calc(var(--radius) - 4px), not this token (audit gap #7).
--radius-lg0.75remDeclared but never read.
Caveat Contradicts `rounded-lg`, which resolves to 0.5rem via --radius (audit gap #7).
Chart
Five series colours, ordered by priority. Series 1 is white so the primary line always wins.
--chart-10 0% 100%text-chart-1Primary series.
--chart-2142 71% 45%text-chart-2Second series.
Caveat Not the same green as --success (112 100% 55%) — a chart will not match its own trend delta (audit gap #11).
--chart-338 92% 50%text-chart-3Third series.
--chart-40 84% 60%text-chart-4Fourth series.
--chart-5217 91% 60%text-chart-5Fifth series.
Custom classes
Not every part of the system is a Tailwind utility. These are hand-written in index.css, and a component that uses one renders unstyled without the stylesheet — so they are part of the contract.
.btn-whiteConvex buttonsThe primary button fill — a white gradient with rim light and drop shadow.
.btn-white-pillConvex buttonsThe same treatment, fully rounded. Used on logged-out surfaces.
.btn-grey-pillConvex buttonsSecondary pill. The only convex variant with a disabled state.
.btn-destructiveConvex buttonsDestructive primary, same convex treatment in red.
.badge-primaryBadgesGradient badge fill matching .btn-white. Supplies no radius — the caller sets it.
.nav-itemNavigationSidebar item base, plus :hover and .active.
.stat-cardCardsDashboard metric surface.
.post-cardCardsFeed post surface, with a --static variant that opts out of hover.
.chart-containerCardsChart frame.
.filter-pillControlsRounded filter chip, plus .active.
.space-switcher-tileSpace switcherThe tile, its gradient border, active ring and drag states (~25 classes).
.holo-cardEffectsCursor-tracked holographic shine, glare and border glow.
.shimmerEffectsLoading shimmer. Currently inert — see the keyframe collision below.
.prose-productTypographyRich-text styling for product descriptions.
.text-linkTypographyUnderline-on-hover inline link.
.scrollbar-thinScrollbarsFour-pixel scrollbar for sidebars and compact panels.
Known gaps
Defects the component audit found in the token layer. They are published rather than hidden — a system that conceals its own bugs gets worked around instead of fixed.
#1 @keyframes shimmer is defined twice with different bodies
index.css animates background-position; the Tailwind theme animates transform. Same name, one document — the theme copy lands later and wins, so .shimmer renders as a static gradient. Rename the CSS copy to shimmer-bg.
#3 Inter is not loaded anywhere in the library
The app pulls it in via a <link> in index.html. Storybook has no equivalent, so -apple-system masks the problem on macOS and the wrong typeface ships on Linux CI. Self-host the woff2 or add a preview-head.
#4 darkMode is class-based, but nothing sets the class
Tokens live on :root so colour is fine, but no dark: variant ever activates. ui/alert.tsx already has dark:border-destructive. Set class="dark" on the root element.
#5 theme.screens is not pinned
The whole max-md: / md: contract rides Tailwind's default 768px, but the preset only extends and never fixes screens. A consumer overriding them silently breaks the dual-breakpoint rule.
#8 The --button-white-* tokens are stale
They describe a 73.7% → 100% ramp; .btn-white hard-codes 96% → 64%. Zero references repo-wide. Wire the class to the tokens or delete them.
#9 .btn-destructive does not derive from --destructive
The class uses hue 0 70% throughout; the token is 0 84% 60%. Changing the token will not move the button.
#10 .nav-item.active uses a hard-coded hsl(0 0% 9.5%)
A magic literal, used three times, sitting between --surface (7.5%) and --background-tertiary (10%). It needs a name before the navigation primitive ships.
#19 @tailwindcss/typography is installed but not registered
prose prose-invert is already inert in production. Porting as-is is correct; registering the plugin would make the design system diverge from the app. Decide deliberately.
Where these come from
docs/component-audit.md §2.3 in the bts-storybook repo. That file is the source; this page is a view of it. Fix a gap there and update it here in the same change.