Color
Save the Dogs colors flow through three layers: raw primitive ramps (--p-*, theme-independent) feed semantic tokens (--color-*, re-pointed per skin) which feed the Tailwind utilities (bg-accent, text-muted…). Components read semantic tokens only, so flipping :root ↔ .dark re-skins everything. Dark is the default; :root holds the LIGHT values and .dark overrides them. Flip the theme in the top bar to watch the semantic swatches move while the primitive ramps stay put.
- Primitives (--p-*)
- Theme-independent raw palette: the full beryl accent ramp, the cool neutral ramp, status ramps, data-viz series, and hairline/glow alphas. Never change between light and dark.
- Semantic (--color-*)
- Purpose-named tokens (accent, surface, text, border, status…) defined on :root for LIGHT and overridden in .dark for DARK. This is what you should reach for in app code.
- Tailwind utilities
- @theme inline maps the semantic tokens to classes: bg-accent / text-accent / bg-surface / text-muted / bg-good etc. The gray-* ramp is remapped to the neutrals and indigo-* to beryl 1:1.
- No bg-bg / text-text
- There is no bg-bg or text-text utility. For page background / primary text use bg-background / text-foreground (legacy bridges) or the arbitrary form bg-[var(--color-bg)] / text-[var(--color-text)].
Beryl accent ramp
The Save the Dogs teal — the brand accent primitive (--p-beryl-50 … --p-beryl-950), 50 lightest → 950 darkest. Theme-independent: identical in light and dark. The semantic accent tokens pick stops out of this ramp (light leans darker for AA contrast, dark leans brighter). Also re-exposed 1:1 as the Tailwind indigo-* ramp.
--p-beryl-50#e6f8f5--p-beryl-100#c4efe8--p-beryl-200#90dfd4--p-beryl-300#50cbbf--p-beryl-400#1bb6ab--p-beryl-500#13a89f--p-beryl-600#00948d--p-beryl-700#007671--p-beryl-800#005654--p-beryl-900#053b3a--p-beryl-950#072928- Light accent stops
- --color-accent = beryl-700, accent-strong = beryl-800, accent-hover = beryl-600, accent-soft = beryl-50. Shifted one stop darker so text-accent on accent-soft clears WCAG AA.
- Dark accent stops
- --color-accent = beryl-500, accent-strong = beryl-600, accent-hover = beryl-400, accent-soft = beryl-950.
- indigo-* alias
- @theme inline maps --color-indigo-50…950 to these exact beryl values, so every legacy indigo-* utility renders Save the Dogs teal with no edits.
Neutral ramp
The cool neutral primitive (--p-neutral-0 … --p-neutral-950). Note the Save the Dogs ordering: 0 is DARKEST and 950 is LIGHTEST — the reverse of Tailwind's default gray. These drive every surface, text and border in the dark skin, and are re-exposed (reversed, so gray-50 = lightest) as the Tailwind gray-* ramp.
--p-neutral-0#080a0e--p-neutral-50#0c0e13--p-neutral-100#12151a--p-neutral-150#191c22--p-neutral-200#21242b--p-neutral-250#2a2e35--p-neutral-300#373b42--p-neutral-400#4f5359--p-neutral-500#71747c--p-neutral-600#8e929a--p-neutral-700#adb1b9--p-neutral-800#d1d4da--p-neutral-900#e2e5e9--p-neutral-950#f1f4f6- Dark surfaces
- bg = neutral-0, surface = neutral-100, surface-2 = neutral-150, surface-3 = neutral-200, overlay = neutral-250, tooltip = neutral-300 — a tight near-black stack that gives subtle elevation.
- Dark text
- text = neutral-900, muted = neutral-700, faint = neutral-500; on-accent / on-danger = neutral-0.
- gray-* alias
- @theme inline maps gray-50…950 to these REVERSED (gray-50 = neutral-950 = lightest … gray-950 = neutral-0 = darkest), matching Tailwind's light→dark convention.
Semantic — accent
The purpose-named accent tokens. Every one is re-pointed between skins (light pulls darker beryl stops for contrast; dark pulls brighter ones). Use these — not the raw beryl ramp — for interactive accent surfaces and text. The swatches resolve live to the current theme.
--color-accent · bg-accent / text-accentlight beryl-700 · dark beryl-500--color-accent-hover · hover:bg-accent-hoverlight beryl-600 · dark beryl-400--color-accent-strong · text-accent-stronglight beryl-800 · dark beryl-600--color-accent-soft · bg-accent-softlight beryl-50 · dark beryl-950--color-on-accentlight #f7fbfa · dark neutral-0--color-linklight beryl-700 · dark beryl-300--color-link-hoverlight beryl-800 · dark beryl-200--color-accent-ringlight #00948d73 · dark #13a89f99- differs per skin
- ALL accent tokens are re-pointed in .dark. Light chooses darker beryl stops (beryl-700/800) for WCAG AA; dark chooses brighter ones (beryl-500/400/300).
- --color-on-accent
- Foreground (label) color to sit ON an accent fill. on-accent on beryl-700 = 5.26:1 (light); neutral-0 on beryl-500 in dark.
- text-accent on accent-soft
- The canonical accent chip pairing — beryl-700 on beryl-50 = 4.99:1, clears AA. Used by .chip-husky / .chip-lead.
- --color-accent-ring
- The translucent outer focus-ring color (fed into --fx-focus-ring). Not a Tailwind utility — read via the var.
Semantic — surfaces & background
The elevation stack. In light these are near-white tints; in dark they are a tight near-black neutral stack. bg is the page canvas; surface → surface-2 → surface-3 step up in elevation; overlay/tooltip sit above. The scrim/seat are translucent.
--color-bg · bg-backgroundlight #f9fafc · dark neutral-0--color-surface · bg-surfacelight #ffffff · dark neutral-100--color-surface-2 · bg-surface-2light #f5f7f9 · dark neutral-150--color-surface-3 · bg-surface-3light #f0f2f5 · dark neutral-200--color-overlaylight #ffffff · dark neutral-250--color-tooltiplight #1b1e25 · dark neutral-300--color-scrimlight #0f121a66 · dark #07090d9e--p-seat-30#0000004d (primitive)- differs per skin
- Every surface token is re-pointed. Light = white/near-white tints; dark = neutral-0…300. Overlay/tooltip invert most dramatically (light overlay is white, dark is neutral-250).
- Utilities
- bg-surface / bg-surface-2 / bg-surface-3 are generated. For the page canvas use bg-background (bridge) or bg-[var(--color-bg)] — there is no bg-bg.
- --color-overlay / --color-tooltip
- Backgrounds for popovers/menus (overlay) and tooltips. No dedicated utility — read via the var or rely on the components that consume them.
- --color-scrim
- Translucent backdrop behind modals/drawers. --p-seat-30 is the inner-glow seat shadow alpha used in the dark inner-glow recipe.
Semantic — text
The three-tier text hierarchy. Primary body text is inherited from <body> (= --color-text); use text-muted for secondary and text-faint for the quietest captions/placeholders. The two foreground-on-fill tokens (on-accent / on-danger) pair with their fills.
--color-text · text-foregroundlight #1b1e25 · dark neutral-900--color-muted · text-mutedlight #494d55 · dark neutral-700--color-faint · text-faintlight #71747c · dark neutral-500--color-on-accentlight #f7fbfa · dark neutral-0--color-on-dangerlight #fdf9f8 · dark neutral-0Primary — text-[var(--color-text)] / inherited body
Secondary — text-muted
Faint — text-faint (captions, placeholders)
- Primary
- Inherited from <body>; for an explicit primary use text-[var(--color-text)] or the text-foreground bridge. There is no text-text utility.
- text-muted / text-faint
- Generated utilities for the secondary and tertiary tiers. faint is the quietest pass-AA tier (placeholders, timestamps, the swatch captions in this very page).
- differs per skin
- All text tokens re-point. on-accent / on-danger flip to near-black neutral-0 in dark so labels on the mid-tone fills clear AA (neutral-0 on bad-500 = 5.87:1).
Semantic — borders & rules
Four hairline tiers from barely-there to a defined edge. In light these are tinted-black alphas; in dark they are white alphas (so lines READ as light-on-dark). Shown as fills here to expose the alpha, plus a live border preview.
--color-hairlinelight #0f121a0d · dark #ffffff0b--color-rulelight #0f121a14 · dark #ffffff14--color-border · border-border-softlight #0f121a1f · dark #ffffff1a--color-border-strong · border-border-stronglight #0f121a33 · dark #ffffff24- Utilities
- Only border (--color-border) and border-strong are exposed as Tailwind classes: border-border-soft and border-border-strong. hairline and rule are var-only.
- Ladder
- hairline (faintest, internal dividers) → rule (table/list rules) → border (default card/control edge) → border-strong (emphasised / overlay edges).
- differs per skin
- All four flip sign: light = black alphas (#0f121a…), dark = white alphas (#ffffff…). They are translucent, so they only render against a surface.
Semantic — status
Good / warn / bad, each a text token paired with a soft -bg fill. In light the text tokens were darkened so text-good/warn/bad clear WCAG AA on their soft backgrounds; the -bg surfaces are unchanged. In dark they map onto the status ramp primitives. These tokens also drive the .chip-* helpers.
--color-good · text-good / bg-goodlight #1f7a44 · dark good-500 #46b86e--color-warn · text-warn / bg-warnlight #7a5300 · dark warn-500 #ebae42--color-bad · text-bad / bg-badlight #b3251f · dark bad-500 #ec5c52--color-good-bglight #dffae5 · dark good-950 #0e2b18--color-warn-bglight #fff0d1 · dark warn-950 #37290f--color-bad-bglight #ffe9e5 · dark bad-950 #3f1916--p-good-300#78d694--p-good-500#46b86e--p-good-600#329d5a--p-good-950#0e2b18--p-warn-300#facc7e--p-warn-500#ebae42--p-warn-600#d69727--p-warn-950#37290f--p-bad-300#fe8c80--p-bad-500#ec5c52--p-bad-600#d1433c--p-bad-950#3f1916- Utilities
- text-good / text-warn / text-bad and bg-good / bg-warn / bg-bad are generated from the TEXT tokens. The soft -bg surfaces are var-only (bg-[var(--color-good-bg)] …).
- Pairing rule
- Always pair the text token with its matching -bg fill — that combination is the one tuned to pass AA (good 4.83:1, warn 6.08:1, bad 5.64:1 in light).
- differs per skin
- Light darkens the text tokens for AA; dark maps text→ramp-500 and -bg→ramp-950. The light -bg surfaces are bespoke tints, not the -950 primitives.
- Never color alone
- Status is conveyed by a label/icon too, never by the hue alone (the chips below carry text).
Data-viz series
A six-color categorical series (--p-viz-1 … --p-viz-6) for charts. Theme-independent primitives — the same six hues in light and dark — ordered for maximum adjacent-hue separation. viz-1 is the brand teal so single-series charts read on-brand.
--p-viz-1#0fb6ac (teal)--p-viz-2#7ba3f6 (blue)--p-viz-3#e1a536 (amber)--p-viz-4#d179ca (orchid)--p-viz-5#6abf6e (green)--p-viz-6#ea856b (coral)- Usage
- Assign in order (viz-1, viz-2, …) so categorical series stay consistent across charts. Read via the var: fill='var(--p-viz-3)' or bg-[var(--p-viz-3)].
- viz-1 = brand
- viz-1 is the brand teal; a single-series or primary metric chart lands on-brand automatically.
- does NOT differ per skin
- These are primitives — identical in light and dark. They are chosen mid-tone so they read on both the near-white and near-black canvases.
Chip helpers (.chip-*)
The .chip base + .chip-* color classes from globals.css — pill labels built from the semantic tokens (so they re-skin automatically). Risk chips keep red/amber/green because the hue carries meaning; team and role chips map onto accent/status families. Add .chip-sm for the compact size.
- .chip
- Base pill: inline-flex, pill radius, 12px / weight 540, 2px 10px padding, transparent border. .chip-sm shrinks to 11px and 1px 8px.
- chip-red / -yellow / -green
- Risk levels. Built from bad / warn / good token families (bg = -bg, border = token @35%, text = token). Kept hue-coded because risk is meaningful — but always render the label too.
- chip-husky / -bulldog / -terrier
- Team chips. husky → accent (beryl), bulldog → bad family, terrier → good family. The hue is decorative team identity, not status.
- chip-lead / -director
- Roles → brand accent. lead uses --color-accent; director uses the deeper --color-accent-strong with a stronger border.
- chip-advise-against
- Advisory flag → warn family (amber). chip-neutral → surface-3 fill + muted text for non-semantic labels.
- differs per skin
- Every chip re-skins automatically — they reference only --color-* semantic tokens (and color-mix on them), so light/dark is handled by the token layer.