Files
com.gregkilmer.www/docs/THEMING.md
T
2026-07-04 07:42:24 -04:00

4.6 KiB

Theming & Motion Guide

How color, light/dark mode, section tone, and animation work on this site.


Palette

From the spec, exposed as CSS variables in src/styles/themes.css and mapped to Tailwind utilities in src/styles/global.css:

Token Value Meaning
--color-slate #1E293B Professional confidence (primary)
--color-amber #F59E0B Warm accent (the making/creative side)
--color-teal #699a94 Soft teal (secondary, favorite)
--color-ink #0F172A Near-black text on light
--color-cream #FFFBEB Cream-white text on dark

Brand colors are fixed. Semantic tokens (--surface, --text, --accent, --secondary, --border, ...) resolve differently per theme + section, and are what components actually use.


Two theming axes

Axis 1 — Global light/dark

Set on <html data-theme="light|dark">:

  • A blocking inline script (src/scripts/theme-init.ts, inlined in <head>) reads localStorage['gk-theme'], falling back to the OS prefers-color-scheme. This runs before first paint, so there is no flash.
  • The nav ThemeToggle island flips it and persists the choice.
  • If the user has never chosen, the site follows OS changes live.

Axis 2 — Section tone

Set on <body data-surface="neutral|professional|interest"> via the surface prop on <BaseLayout>:

Surface Used on Tone
neutral Home, About, Contact Balanced — clean in light, calm in dark
professional Professional page Dark slate, polished, amber accents
interest Interests pages Warm cream/amber, inviting

The Professional surface leans dark even in light mode (it is the "polished wing"). The Interest surface is warm in both modes. This is what gives the two wings their distinct feel while sharing the same typography and spacing.

Each (data-theme, data-surface) combination redefines the semantic tokens in themes.css. Add or adjust a combination there to change any section's palette.


Changing colors

  1. Tweak a brand color: edit the fixed values in global.css under @theme (e.g. --color-amber).
  2. Tweak a section's tone: edit the relevant [data-surface="..."] block in themes.css. Each block lists --surface, --surface-raised, --surface-sunken, --text, --text-muted, --text-subtle, --border, --accent, --accent-contrast, --secondary.
  3. Components use utilities like bg-surface, text-text, text-accent (mapped from these variables), so they update automatically.

Contrast

Verify WCAG AA (4.5:1 for body text) whenever you change a token pair. The current combinations were chosen for AA compliance. The --accent on the neutral light surface uses a deeper amber (#b45309) specifically to meet AA on white.


Motion

Motion is layered and always respects prefers-reduced-motion:

  1. Hero background (AnimatedBackground.tsx):
    • A CSS mesh-gradient layer (GPU-friendly, animated via the animate-gradient-shift keyframes in animations.css).
    • An optional <canvas> particle field (slow drifting dots) on the home hero only. It renders at devicePixelRatio (capped at 2), pauses when off-screen, and is skipped entirely under reduced motion.
  2. Scroll reveals (Reveal.astro + the .reveal class):
    • IntersectionObserver toggles .is-visible, triggering a CSS fade-up. Zero framework cost. The reduced-motion media query in global.css forces these to their final state instantly.
  3. Page transitions: Astro <ClientRouter /> (View Transitions API) morphs between pages and persists the nav.
  4. Nav blur on scroll: a tiny inline script adds .scrolled after a few px, enabling backdrop-blur + a hairline border + shadow.
  5. Hover micro-interactions: Tailwind transitions on cards/buttons (lift, accent glow, arrow nudge).

Disabling motion

Everything is gated by the global reduced-motion rule in global.css:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
    ...
  }
}

The canvas island additionally checks matchMedia('(prefers-reduced-motion: reduce)') before drawing, so even JS-driven motion is skipped.


Typography

Self-hosted variable fonts via Fontsource (woff2, no external requests):

  • Headings / UI: Plus Jakarta Sans Variable (geometric sans)
  • Body: Inter Variable
  • Mono / kickers / quotes: JetBrains Mono Variable

Font sizes use clamp() for fluid scaling (see the h1-h4 rules in global.css).