A House Divided A House DividedDocumentation
Changelog
Engineering/Design System

Design System

Last updated 2026-08-21
Source files

This document is the source of truth for the A House Divided visual language. Source-of-truth code lives in (tokens) and src/components/ui/* (primitives). This doc describes the rules those files encode.

Related reading:

Why this exists#

AHD is data-dense and ships 12 themes. Without a shared token contract, each contributor ends up hard-coding a hex, a theme breaks in one place, and the product starts to feel like four different apps. The rules below are the minimum needed to keep that from happening. Everything else — layout, rhythm, motion — is secondary.

Tenets#

  1. Token, not hex. Every surface, text, border, and accent color resolves through a CSS custom property (var(--primary), var(--card)). A raw #1d4ed8 in a component is a bug — it breaks every theme except Default. The one lawful exception is the brand duality values (#dc2626, #1d4ed8) used inside the .animated-gradient utility, which is intentionally off-theme.
  2. Serif display, sans body, mono numerics. Lora for hero/section titles; Geist for everything else; Geist Mono for values, timestamps, and IDs. The contrast is the system.
  3. Dense, consequential copy. Lead with the mechanic, disclose the trade-off, include units. No emoji. No "Seamlessly / Powerfully / Leverage." Second person for the player.
  4. Cards are rounded-xl with 1px var(--card-border). Colored card variants use a 10% fill / 30% border tint pair. Never full saturation.
  5. Every component must work in all 12 themes. If you need a color that doesn't exist in the token set, add a token — don't branch on data-theme.

Tokens#

All tokens live on :root and per-theme [data-theme="..."] blocks in . There are four layers:

1. Palette tokens (theme-scoped)#

Token Purpose
--background Page backdrop.
--foreground Default text color; the only "white" value — never #fff directly.
--primary / --primary-dark Political red. CTAs, destructive, live indicators, alignment bars.
--secondary / --secondary-dark Political blue. Secondary CTAs, informational chips.
--accent Strong text emphasis (equals --foreground in most themes).
--muted Labels, captions, disabled text.
--card / --card-border Card surface + 1px border.
--card-default / --card-elevated / --card-muted Elevation tiers inside a card stack.
--success / --warning / --error / --info + -muted Semantic ramp. Tailwind-500 conventions in Default; overridden in Retro, Solarized, OLED, Cold War.
--track / --overlay Slider + range tracks; modal/page backdrops.

2. Elevation tokens (theme-scoped)#

Token Use
--shadow-sm (--shadow-card) Cards at rest.
--shadow-md (--shadow-panel) Popovers, panels, dropdowns.
--shadow-lg (--shadow-modal) Modals, fullscreen overlays.
--glow-primary / --glow-primary-sm Focus ring and live-indicator glows.

Shadows are deep (up to 70% black alpha) because the default backdrop is near-black — they read as soft glows, not grey halos.

3. Typography tokens (global)#

Declared once on :root. Do not override per theme.

Token Value Use
--text-body-xs 10px Micro captions, eyebrow labels.
--text-body-sm 12px Meta, tags, table cell metadata.
--text-body 14px Default body. Data-dense dashboards.
--text-body-lg 16px Emphasized body.
--text-heading-sm 18px Card headings.
--text-heading 20px Panel titles.
--text-heading-lg 24px Page-section titles.
--text-display 30px Section display (landing only).

Fonts are wired via next/font in : --font-geist-sans, --font-geist-mono, --font-lora, plus --font-fraunces (mapped to font-display, used at the --text-display scale) and --font-jetbrains-mono (used ahead of Geist Mono in the command-1953 theme's body font stack). Components reference the first three through the @theme inline mapping — font-sans / font-mono / font-serif Tailwind utilities resolve correctly.

4. Radii, spacing, motion (global)#

Themes (overview)#

AHD ships 12 themes. Full tables in design-system-themes.md.

Theme Mood Use case
default Warmed graphite, soft ivory text The canonical look.
oled True #000 with punched accents AMOLED devices, night play.
usa Navy + crimson + parchment Patriotic "prestige" look.
light Slate-50 clean Day mode, professional.
pastel Fuchsia/violet soft Approachable day mode.
dark-pastel Purple + cyan neo Alt dark.
retro Green-phosphor CRT Novelty / nostalgic.
solarized Burnt orange + teal Classic terminal.
cloakroom Ivory on warm graphite, oxblood + brass Statesman mood; leadership screens.
broadsheet Cream paper + deep ink + crimson Editorial day mode.
coldwar Amber on near-black Sit-room console at night.
command-1953 Green-phosphor CRT scanlines, JetBrains Mono body Console-room mood for the 1953 command era; heaviest per-theme override block in (body background, focus ring, tables, media).

The default theme is applied automatically on unauthenticated / marketing pages. User selection persists to User.theme (see design-system-themes.md for the server sync flow).

Layout rules#

Iconography#

Imagery#

Hover / press / focus#

Glass and transparency#

Where components live#

When adding a new primitive, put it in src/components/ui/ and export from .

Checklist for new UI work#

Before opening a PR that adds or changes UI, confirm:

Adding a new token#

Tokens are cheap; adding them correctly is not. Before adding:

  1. Check if an existing token covers it. --card-elevated already exists — don't add --card-raised.
  2. Name semantically, not visually. --card-warning-border over --yellow-border.
  3. Add to every theme block in . A missing token silently falls back to the cascade, which usually looks broken.
  4. Update @theme inline if you want a Tailwind utility generated (e.g. bg-X).
  5. Document in this file's token tables and in design-system-themes.md.
  6. Run npm run typecheck && npm run lint — the no-country-literals rule is the closest analogue to a token-linter; there isn't one for colors yet, so peer review must catch hex leaks.

Adding a new theme#

See design-system-themes.md for the full contributor checklist. Summary:

  1. Pick a mood and justify it — each theme should have a real use case, not just a color shift.
  2. Add the [data-theme="<name>"] block to with every token filled.
  3. Add <name> to the Theme union in , the VALID_THEMES array, User.theme in , and themeSchema in .
  4. Add a carousel option to THEME_OPTIONS in with hex swatches (these are embedded in the settings page and do not resolve through CSS variables, so the carousel works regardless of which theme is currently active).
  5. Restart npm run dev (Ctrl+C then re-run) and hard-refresh the browser. Turbopack's HMR for @import "tailwindcss" does not reliably pick up newly-added [data-theme=...] blocks. Skipping the restart is the #1 reason a new theme appears to "do nothing" when toggled.
  6. Spot-check Button, Badge, Modal, StatsCard, and /dashboard for obvious breakage.
  7. Add a CHANGELOG entry under ### UI**Theming**.

Anti-patterns#

These are specific failure modes seen in PRs — flag them in review.