Skip to content

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 with data-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 follow prefers-color-scheme on 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: balance on headings and decks, pretty on prose.
  • Use font-variant-numeric: tabular-nums for 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-areas so 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.