Colophon
How this site is built
Four typefaces, six colours, and a rule for each. If a device appears without its meaning, it is a bug.
Typefaces
Each family has one job. None of them does a second one.
| Family | Weights | Job |
|---|---|---|
| Bodoni Moda | 600 / 700 | Display. A claim I am making — headlines and the proof-band numbers. Never data, never labels. |
| Archivo | 400 / 500 / 600 | Structure. Body copy, navigation, buttons, h3 — everything functional. |
| IBM Plex Mono | 400 / 500 | Data. A verifiable fact: dates, locations, counts, scope notes, breadcrumbs. |
| Silkscreen | 400 | Pixel. A credential or a status token, at 10px. Never a full sentence. |
Colours
Six, and no additions. Tints are derived with color-mix() rather than invented, so every shade on the site traces back to one of these.
| Token | Value | Meaning |
|---|---|---|
| --ink-plate | #0B0D14 | Base canvas and every dark band. |
| --press-paper | #F2EFE6 | Text on dark bands. Also the inverted editorial band, which the home page no longer uses. |
| --indigo-signal | #5B5BF0 | Actionable: links, focus rings, primary CTA fill. |
| --cyan-scan | #35E6FF | A relationship between data points: hairlines, data labels, the scanline. |
| --brass-nouveau | #C9A227 | Victorian ornament only, never above 12% opacity. |
| --graffiti-red | #FF3B2E | This is live right now. Three uses per page, no more. |
One exception, and it is deliberate. --indigo-signal measures 3.88:1 against the dark band and 4.35:1 against paper — both below the 4.5:1 that body-size text needs. Link text therefore uses a derived mix of it (5.77:1 and 5.04:1), while the focus ring keeps the pure token, where 3.88:1 clears the 3:1 a non-text indicator requires. Accessibility wins over the palette.
What each device means
Every visual device is assigned one job and appears only where that job applies. You should be able to work the rule out without being told. Two consequences follow literally: a download button is never glass, and an in-page card never gets the brutalist offset.
| Device | Means | Appears on |
|---|---|---|
| Bodoni display | A claim I am making | Headlines only |
| IBM Plex Mono | A verifiable fact | Dates, locations, counts, scope notes, breadcrumbs |
| Silkscreen pixel | A credential or a status token | Certification badges, the availability pill |
| Graffiti stroke | This is live right now | Hero headline, current role, contact CTA — exactly three |
| Brass filigree | A chapter boundary | Once per page, at the one chapter break |
| Cyan hairline | A relationship between data points | Stat separators, timeline spine, tag borders |
| Glass surface | Floats above the page and stays on it | What I Do cards, certification cards |
| Brutalist offset | Takes you away from the page | Résumé download, case-study links, external certificate links |
| Indigo signal | Actionable | Links, focus rings, primary CTA fill |
The budget this site holds itself to
When an effect cannot be built inside these numbers, the effect is cut rather than the budget raised.
| Measure | Limit |
|---|---|
| Added JavaScript for motion | ≤ 45KB gzipped over the no-motion build |
| Cumulative Layout Shift | ≤ 0.02 |
| Interaction to Next Paint | ≤ 200ms |
| Mobile Largest Contentful Paint | ≤ 2.0s |
| Long tasks during scroll | None over 50ms |
| Simultaneous ambient animations | At most two, and none while the tab is hidden |
| Animatable properties | transform, opacity, clip-path, stroke-dashoffset — nothing else |
| Body copy contrast | ≥ 4.5:1. Large display ≥ 3:1. |
Two checks that run on every build
scripts/verify-assets.mjs resolves every referenced file against a real directory listing and fails the build on anything missing or mis-cased. It reads the directory rather than calling fs.existsSync, which returns true for the wrong case on Windows and macOS and then 404s on a case-sensitive host.
scripts/verify-tokens.mjs fails the build on a raw colour, a raw duration, an easing curve, a font-family declaration or a forced override outside the token layer. One-off values are how a system quietly rots into a dump three edits from now.