CaseSolo UI design system
Living record of what is true in code. MASTER freeze: planning/design/MASTER.md. Visual primitives: docs/dev/dashboard-visual-system.md.
Tokens stay the single source of truth. Components consume names, never hex.
T0 — tokens
Source: src/app/globals.css (:root, .dark, @theme) and public/brand/brand-tokens.css.
| Token | Role | |---|---| | --cs-navy | Ground, ink, shell | | --cs-amber | One-match accent, gold CTA fill | | --cs-amber-600 | Amber text on light | | --cs-blue | Links, focus, in-app primary | | --cs-font-display | Page titles (alias of branded Inter) | | --font-sans / --font-heading | Inter UI | | --font-mono | Data / code | | --background --foreground --card --primary --muted --border --ring --destructive | Semantic shadcn roles | | --shell-bg --shell-rail --shell-panel --shell-flyout | App chrome (always navy) | | --elevation-1..4 | Layered navy-tinted shadows | | --radius | 0.625rem base; sm/md/lg/xl derived | | --motion-ease --motion-fast --motion-base | Spring ease, 180ms / 220ms |
Raw palette lives only here. Components use bg-background, text-primary, variant="gold", .ds-eyebrow, .dashboard-card-chrome.
T1 — primitives (src/components/ui)
| Primitive | Variants | Required states | |---|---|---| | Button | default (Counsel Blue), gold (Signal Amber), outline, secondary, ghost, destructive, link | default / hover / focus-visible / active / disabled / loading | | Card | default, sm | hover bloom only when the card is a control (via dashboard-border-motion) | | Input Select Textarea | default | focus-visible, disabled, aria-invalid | | Badge | default, secondary, destructive, outline, ghost, link, gold | focus-visible | | EmptyState | empty, error, loading × sm/default/lg | role=status on loading, role=alert on error | | Skeleton | .dashboard-skeleton-line | reduced-motion: no shimmer |
Gold CTA: navy type on amber (color: #0b1b34). Never white on amber.
T2 — page kit (src/components/page)
| Pattern | Rule | |---|---| | PageShell | Only page root. Width full \| medium \| narrow. Layout owns padding. | | PageHeader | Exactly one per page. Branded Inter h1. Optional eyebrow. Actions right. One gold button max. | | Eyebrow | Mission label: 0.65rem, 600, tracking 0.16em, amber. | | StatCard / StatGrid | Tabular stat-figure. Semantic trend color only. | | Toolbar | Search grows, filters, actions. |
Chrome
- Sidebar: The Citation lockup, amber active hairline, navy rail.
- Main:
.dashboard-shell.dashboard-glow(navy + blue/amber radials + precision grid + grain). - Top of app: 1px amber match line (same as public header).
- Cards:
.dashboard-card-chromequiet lifted navy. Hover: amber edge opacity,translateY(-1px). - Marketing stays on
.mkt-cta/.mkt-eyebrow/.mkt-panel. App reuses the same tokens via.ds-eyebrow/ gold variant.
Usage rules
- No page-local hex,
text-gray-*, orbg-whiteon dashboard surfaces. - No second page wrapper (
container,mx-auto max-w-*) —PageShellwidth prop. - Empty, loading, and error are three treatments (
EmptyStatevariants). - Hover only on actual controls.
- One primary CTA per view. Gold is that CTA. Everything else is outline/ghost/blue.
- Numbers use
.tabular/.stat-figure. - Page titles use
font-display(thePageHeaderdefault).
Drift ratchet (manual until CI)
Grep for: #0b0b16, #7c3aed, from-blue-500 to-cyan, violet-500, conic-gradient, text-gray-. Each hit is a named migration onto a token that already exists.