ZeroStarter

Theming

Dark mode, OKLCH design tokens, and Tailwind v4: restyle without fighting the system.

You restyle ZeroStarter by editing tokens, not components. Colors, fonts, and radii are CSS custom properties in web/next/src/app/globals.css; Tailwind v4's @theme binds them to utility classes; every component references the utility, never a raw color. Change a token once and it flows to every surface, in both light and dark mode.

Dark mode

next-themes toggles a .dark class on <html> and follows the OS by default:

// web/next/src/app/providers.tsx
<NextThemesProvider
  attribute="class"
  defaultTheme="system"
  enableSystem
  disableTransitionOnChange
>

The ModeToggle (web/next/src/components/common/mode-toggle.tsx) is a smart three-state toggle: from system it jumps to the opposite of the OS preference, flips between light and dark while they diverge, and snaps back to system once the explicit choice matches the OS again.

Design tokens

Colors live in :root and .dark blocks as OKLCH values:

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --border: oklch(0.922 0 0);
  --success: oklch(0.627 0.194 149.214);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
}

OKLCH is perceptually uniform. Its first channel is lightness, so a dark variant is often the same hue with the L flipped, and edits stay balanced instead of shifting hue unexpectedly.

Wiring tokens to Tailwind

Two declarations in globals.css connect the custom properties to Tailwind v4. A custom variant teaches Tailwind that .dark triggers dark: utilities, and an @theme inline block maps --color-* utilities onto the tokens:

@custom-variant dark (&:is(.dark *));

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-border: var(--border);
  --color-success: var(--success);
  --font-sans: var(--font-dm-sans);
  --font-mono: var(--font-jetbrains-mono);
  --font-heading: var(--font-sans);
}

That mapping is what makes bg-background, text-foreground, border-border, and text-success (plus bg-success/10, border-success/20) resolve to the tokens above.

Use semantic tokens, not raw colors

Style with the semantic utility (bg-card, text-muted-foreground, border-border), never a literal like bg-white or bg-[#111]. Semantic tokens are the whole point: they carry the light/dark pair and let a rebrand happen in one file. A raw color hard-codes one theme and breaks the other.

Fonts

Fonts are self-hosted, not fetched at runtime, localized through next/font/local and each exposing a CSS variable. The core faces live in web/next/src/lib/fonts.ts (.woff2 in web/next/src/fonts/):

  • DM Sans--font-sans: body and headings.
  • JetBrains Mono--font-mono: code.

Caveat and Newsreader (--font-caveat, --font-newsreader) are author-only accents for /hire and /resume, applied via their .className. They live in the fork-excluded web/next/src/lib/marketing/fonts.ts (.woff2 in web/next/src/fonts/marketing/), kept out of lib/fonts.ts so their preload stays scoped to those two pages.

Headings track --font-sans unless you repoint --font-heading. To add or swap a font, follow the fonts skill: it fetches the variable woff2 and wires the preload so you don't ship layout shift.

Next

  • AI Skills: the fonts and design skills for font and UI work.
  • Dashboard: the shell these tokens style.