Tokens
OKLCH variables, their Tailwind class, and the dual-theme system used by the dashboard.
All tokens live in apps/client/app/globals.css. The system supports two themes via class-based switching on <html>: .light and .dark. Every token name resolves to a different value per theme; the same Tailwind utility (bg-card, text-foreground, border-border, etc.) does the right thing in both contexts.
The base scale is reverse-engineered from vercel.com's Geist design system (research lives at docs/design-research/vercel/STYLE-GUIDE.md). Body text is 15px (set on html); shadcn primitives, text-sm (~13px), and text-base (15px) all derive from there.
Always reference the semantic class. Never hardcode an OKLCH literal in a component.
Surfaces (lifted from page bg)
| Token | Class | Light | Dark |
|---|---|---|---|
--background | bg-background | oklch(0.985 0 0) (~#FAFAFA) | oklch(0 0 0) (#000) |
--foreground | text-foreground | oklch(0.21 0 0) (~#171717) | oklch(0.94 0 0) (~#EDEDED) |
--card | bg-card | oklch(1 0 0) (#FFF) | oklch(0.087 0 0) (~#0A0A0A) |
--popover | bg-popover | oklch(1 0 0) | oklch(0.087 0 0) |
--muted | bg-muted | oklch(0.95 0 0) | oklch(0.21 0 0) |
--muted-foreground | text-muted-foreground | oklch(0.45 0 0) | oklch(0.62 0 0) |
The page bg (--background) is the flatter color; cards, popovers, and other elevated surfaces use --card, which sits one lightness step toward the inverse. This avoids alpha-stacking issues when one card is over another.
CTA — paper-white on black, black on white
| Token | Class | Light | Dark |
|---|---|---|---|
--primary | bg-primary | oklch(0.21 0 0) (near-black) | oklch(0.94 0 0) (paper-white) |
--primary-foreground | text-primary-foreground | oklch(0.98 0 0) | oklch(0 0 0) |
--accent | bg-accent | oklch(0 0 0 / 0.06) (transparent black) | oklch(1 0 0 / 0.06) (transparent white) |
--accent-foreground | text-accent-foreground | oklch(0.21 0 0) | oklch(0.94 0 0) |
--primary is the inverse-of-bg fill — the canonical CTA. --accent is not a brand color; it's the hover-overlay (transparent) used by shadcn primitives to indicate active/hover state. For brand color use --brand below.
Brand & info — Vercel blue
| Token | Class | Both themes |
|---|---|---|
--brand | bg-brand, text-brand | oklch(0.591 0.227 256) (~hsla(212, 100%, 48%, 1)) |
--brand-foreground | text-brand-foreground | oklch(0.98 0 0) |
--info | bg-info, text-info | alias of --brand |
--ring | ring-ring | --brand (focus rings use the brand color) |
Use --brand for links, badges that mean "info", inline highlights, and focus rings. Do not use it as a button bg — buttons stay paper-white/black via --primary. This mirrors Vercel's split: blue conveys "informational accent", contrast conveys "primary action".
Status colors
| Token | Class | Light | Dark |
|---|---|---|---|
--success | bg-success, text-success | oklch(0.55 0.13 152) | oklch(0.62 0.135 152) |
--warning | bg-warning, text-warning | oklch(0.74 0.16 70) (amber) | same |
--destructive | bg-destructive, text-destructive | oklch(0.59 0.21 22) (red) | same |
Use the <StatusBadge> primitive (components/dashboard/status-badge.tsx) to render these consistently. Always combine color + icon/dot + text — never use color alone (Vercel's a11y rule, see STYLE-GUIDE §3.5).
Stage-coded brand colors
Reserved for environment indicators when we surface deploy stages. Not used by anything yet, but documented so we don't repaint them inconsistently.
| Token | Class | Hex | Use |
|---|---|---|---|
--develop | text-develop | #0a72ef | Development env |
--preview | text-preview | #de1d8d | Preview deployments |
--ship | text-ship | #ff5b4f | Production / shipped |
Borders — box-shadow, not 1px solid
The Vercel pattern: card borders are shadows, not border: 1px solid. This avoids box-sizing math (the border doesn't consume layout space) and stack-composes with drop shadows.
--border-hairline: 0 0 0 1px <hairline alpha>;
--border-hairline-inset: inset 0 0 0 1px <subtler alpha>;
--border-hairline-large: hairline + drop shadows for raised cards;Three utilities are exposed in @layer utilities:
border-hairline— hairline outer border, the defaultborder-hairline-inset— subtle inner line (use for table headers, scroll containers)border-hairline-lg— composite border + drop shadow for floating cards / popovers
Existing components using border-border (the 1px solid version) keep working. Migrate them gradually as they're rebuilt — don't rewrite untouched shadcn primitives.
Charts
--chart-1 through --chart-5 are monochrome (pure foreground descending to muted), with --chart-5 aliasing --brand for emphasis when one series should pop.
Sidebar
A small sub-scale (--sidebar, --sidebar-foreground, --sidebar-border, --sidebar-accent, …) shifts a couple of lightness steps so the dashboard's nav rail reads as a distinct surface without breaking the monochrome.
Custom utility classes
Defined in globals.css @layer utilities:
.label-mono— small uppercase mono label at 0.18em letter-spacing, foreground at 55% alpha. Wrap in<MonoLabel>(see primitives)..border-hairline,.border-hairline-inset,.border-hairline-lg— box-shadow-as-border (see above)..shadow-soft/.shadow-soft-lg— drop shadows tuned for the dark base..card-premium/.card-lift/.card-glow— landing-page-only effects (UseDeploy hero cards). Don't reach for these in the dashboard.
Radius scale
--radius: 0.5rem (8px effective at base 15px)
--radius-sm: calc(var(--radius) - 2px)
--radius-md: var(--radius)
--radius-lg: calc(var(--radius) + 2px)
--radius-xl: calc(var(--radius) + 6px)Reach for rounded-md for most cards, rounded-sm for chips, rounded-lg for floating overlays.
Typography
Geist Sans + Geist Mono are loaded in app/layout.tsx. Use font-sans (default) or font-mono. Body has font-feature-settings: "cv11", "ss01" enabled for Geist's stylistic alternates — don't override globally.
Body font-size is 15px on <html> — between Vercel's 14 (denser) and the default 16 (more accessible). All text-* utilities derive from this base.