Back to home
Design System

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.

01 / Color

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-foreground

Secondary text

text-subtle

Tertiary / placeholder

border-strong

Emphasized borders

text-accent

Brand links / labels

bg-secondary

Chip backgrounds

text-destructive

Error states

02 / Type

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-serif

Aa

Inter

Body / UI

font-sans

Aa

JetBrains Mono

Code / Labels

font-mono
The craft of software
Displayfont-serif text-[clamp(2rem,5vw,2.75rem)] font-medium tracking-[-0.02em] text-foreground
Section heading
H2font-serif text-2xl md:text-[1.75rem] text-foreground
Sub-section heading
H3font-serif text-xl text-foreground
A larger intro paragraph to ease readers in.
Body ledetext-lg text-muted-foreground leading-relaxed
Regular body copy at 16px with a 1.65 line-height for comfortable reading on both desktop and mobile.
Bodytext-base text-muted-foreground
Captions, helper text, timestamps.
Smalltext-sm text-muted-foreground
Section eyebrow / label
Mono labelfont-mono text-[11px] uppercase tracking-[0.16em] text-subtle
03 / Spacing

4px 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

gap-2 / 8px
gap-4 / 16px
gap-6 / 24px
gap-8 / 32px
gap-12 / 48px
gap-16 / 64px

Border radius

rounded / 4px
rounded-md
rounded-lg (--radius)
rounded-xl
rounded-2xl
rounded-full

--radius: 0.75rem (CSS custom property). Cards use rounded-2xl, buttons use rounded-full.

04 / Primitives

Layout building blocks

Four server-safe primitives handle nearly all layout needs. They live in components/layout/.

Containerwidth="reading" | "wide"

Centers content. reading = 760px max-width for prose; wide = 1080px for full layouts.

Sectionnumber, label, title, width, action

Wraps a Container with py-10/md:py-14 rhythm and an optional numbered eyebrow + serif h2 header.

BentoclassName (grid cols/rows)

A grid wrapper with hairline border and 1px gap rendered via bg-border. Use for feature card grids.

Divider(none)

A full-width faded hairline (gradient from transparent to border-strong to transparent) separating sections.

Live Bento example

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.

C
Live Divider
05 / Components

UI patterns

These patterns are composed from the tokens above. Prefer these shapes when adding new UI so the visual language stays consistent.

Buttons
bg-accent rounded-full text-accent-foregroundborder-border-strong rounded-fullbg-muted rounded-full
Labels and badges
Eyebrow labelBadgeChipTag
StackIcon chips
ReactTypeScriptTailwind CSSNext.jsFramer MotionFigma

Import from components/common/StackIcon. Pass a name: StackName and optionally showLabel= for icon-only.

Inline links

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-accent
Text inputborder-border bg-card focus:ring-2 focus:ring-ring/60
06 / Motion

Purposeful, 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.

Primary easingcubic-bezier(0.22, 1, 0.36, 1)

--ease-out. Fast start, smooth settle. Used for entrances.

In-out easingcubic-bezier(0.77, 0, 0.175, 1)

--ease-in-out. Slow start and end. Used for transitions between states.

Spring easingcubic-bezier(0.34, 1.56, 0.64, 1)

--ease-spring. Slight overshoot. Used for playful micro-interactions.

Duration range200ms - 400ms

200ms for hover/micro; 300-400ms for layout and reveal animations.

Stagger~60ms per item

List items and grid cells stagger with a ~60ms delay between each.

Hover liftstranslateY(-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.

Back to home