- Palette: Patiun Teal, Midnight, night sky, Starlight, flat Alchemist Gold; violet/indigo only as nebula light. Starlight content sections, night-sky nav and heroes (.on-sky scope re-points accent/gold to on-dark values). - Type: Cinzel for headings, names and card titles; Montserrat for body, UI and eyebrows (font-eyebrow); JetBrains Mono kept for code-like blocks. - Header lockup: Patiun glyph + Cinzel "GREG KILMER" + "Web Development · a Patiun LLC company"; glyph also marks the footer and store nav. - Teal filled buttons with Midnight text; gold eyebrows; small links use a darker teal so they pass WCAG AA on Starlight. - Favicons, app icons and web manifest from the GregKilmer.com brand package. - docs/THEMING.md updated for the new palette and fonts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
5.6 KiB
Theming & Motion Guide
How color, light/dark mode, section tone, and animation work on this site.
Palette
The site uses the Patiun LLC brand palette (shared by Patiun and Greg Kilmer),
exposed as CSS variables in src/styles/global.css under @theme:
| Token | Value | Meaning |
|---|---|---|
--color-teal |
#14A39A |
Patiun Teal, primary: buttons, fills, the glyph |
--color-teal-bright |
#2BC2B7 |
Teal text and links on dark grounds |
--color-teal-deep |
#0F8C84 |
Teal marks on light grounds |
--color-slate-teal |
#2D4A58 |
Dark end of the Tide gradient |
--color-gold |
#C9A24A |
Alchemist Gold: stars, rules, seal lines (never a large fill) |
--color-gold-bright |
#D8B35A |
Gold text on dark grounds |
--color-gold-deep |
#8A6516 |
Gold text under 24px on Starlight |
--color-violet / --color-indigo |
#6A2FE0 / #3B1F8F |
Nebula light only, never text |
--color-midnight |
#131C2B |
Panels, tiles, browser theme color |
--color-sky |
#0E1522 |
Night-sky ground |
--color-starlight |
#F4F2ED |
Text on dark; paper ground |
The semantic Tailwind tokens (--color-accent, --color-surface, ...) resolve
at :root, so content sections render with the Starlight values in
themes.css. Night-sky bands (the nav and page heroes) carry the .on-sky
class, which re-points accent and gold to their on-dark values.
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>) readslocalStorage['gk-theme'], falling back to the OSprefers-color-scheme. This runs before first paint, so there is no flash. - The nav
ThemeToggleisland 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
- Tweak a brand color: edit the fixed values in
global.cssunder@theme(e.g.--color-amber). - Tweak a section's tone: edit the relevant
[data-surface="..."]block inthemes.css. Each block lists--surface,--surface-raised,--surface-sunken,--text,--text-muted,--text-subtle,--border,--accent,--accent-contrast,--secondary. - 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:
- Hero background (
AnimatedBackground.tsx):- A CSS mesh-gradient layer (GPU-friendly, animated via the
animate-gradient-shiftkeyframes inanimations.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.
- A CSS mesh-gradient layer (GPU-friendly, animated via the
- Scroll reveals (
Reveal.astro+ the.revealclass):- IntersectionObserver toggles
.is-visible, triggering a CSS fade-up. Zero framework cost. The reduced-motion media query inglobal.cssforces these to their final state instantly.
- IntersectionObserver toggles
- Page transitions: Astro
<ClientRouter />(View Transitions API) morphs between pages and persists the nav. - Nav blur on scroll: a tiny inline script adds
.scrolledafter a few px, enabling backdrop-blur + a hairline border + shadow. - 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, names, card titles: Cinzel Variable (
font-display, used byh1-h4automatically). Open tracking, never for paragraphs. - Body and UI: Montserrat Variable (
font-sans,font-body). - Eyebrows / kickers: Montserrat uppercase with wide tracking (
font-eyebrow). - Code-like blocks only: JetBrains Mono Variable (
font-mono).
Font sizes use clamp() for fluid scaling (see the h1-h4 rules in
global.css).