Foundations

Typography

Two typefaces. GT Sectra is a broad-nib serif where the pen origin is visible in the letterforms. It represents the complexity of humans. Pentameter is a monospace with calligraphic movement: fixed widths, lively forms. The built × natural tension lives inside this one typeface. Sectra speaks for people, Pentameter speaks for the system.

The rule is simple. If you're reading sentences, it's Sectra. If you're scanning the interface, it's Pentameter.

Each role is rendered here in its actual specs — the same CSS classes that build the site.

Body is 16px at 1.5 line-height. That gives 24px, which becomes the grid cell. The entire layout system grows from this single typographic decision.

Type does not respond to context. Colour shifts with time of day. Typography stays fixed. It's the anchor.

Pentameter is a variable font (weight axis 100–900, 300–600 used). Sectra is static weights: Book (400) and Medium (500). Bold (700) is reserved for case study content only.

DISPLAY See the system.
HEADING-LG I was making this case in 2023.
HEADING-LG-MONO 16:42
HEADING Roles that fit.
BODY-LG A portfolio is a conversation between two people. Every design decision reveals how a person thinks.
BODY-LG-MONO A portfolio is a conversation between two people.
BODY A portfolio is a conversation between two people. Every design decision reveals how a person thinks.
BODY-MONO CEO, Interbrand · LinkedIn · 2023

Layout

The cell is 24px, derived from body line-height, and constant at every screen width. The page uses as many square cells as fit, capped at 55 (1320px) and centred above that. The scale runs from space-012 (3px) to space-400 (96px). Every step is a multiple or fraction of the cell, including a 1½-cell step.

The grid is not hidden infrastructure. It's discoverable. This is visible construction in practice. In deliberate moments, the system shows its own working: column widths, type measures, spatial values surfaced on screen.

Spacing is measured because it is derived. Every space comes from the cell; the scale itself may be generous.

Components don't pick from the scale directly. Spacing is assigned by role; every role is a step on the scale. Heading spacing is asymmetric by definition: 36px before, 12px after.

The cell is a CSS custom property. Every spacing token is a calc() expression derived from it.

×
SPACE-012 ·
SPACE-025 ·
SPACE-050 ·
SPACE-100 ·
SPACE-150 ·
SPACE-200 ·
SPACE-300 ·
SPACE-400 ·

9 roles, read live from the CSS — this list is generated from the role declarations themselves, so a role added or repointed appears here without an edit. Each role aliases a step. space-sections is the total between sections — where a hairline splits the gap, each side takes half.

SPACE-WITHIN SPACE-025 · ·
SPACE-RELATED SPACE-050 · ·
SPACE-INSET SPACE-100 · ·
SPACE-BLOCKS SPACE-200 · ·
SPACE-SUBSECTIONS SPACE-300 · ·
SPACE-SECTIONS SPACE-400 · ·
SPACE-PAGE SPACE-200 · ·
SPACE-HEADING-BEFORE SPACE-150 · ·
SPACE-HEADING-AFTER SPACE-050 · ·

Stroke

Every visual boundary is a 1px hairline in --border. No decorative strokes, no rounded corners, no variable weights. Structure is communicated through position and colour, not border treatment.

How it is drawn follows what it bounds. A stroke that encloses content is an outline: a border would occupy layout and push everything inside it a pixel off the cell, and an outline occupies nothing, so the inset stays a spacing role. A stroke that marks a single edge stays a border. Adjacent items sharing an edge use the collapsed shadow below.

Focus rings are the exception: 2px solid in --accent with a 2px offset for visibility.

box-shadow Layout-neutral hairlines. Collapsed borders between adjacent items. Does not affect the box model.
border Structural dividers between page sections. Form field boundaries. Affects the box model.
outline Focus rings at 2px in --accent. Element borders that must not affect layout at 1px in --border.
Collapsed borders

When adjacent elements share an edge, each hairline must come from a single source. Overlapping borders at fractional pixel positions produce a darker, thicker line — the browser composites two 1px strokes that don't align to the same device pixel.

