Design system
The rules this site is built on. Every value and specimen below is read from the same tokens, fonts and components the site uses, so this page can’t drift from the real thing.
Principles
- One grid. Every page sits on the same columns, so things line up from page to page without special cases.
- Few decisions, all named. Colors, radii and motion are tokens with a job. A value without a name doesn’t ship.
- Quiet by default. The work is the loudest thing on the page. Motion is small and only shows up where it helps.
- Content is data. Case studies are checked files, so adding one never means touching layout code.
Color
Two neutral scales, one for text and one for surfaces, each with a light and a dark value. Text steps down in contrast as it gets less important, and every step clears WCAG AA (4.5:1) on the page background.
Textlight / dark
foreground
Main text, headings and links
#0a0a0a · 19.8:1#ededed · 16.9:1
secondary
The name at rest in dark mode, so the shimmer shows
#3d3d3d · 10.9:1#c2c2c2 · 11.1:1
tertiary
Supporting links, like contact links
#5c5c5c · 6.7:1#a6a6a6 · 8.1:1
muted
Captions, labels and metadata
#767676 · 4.5:1#8f8f8f · 6.1:1
Surfaceslight / dark
background
Page background
#ffffff#0a0a0a
surface
Image placeholders and code
#f2f2f2#171717
border
Dividers and rules
#e6e6e6#262626
Snake gamelight / dark
snake
The snake. Lighter in dark mode so it stands out
#2541b2#4d69db
snake-eye
The snake’s eyes
#ffffff#ffffff
snake-board
The grey squares of the board
#f7f7f7#121212
Typography
Two typefaces. Switzer does the work, and Geist Pixel is the accent, used in small doses so it stays special. Sizes come from a short scale.
SwitzerPrimary
Everything by default. Italic adds emphasis inside a sentence.
Geist PixelEmbellishment
Used sparingly: the name, work card titles, the word “for” and small labels.
- LabelKey insight12 / 16
- BodyProduct strategy + design + code16 / 20
- ReadingLong-form case study writing16 / 24
- HeadingSection heading24 / 30
- DisplayCase study summary30 / 37.5
- Figure+18%36 / 36
Grid
Four columns on phones, eight on tablets and twelve on desktop. On desktop the intro takes the first three columns, and everything else starts at column four.
| Breakpoint | From | Columns | Gutter | Margin |
|---|---|---|---|---|
| Phone | 0 | 4 | 12px | 16px |
| Tablet | 768px | 8 | 16px | 20px |
| Desktop | 1024px | 12 | 16px | 20px |
Radius
Corners stay nearly sharp. There are three steps, and Tailwind’s larger radius utilities are switched off, so nothing rounder than 6px can ship.
rounded-xs2px
Inline code and small marks
rounded-sm4px
Images, cards and placeholders
rounded-md6px
The ceiling. Nothing is rounder.
Motion
Three motions, all short.
- Hover over me
Hover
Words get slightly darker. Every link uses it.
brightness 0.8 · 150ms
Icon swap
The light/dark toggle. Click the icon to play it.
250ms · blur 2px · from 0.25×
- Anannyaa Kapur
Shimmer
A band of light across the name in the header.
2000ms sweep · every 10s
The icon swap and shimmer come from transitions.dev, and both switch off for anyone who prefers reduced motion.
Components
Built once and reused everywhere.
Data-connected report builder for university funds
2026
A line can mix links, italic and pixel words.
Case study blocks
- Completion
- +18%
- Tickets
- −32%
- Rating
- 4.8
- FigureAn image with an optional caption, at text width, wide or edge to edge.
- GalleryA grid of figures, two or three across.
- ColumnsText next to a visual. Stacks on phones.
- CalloutA key insight or pull quote.
- StatsOutcome numbers.
- VideoA looping, muted prototype clip.
See them in Publication-grade charting for financial analysts →
Content model
Each case study is a folder in content/work with an index.mdx file. Its details are checked against this schema when the site builds, so a missing field stops the build instead of breaking a page. The table is generated from that schema.
| Field | Type | Required | Description |
|---|---|---|---|
| caption | string | No | Small grey line above the heading, e.g. the company or the kind of work. |
| title | string | Yes | Project name. |
| role | string | Yes | Your role on the project. |
| skills | string | string[] | No | Disciplines the work drew on. |
| timeline | string | Yes | Free text, e.g. “2024” or “Mar – Aug 2025”. |
| team | string | string[] | No | Who you worked with. |
| tools | string | string[] | No | Tools used. |
| status | string | No | Where the work stands, e.g. “Shipped”. The row is left out without one. |
| cover | image | No | Imported image. Without one, a placeholder shows. |
| coverAlt | string | No | Cover description for screen readers. Required with a cover. |
| coverVideo | string | No | Path to a video in /public, e.g. “/work/my-project/cover.mp4”. Plays muted on a loop over `cover`. |
| coverCarousel | any[] | No | Imported images that cycle across the cover, one in the middle and its neighbours cropped at the edges. Sits on top of `cover`. |
| coverCards | any[] | No | Imported images shown as a stack of cards on the cover: every few seconds the front one tilts away and the next moves up. Sits on top of `cover`. |
| year | integer | Yes | Shown on the gallery card. |
| order | number | Yes | Gallery position, lowest first. |
| draft | boolean | No | Shown locally with a Draft label, left out of the live site. |
| summary | string | Yes | One or two sentences describing the project. Not shown on the page: it's the description search engines and link previews use. |
| tags | string[] | No | Topics, kept for future filtering. |