01 · Philosophy
- Tokens first. A color only appears in a component as
var(--color-*). No hardcoded hex values inside modules. - Four layers, referenced top-down:
- Primitives — typography, spacing, radius, motion, layout.
- Palette — raw OKLCH grayscale + accents (orange, green).
- Semantic —
--color-bg,--color-text-*,--color-brand,--color-border*, etc. Dark lane only. - Legacy aliases —
--bg,--text,--white,--border,--radius,--font,--mono,--easeso existing rules keep rendering without a global find-and-replace.
- No light theme. The single lane is dark —
color-scheme: dark. The TUI ships its own palette, independent. - Fluid scale. Typography + spacing grow with the viewport via
clamp()— one scale for every breakpoint.
Tokens live in src/css/tokens.css. Any semantic change happens there — it cascades.
02 · Colors
2.1 — Raw palette (OKLCH grayscale)
Perceptually uniform source, used only to derive the semantic tokens. Do not consume directly in components.
| Token | OKLCH | Indicative usage |
|---|---|---|
--gray-0 | oklch(1 0 0) | Pure white (avoid outside borders) |
--gray-1 | oklch(0.98) | Hi-contrast on dark |
--gray-5 | oklch(0.60) | Mid-gray |
--gray-6 | oklch(0.47) | Text-muted zone |
--gray-9 | oklch(0.17) | Surface zone |
--gray-10 | oklch(0.10) | Surface elevated |
--gray-11 | oklch(0.04) | --color-bg source |
2.2 — Accents
| Token | Value | Usage |
|---|---|---|
--orange-400 | #ff8533 | Light variant |
--orange-500 | #ff6a00 | --color-brand source |
--orange-600 | #e05a00 | Hover / pressed |
--green-500 | #2ed573 | --color-status-available source |
--green-600 | #26b462 | Dark variant |
2.3 — Semantic tokens (what you actually consume)
Component working layer. Any theme shift would flow through these tokens alone.
| Token | Value | Role |
|---|---|---|
--color-bg | var(--gray-11) | Page background |
--color-surface | rgba(255,255,255,0.02) | Subtle surface |
--color-surface-hover | rgba(255,255,255,0.05) | Surface hover |
--color-surface-pressed | rgba(255,255,255,0.08) | Active / pressed |
--color-surface-elevated | #0a0a0a | Cards, raised panels |
--color-surface-floating | rgba(10,10,10,0.96) | Overlays (legal sheet, consent) |
--color-text-strong | #ffffff | Title, max emphasis |
--color-text-primary | #e0e0e0 | Main copy |
--color-text-secondary | rgba(255,255,255,0.7) | Paragraphs |
--color-text-muted | #808080 | Labels, meta |
--color-text-faint | rgba(255,255,255,0.3) | Whisper |
--color-border | rgba(255,255,255,0.06) | Subtle rule |
--color-border-strong | rgba(255,255,255,0.15) | Emphasised rule |
--color-focus-ring | rgba(255,255,255,0.55) | :focus-visible outline |
--color-brand | var(--orange-500) | Accent orange |
--color-brand-soft | rgba(255,106,0,0.25) | Glow, hover halo |
--color-status-available | var(--green-500) | Availability pill |
--color-emphasis | rgba(255,255,255,0.85) | <strong> / heavy copy |
2.4 — Foreground opacity scale
Replaces every hand-rolled rgba(255,255,255,X). One base token --color-fg = white, stepped from 2 % to 90 %.
--color-fg-2 · -4 · -5 · -6 · -8 · -10 · -12 · -15 · -18 · -20 · -25 · -30 · -35 · -40 · -45 · -50 · -55 · -60 · -65 · -70 · -75 · -80 · -85 · -90
Typical use: borders (--color-fg-6), quiet surfaces (--color-fg-10), secondary text (--color-fg-70), separators (--color-fg-15).
2.5 — TUI palette (terminal lane)
Independent of the main palette. Three themes stacked, switched via /theme <name> inside the TUI.
| Theme | bg | text | accent | green | cyan | yellow |
|---|---|---|---|---|---|---|
default | #0d1117 | #e6edf3 | #ff6a00 | #56d364 | #58a6ff | #f9c74f |
matrix | #000a00 | rgba(0,255,70,.8) | #00ff46 | #00ff46 | #00cc36 | #66ff66 |
retro | #1a0a2e | rgba(255,183,77,.85) | #ff6ec7 | #00ff9f | #00d4ff | #ffb74d |
03 · Typography
3.1 — Families
| Token | Family | Usage |
|---|---|---|
--font-sans | Manrope | Prose, headings, UI — variable weight 200–800 |
--font-mono | JetBrains Mono | Labels, meta, TUI, code |
Both fonts are self-hosted (src/assets/fonts/*.woff2) and preloaded in <head> to eliminate FOUT.
3.2 — Fluid scale (clamp())
| Token | Range | Usage |
|---|---|---|
--text-xs | 0.6rem → 0.68rem | Mono uppercase labels |
--text-sm | 0.78rem → 0.88rem | Captions |
--text-base | 0.95rem → 1.05rem | Body copy |
--text-lg | 1.1rem → 1.3rem | Lead paragraph |
--text-xl | 1.4rem → 1.75rem | Sub-headings |
--text-2xl | 1.8rem → 2.3rem | Section titles |
--text-3xl | 2.5rem → 3.5rem | Page titles |
--text-4xl | 3.5rem → 5rem | Hero / display |
3.3 — Conventions
- Labels (
.labelinbase.css):--font-mono,--text-xs, uppercase, letter-spacing0.14em, color--color-brand. - Emphasis:
<strong>inherits--color-emphasis. In statements,<strong>chars turn into.char-boldat 800 uppercase. - Tracking: headings from
-0.02emto-0.05emdepending on size (larger = tighter).
04 · Spacing — 8pt grid
Only these tokens should drive padding, margin, gap. No arbitrary values.
| Token | rem | px |
|---|---|---|
--space-0 | 0 | 0 |
--space-1 | 0.25rem | 4 |
--space-2 | 0.5rem | 8 |
--space-3 | 0.75rem | 12 |
--space-4 | 1rem | 16 |
--space-5 | 1.5rem | 24 |
--space-6 | 2rem | 32 |
--space-7 | 3rem | 48 |
--space-8 | 4rem | 64 |
--space-9 | 6rem | 96 |
--space-10 | 8rem | 128 |
Derived layout tokens:
--container=clamp(1.5rem, 4vw, 4rem)— container horizontal padding.--section=clamp(6rem, 12vh, 10rem)— vertical padding between sections.
05 · Radius
| Token | Value | Usage |
|---|---|---|
--radius-sm | 4px | Inputs, micro-surfaces |
--radius-md | 8px | Standard cards |
--radius-lg | 12px | Content panels |
--radius-xl | 16px | Floating panels (legal sheet) |
--radius-pill | 9999px | Pills, tags, round buttons |
06 · Motion
Every animation consumes one of 5 durations and one of 4 eases. No free-form timings.
6.1 — Durations
| Token | Value | Usage |
|---|---|---|
--duration-instant | 100ms | Immediate feedback (toast, focus ring) |
--duration-fast | 200ms | Hover, toggle |
--duration-base | 300ms | Default for most transitions |
--duration-slow | 600ms | Reveal, char lit, page transitions |
--duration-deliberate | 900ms | Marked entry effects |
6.2 — Eases
| Token | cubic-bezier | Character |
|---|---|---|
--ease-out | (0.19, 1, 0.22, 1) | Default — soft exit |
--ease-in-out | (0.65, 0, 0.35, 1) | Two-way |
--ease-spring | (0.2, 0.9, 0.3, 1.2) | Slight overshoot |
--ease-power3 | (0.33, 1, 0.68, 1) | Stronger ease-out |
6.3 — Conventions
- Every transition respects
prefers-reduced-motion: reduce— section-level opt-out. - Entry animations (reveal) use GSAP + ScrollTrigger (
src/js/main.js). - The hero ASCII animation is orchestrated in
src/js/hero.js— 3 phases: scramble → color transition → cross-fade.
07 · Elevation (shadows)
| Token | Value | Usage |
|---|---|---|
--shadow-sm | 0 1px 2px rgba(0,0,0,0.4) | Subtle cards |
--shadow-md | 0 6px 20px rgba(0,0,0,0.35) | Panels, overlays |
--shadow-lg | 0 20px 60px rgba(0,0,0,0.6) | Modals, floating menus |
08 · Components
Every component ships in the main CSS bundle and is consumed through its root class. Section 08 above exposes each component in playground mode (variants / states / props).
8.1 — Primitives
| Name | Root class | File |
|---|---|---|
| Section label | .label | base.css |
| Expertise tag | .exp-tags span | sections.css |
| Availability pill | .exp-available | sections.css |
| Speaking venue chip | .speaking-venue | sections.css |
| Speaking CTA | .speaking-cta | sections.css |
| Consent button | .consent-btn[.--accept] | consent.css |
| Copy button | .copy-btn | sections.css |
| Writing arrow | .writing-arrow | sections.css |
8.2 — Content blocks
| Name | Root class | Anatomy |
|---|---|---|
| Experience item | .exp-item | logo · title-row (+ availability) · date · desc · tags |
| Expertise card | .expertise-item | icon · num · title · desc (hover lift) |
| Speaking item | .speaking-item | type · venue · title · desc · CTA |
| Writing item | .writing-item | title · date · arrow |
| Connect link | .connect-link | label · arrow (hover pad-left) |
8.3 — Shell & nav
| Name | Root class | Note |
|---|---|---|
| Footer legal button | .footer-legal button | Animated underline reveal |
| View toggle (CLI/site) | .view-toggle | Floating pill, revealed after hero |
| Scroll progress bar | .scroll-progress | scaleX(0→1) in rAF |
| Meta chip | .ds-chip | Mono uppercase pill |
8.4 — Overlays
| Name | Root class | Interactions |
|---|---|---|
| Legal sheet | .legal-sheet | Swipe-to-dismiss, focus trap, scroll lock, Escape |
| Consent banner | .consent-banner | GA4 gate, localStorage, consent:change event |
8.5 — Motion-enhanced
| Effect | Implementation |
|---|---|
| Reveal on scroll | .reveal + ScrollTrigger (main.js) |
| Availability pulse | CSS keyframes exp-available-pulse (2s infinite) |
| Statement char-by-char | Progress 0→1 → .char.lit per segment |
| Hero ASCII scramble | 3-phase rAF loop with dedicated glyph set |
| Legal sheet slide-in | Transform + opacity, spring ease |
8.6 — TUI
| Element | Class |
|---|---|
| User message | .tui-block-user |
| Assistant message | .tui-block-assistant |
| Tool message | .tui-block-tool |
| Quick command chip | .tui-cmd-btn[.active] |
| Status bar | .tui-statusbar |
| Theme palette | --tui-{default/matrix/retro}-* |
09 · Focus & accessibility
- Focus visible: outline
2px solid var(--color-focus-ring)+outline-offsetof 2 to 4px. Applied to every:focus-visible. - Tab order preserved by construction — no custom
tabindexoutside overlays (the legal sheet installs a focus trap). - Reduced motion: every animation has its fallback via
@media (prefers-reduced-motion: reduce). - Contrast: palette tuned to ≥ AA on
--color-bg.--color-text-faintis reserved for decorative whisper (never informational copy).
10 · Interactive explorer
This page exposes every token and component in Storybook-style playground mode:
- Sections 01–07: palette, semantic, fg opacity, typography + type tester, spacing, radius, motion player.
- Section 08 · Components: filterable grid (search + category chips — Primitives / Content / Shell / Overlays / Motion / TUI). Each story has: live canvas (with
bg/surface/elevatedbackground toggle), variant/state/props controls, copyable source block. - Section 09 · TUI palette: the 3 terminal themes.
:hover and :focus are simulated through a data-state attribute mirrored in ds.css — scoped to .ds-story-canvas, zero production impact.
Access
- Direct URL:
/design-system.html. - Easter egg: Konami code (↑↑↓↓←→←→BA) on the home page → reveals the
///glyph in the footer which links to this page. The unlock persists inlocalStorage:hm:ds-unlocked.
11 · Code conventions
- Tokens only. A component never consumes a hex / rgba value directly. Go through
var(--*). - Naming. Component classes in kebab-case, prefixed by their scope (
exp-,speaking-,writing-,tui-,legal-,consent-,ds-). - CSS modules in
src/css/— one file per domain (sections.css,tui.css,legal.css,consent.css,animations.css,responsive.css). Inclusion order enforced byscripts/build.mjs(CSS_ORDER). - JS modules in
src/js/. Same ordering pattern (JS_ORDERinbuild.mjs). - No inline CSS except when a style depends on a runtime value (e.g.
style="transform: scaleX(X)"for scroll-progress, or palette swatches that must display avar(--token)as background).
12 · Build & loading
- esbuild for JS (concat + minify + sourcemap).
- Lightning CSS for CSS (bundle + minify, targets via browserslist).
- html-minifier-terser for HTML.
- Cache busting: 8-char hash in the filename (
main.{hash}.{css|js},ds.{hash}.{css|js}).immutable · max-age=31536000headers on hashed files through the generated.htaccess. - Self-hosted fonts,
preload-ed in<head>to avoid FOUT. - OG image generated at build time (1200×630, SVG with Manrope/JBM embedded as base64 → PNG via sharp).
13 · Quick cheatsheet
| Need | Token / class |
|---|---|
| Main text | color: var(--color-text-primary) |
| Emphasis text | color: var(--color-text-strong) |
| Subtle border | border: 1px solid var(--color-border) |
| Accent pill | background: var(--color-brand); border-radius: var(--radius-pill); |
| Hover state in a DS story preview | [data-state="hover"] on .ds-story-canvas |
| New spacing | Check --space-* before any custom value |
| One-off new color | Use a --color-fg-* opacity step |