A
B
C
D
Frame outline: 1px solid var(--border); outline-offset: -1px
Items box-shadow: -1px -1px 0 0 var(--border)
Required background: var(--ground) on every item

Colour

The colour creates a connection between Alan and the viewer. Both share the same sky.

The palette is modelled on natural light to make that connection real. One input (time, 0–24) generates every colour. Lightness dwells at day and night, transitions quickly at dawn and dusk, the way the sky does. Colour drains out during transition, the way it does at twilight. Two hue families (warm and cool) switch invisibly through this achromatic zone. Accent peaks at golden hour.

Three roles: ground, ink, accent. Ground holds everything. Ink is its complement. Accent steers the eye.

Particulate is the fourth, and the only one that exists for a single surface. It is accent’s hue pulled a third of the way back toward ground, and it exists because accent could not do two jobs at once. At night ground is near-black, so accent has to be light — L 0.72 — to read as the interactive colour. But ink at night is also light, and a dithered field is a spatial average of ground and whatever is suspended in it: tile a light mark behind a sentence and the background climbs toward the text’s own lightness until the words sink into it. Measured with APCA at 20:00, body text needs the mark below L 0.56 and a link needs it above L 0.68. There is no single value, which is why this is a token and not a use of accent.

The colour clock is the control. YOUR TIME shows the viewer's sky. ALAN'S TIME shows Alan's. Drag it to any point in the day.

On /system, the clock overrides the page palette. Drag it to golden hour and the entire page shifts — every ground, ink, and accent value updates live. Scroll past the colour section and the page reverts to your real time. This lets you verify the system works at every point in the day without leaving the page.

OKLCH colour space. CSS custom properties. All derived from time, nothing hardcoded. WCAG contrast maintained at every point on the spectrum.

The token architecture has two layers, not three. Most design systems have primitive tokens (static swatches), then semantic tokens mapped from them. Here, there are no static swatches. A generation function takes time as input and produces every colour through OKLCH derivation. The math is the primitive — it lives in code, not in tokens. The semantic tokens (ground, surface, ink, ink-secondary, accent, particulate, border) are the first tokenised expression of colour. They describe roles, not values. The palette moves, but the roles are stable. Component tokens reference the semantic layer: a nav label uses ink-secondary, a section heading uses ink. Two layers of tokens. The generation function replaces the third.

Colour clock — drag to scrub the time of day
00 06 12 18
GROUND
SURFACE
INK
INK-SECONDARY
ACCENT
PARTICULATE
BORDER

Motion

Motion creates the sense that content belongs where it lands. That's why it's here.

Composition, not animation. Content finds its place on the grid as the viewer scrolls. The grid is already there; the content arrives and settles. Long deceleration: fast at the start because it knows where it's going, slow at the end because it's precise about where it lands. Not bouncing, not snapping, not springing. Settling, because it belongs there.

Pace is slow. Content is being told to you, not thrown at you.

One easing curve: cubic-bezier(0.05, 0.7, 0.1, 1.0). Applied to everything. prefers-reduced-motion removes scroll-driven composition. Everything is simply present. The page works perfectly static because the structure holds without motion.

This section demonstrates itself. Trigger the animation and watch the easing in action.

0 time → 1

Iconography

All icons come from Phosphor Light — a fill-based set on a 256-unit viewBox. The light weight matches Pentameter 400 optically without runtime calculation.

Icons live in a central registry (src/lib/icons.ts). The <Icon> component reads from the registry and renders via set:html — no inline SVG paths in consuming components. To add an icon: copy the Phosphor Light path data into the registry, pick a name, and it's available everywhere.

One-off icons that appear in a single context (like the cycling map key) use inline SVG directly. The registry holds two populations: the handful the interface reuses, and one mark per design-system entry — the icons on the cards that open each section here. The test for whether a name belongs is whether you can say what it names.

Arrow Left Phosphor Light · registry · 14×14 Nav back links
Close Phosphor Light · registry · 12×12 Search dismiss, overlays
Bicycle Phosphor Light · inline · 16×16 Cycling map key
Shield Check Phosphor Light · inline · 16×16 Cycling map key
Train Phosphor Light · inline · 16×16 Cycling map key