Foundations
The character: quiet, typographic, warm-neutral, editorial. Most of the hierarchy comes from type size and the two text colors, not from color or boxes. Surfaces are flat and split by hairlines. The one accent is used rarely.
Color
| Role | Token | Light | Use |
|---|---|---|---|
| Primary text | --color-text-primary |
#0a0a0a |
headings, active nav, values |
| Secondary text | --color-text-secondary |
#717182 |
body copy, labels, meta, idle links |
| Page | --color-surface-0 |
#ffffff |
page background, floating panels |
| Raised | --color-surface-10 |
#fbf9f8 |
tiles, cards, inputs, pills (warm off-white) |
| Inverse | --color-surface-inverse |
#1f1e1d |
dark cards/tiles, primary button |
| Border | --color-border |
rgba(0,0,0,.06) |
every surface edge, row rules |
| Divider | --color-divider |
rgba(0,0,0,.05) |
full-width section rules |
| Accent | --color-accent |
#6fb0c0 |
eyebrow on inverse, accent callout, glow |
| Danger | --color-text-danger |
#a43a30 |
error text only |
Rules:
- Two-tone text. Pair primary with secondary in the same line ("Abhishek Shankar" puts the last name in secondary; a heading in primary with the subtitle in secondary). This is the signature move.
- Never pure grey or pure black surfaces. Neutrals lean warm (
#fbf9f8,#1f1e1d,#f8f7f5). - Borders are barely visible (6% black). Don't go above 0.1 except for a focused field (
--color-border-strong). - The accent is not a brand fill. It shows up as a tint (8%), a border (24%), a radial glow on dark, or small text on inverse. No accent-filled buttons.
- Status colors beyond danger don't exist yet. If you need success or warning, add them as derived tokens at similar low saturation and say that you did.
- Light is the default. Everything renders light, whatever the OS is set to. Dark is a user preference: offer it as a choice (a theme toggle or setting) only where it makes sense, and remember the choice.
- Dark mode is derived: inverse surfaces become the page, and text flips to
#f8f7f5/#a09f9b. Turn it on withdata-theme="dark"on<html>when the user picks it.data-theme="system"follows the OS, but only when the user chose "match system". Don't followprefers-color-schemeon your own.
Typography
Sans: Switzer (free, Fontshare, variable 100–900). Serif: Newsreader (Google Fonts) for long-form reading. Mono: Reddit Mono. If Switzer can't load, system-ui is the fallback. Don't swap in Inter as the primary face.
| Style | Size | Weight | Tracking | Leading |
|---|---|---|---|---|
| Display (page H1) | clamp(42px, 3.8vw, 58px) | 450 | −0.055em | 0.98 |
| Subtitle / deck | clamp(22px, 2vw, 28px) | 400 | −0.02em | 1.4, secondary color |
| Section H2 (editorial) | clamp(29px, 4vw, 48px) | 400 | −0.035em | 1.12 |
| Title (list/section) | 32px (28 mobile) | 500 | −0.02em | normal |
| H3 (card, callout) | 18–22px | 500 | −0.02em | 1.25–1.3 |
| Body large | 18px | 400 | 0 | 1.5 |
| Body | 16px | 400 | 0 | 1.5 |
| Small | 14px | 400–500 | 0 | 1.45–1.5 |
| Caption | 13px | 400 | 0 | — |
| Eyebrow | 11–12px | 600, UPPERCASE | +0.08–0.09em | 1.3, secondary |
| Prose (serif) | 17px | 400 | 0 | 1.68, secondary, max 700px |
Rules:
- Large type is tight. The bigger the size, the more negative the tracking. Small uppercase text gets positive tracking.
- Weights are restrained: 400 by default, 450 for display, 500 for titles, 600 only for eyebrows. Never use bold 700 for headings.
- Use
text-wrap: balanceon headings and decks,prettyon prose. - Use
font-variant-numeric: tabular-numsfor years, counts and anything in a column. - On mobile, drop display leading to 1.0.
- Optional fluid mode: the site sets
html { font-size: clamp(8px, 0.2vw + 7.4px, 10px) }and writes sizes in rem (1.6rem = 16px on desktop), so the whole UI shrinks slightly on small screens. Only use it in a project you fully control. It breaks rem-based third-party libraries.
Space
4px base: --space-1…20 = 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80.
- Section padding:
--space-section= clamp(88px, 11vw, 152px). Compact: clamp(64px, 8vw, 96px). 72px on phones. - Card padding: clamp(24px, 3vw, 36px). Callout: clamp(24px, 3.5vw, 40px).
- Grid gaps: 20px desktop, 16px tablet, 12px phone.
- Prefer
clamp()for vertical rhythm instead of breakpoint jumps. - Generous whitespace is the point. When unsure, add space, not a border.
Radius
4 small in-widget controls · 6 chips · 10 inputs, icon wells, buttons · 16 tiles, cards, callouts, media · 20 floating sheets · 999px pills/captions.
Nest correctly: an inner element's radius is smaller than its container's.
Elevation
Flat. Borders separate. Shadows only for floating layers:
--shadow-subtle: small lifted controls.--shadow-float: sheets, popovers, dialogs.--shadow-float-dark: floating elements on inverse surfaces.
No shadows on cards or tiles at rest, and no hover-lift shadows.
Layout
--container-inner: up to 1280px, for grids and wide sections.--center-column-width: 700–900px, for nav, hero, lists. The nav aligns to this column, not the container.--reading-column-width: 700px, for prose.--container-padding: clamp(20px, 3vw, 40px) side gutter.- Breakpoints: 560 (single column rows), 768 (mobile nav, touch targets, always-on captions), 1024 (bento grid to 2 columns).
- Bento grids use
grid-template-areasso tiles are placed by name per breakpoint. Use container queries inside tiles whose content changes with tile width. - Editorial headers are two-column (heading left ~1.35fr, lede right ~0.65fr, aligned to the bottom) and collapse to one column under 820px.