The system behind this site
Every color, typeface, spacing step, and interaction on this portfolio is driven by the tokens and primitives documented here. The full reference lives in docs/design-system.md and a reusable Claude Code skill in .claude/skills/design-system/SKILL.md. Feel free to learn from, fork, or adapt any of it.
Graphite + Indigo
All colors are CSS custom properties exposed as Tailwind tokens. Dark mode (graphite) is the default; light mode mirrors the same semantic roles with brighter values. Use semantic tokens, never raw hex in components.
bg-background
#0B0B0F (dark) / #FAFAFF (light)
Page background
bg-card
#15151B (dark) / #FFFFFF (light)
Card and panel surfaces
bg-elevated
#1C1C24 (dark) / #F3F3F9 (light)
Elevated overlays and popovers
bg-muted
#232330 (dark) / #F3F3F9 (light)
Muted / low-emphasis backgrounds
bg-border
#232330 (dark) / #E5E5F0 (light)
Default hairline borders
bg-foreground
#EDEDEF (dark) / #17171A (light)
Primary text and icons
bg-accent
#6E6BF2 (dark) / #5B58DD (light)
Brand accent, CTAs, highlights
bg-accent-hover
#807DF5 (dark) / #6E6BF2 (light)
Accent on hover
Additional semantic tokens
text-muted-foregroundSecondary text
text-subtleTertiary / placeholder
border-strongEmphasized borders
text-accentBrand links / labels
bg-secondaryChip backgrounds
text-destructiveError states
Three families, one voice
Fraunces (variable serif) for display and headings, Inter for body and UI, JetBrains Mono for code labels and eyebrows. All are loaded via Next.js font optimization with font-display: swap.
Aa
Fraunces
Headings
font-serifAa
Inter
Body / UI
font-sansAa
JetBrains Mono
Code / Labels
font-monofont-serif text-[clamp(2rem,5vw,2.75rem)] font-medium tracking-[-0.02em] text-foregroundfont-serif text-2xl md:text-[1.75rem] text-foregroundfont-serif text-xl text-foregroundtext-lg text-muted-foreground leading-relaxedtext-base text-muted-foregroundtext-sm text-muted-foregroundfont-mono text-[11px] uppercase tracking-[0.16em] text-subtle4px grid, generous rhythm
All spacing follows Tailwind's 4px base. Sections use py-10 md:py-14 (~40-56px) vertical padding, creating a calm reading rhythm. Component gaps use multiples of 4px.
Gap scale
Border radius
--radius: 0.75rem (CSS custom property). Cards use rounded-2xl, buttons use rounded-full.
Layout building blocks
Four server-safe primitives handle nearly all layout needs. They live in components/layout/.
width="reading" | "wide"Centers content. reading = 760px max-width for prose; wide = 1080px for full layouts.
number, label, title, width, actionWraps a Container with py-10/md:py-14 rhythm and an optional numbered eyebrow + serif h2 header.
className (grid cols/rows)A grid wrapper with hairline border and 1px gap rendered via bg-border. Use for feature card grids.
(none)A full-width faded hairline (gradient from transparent to border-strong to transparent) separating sections.
Cell A
Any content goes here. The 1px gap between cells is the bg-border color.
Cell B
Cells share a parent overflow-hidden rounded-2xl container.
UI patterns
These patterns are composed from the tokens above. Prefer these shapes when adding new UI so the visual language stays consistent.
bg-accent rounded-full text-accent-foregroundborder-border-strong rounded-fullbg-muted rounded-fullImport from components/common/StackIcon. Pass a name: StackName and optionally showLabel= for icon-only.
Text with an accent-tinted underline that deepens on hover. This is the default prose link style.
underline decoration-accent/50 underline-offset-4 hover:decoration-accentborder-border bg-card focus:ring-2 focus:ring-ring/60Purposeful, not decorative
Animations guide attention and confirm interactions. They are kept short (200-400ms) and always respect prefers-reduced-motion, which collapses all durations to 0.01ms.
cubic-bezier(0.22, 1, 0.36, 1)--ease-out. Fast start, smooth settle. Used for entrances.
cubic-bezier(0.77, 0, 0.175, 1)--ease-in-out. Slow start and end. Used for transitions between states.
cubic-bezier(0.34, 1.56, 0.64, 1)--ease-spring. Slight overshoot. Used for playful micro-interactions.
200ms - 400ms200ms for hover/micro; 300-400ms for layout and reveal animations.
~60ms per itemList items and grid cells stagger with a ~60ms delay between each.
translateY(-2px)Cards and interactive tiles lift 2px on hover to indicate affordance.
All motion is powered by Motion (Framer Motion) via the Reveal primitive in components/layout/Reveal.tsx. Wrap any element in <Reveal> to get a fade-up on first viewport entry.
All tokens live in app/globals.css and tailwind.config.ts.