Design system

A small system, built to scale.

Every page here is made from the same few decisions, so a new page, component or theme adds no new rules and ships fast. Each decision is a token defined once and named by its job; components only use tokens, and motion is opt-in through attributes. That makes the system the groundwork for AI workflows too: the rules are explicit and written down, so an AI agent builds within the system instead of around it. The values on this page are read from the stylesheet at build time, so the documentation grows with the system.

Principles

  • One accent

    Black and white with a single lime. Colour is reserved for what matters: links, focus, kickers, highlights.

  • Light type, quiet emphasis

    Inter Light for display. Emphasis is a dimmed second voice in the same weight, not bold, italics or a second family.

  • Roles, not colours

    Tokens describe a job (--ink, --muted, --accent). Dark mode swaps the values; no component knows which theme it is in.

  • Motion is optional

    Every effect is an enhancement. Without JavaScript or with reduced motion, content is complete and still.

Colour

Ten roles, each with a light and a dark value, all written in oklch: the first number is perceived lightness, so values compare at a glance across themes. Lines are translucent so they work on any surface. The accent has three jobs in light mode, because the lime itself is too pale for text on white: --accent for text (passes AA), --accent-glow for decoration, --accent-fill as a surface. In dark mode all three are the lime.

  • --bgoklch(98.5% 0 0)oklch(0% 0 0)
  • --surfaceoklch(100% 0 0)oklch(14.5% 0 0)
  • --inkoklch(14.5% 0 0)oklch(100% 0 0)
  • --ink-2oklch(36% 0 0)oklch(85.5% 0 0)
  • --mutedoklch(52.8% 0 0)oklch(63.3% 0 0)
  • --lineoklch(0% 0 0 / 0.12)oklch(88.5% 0 0 / 0.2)
  • --line-strongoklch(0% 0 0 / 0.24)oklch(88.5% 0 0 / 0.38)
  • --accentoklch(52.9% 0.152 134)oklch(92.4% 0.147 123.2)
  • --accent-glowoklch(73.2% 0.212 134.5)oklch(92.4% 0.147 123.2)
  • --accent-filloklch(92.4% 0.147 123.2)oklch(92.4% 0.147 123.2)

Typography

Two families. Inter carries everything readable, in Light (300) for display and Regular for text; JetBrains Mono is only for kickers and labels. Sizes are a fluid scale: each step grows with the viewport between a minimum and a maximum, so there are no breakpoint jumps.

  • --font-sansInter Variable Aa'Inter Variable', ui-sans-serif, system-ui, …
  • --font-displayInter Variable Aavar(--font-sans)
  • --font-monoJetBrains Mono 0123'JetBrains Mono Variable', ui-monospace, 'SF…
  1. --step-4Designing progressclamp(2.25rem, 1.6rem + 3vw, 4.25rem)
  2. --step-3Designing progressclamp(1.75rem, 1.4rem + 1.6vw, 2.75rem)
  3. --step-2Designing progressclamp(1.4rem, 1.25rem + 0.7vw, 1.8rem)
  4. --step-1Designing progressclamp(1.15rem, 1.08rem + 0.35vw, 1.3rem)
  5. --step-0Designing progressclamp(1rem, 0.96rem + 0.2vw, 1.0625rem)
  6. --step--1Designing progressclamp(0.83rem, 0.8rem + 0.1vw, 0.875rem)
  7. --step--2Designing progress0.6875rem

Space and shape

The page breathes through a fluid gutter and generous section padding rather than a spacing scale. Shapes are either barely rounded (images) or fully round (pills).

  • Layout

    Content sits in a centred column of at most --max, inset by a fluid gutter that grows with the viewport.

    --gutter
    clamp(16px, 4vw, 48px)clamp(16px, 4vw, 48px)
    --max
    1240px1240px
  • Radius

    Two shapes only: images barely rounded, pills fully round.

    --r-image
    4px4px
    --r-pill
    100px100px
  • Hover me

    Glass

    Cards and ghost buttons. Hover deepens the border and the inner glow.

    --glass
    radial-gradient(140% 180% at 22% -20%, oklch…radial-gradient(140% 180% at 22% -20%, oklch…
    --glass-border
    oklch(0% 0 0 / 0.1)oklch(100% 0 0 / 0.1)
    --glass-border-hover
    oklch(0% 0 0 / 0.2)oklch(100% 0 0 / 0.22)
    --glass-glow
    inset 0 0 20px oklch(0% 0 0 / 0.04)inset 0 0 20px oklch(100% 0 0 / 0.1)
    --glass-glow-hover
    inset 0 0 28px oklch(0% 0 0 / 0.1)inset 0 0 34px oklch(100% 0 0 / 0.35)
  • Button

    The primary pill: solid in light mode, glass with a strong glow in dark mode.

    --btn-bg
    oklch(14.5% 0 0)var(--glass)
    --btn-ink
    oklch(100% 0 0)oklch(100% 0 0)
    --btn-border
    oklch(0% 0 0 / 0.9)oklch(100% 0 0 / 0.14)
    --btn-glow
    inset 0 0 20px oklch(100% 0 0 / 0.18)inset 0 0 34px oklch(100% 0 0 / 0.35)
    --btn-glow-hover
    inset 0 0 28px oklch(100% 0 0 / 0.34)inset 0 0 40px oklch(100% 0 0 / 0.5)
  • Case study

    Kicker

    A lime chip in light mode; glowing lime text without a chip in dark mode.

    --kicker-ink
    oklch(14.5% 0 0)oklch(92.4% 0.147 123.2)
    --kicker-bg
    var(--accent-fill)transparent
    --kicker-pad
    0.3em 0.5em 0.25em0
    --kicker-glow
    none0 0 14px oklch(92.4% 0.147 123.2 / 0.45)
  • Case studyExample projectCover image coming

    Tint and grain

    How strongly a project hue washes its cover, and the film grain over every page.

    --cover-tint
    0.140.2
    --grain-opacity
    0.070.06

Components

Each component does one job and takes its look only from tokens. Live examples below render with the same code as the rest of the site.

  • Button

    .btn · .btn-ghost

    Glass pill. Primary is the solid one per group; ghost buttons sit beside it. Hover deepens the inner glow instead of changing colour.

  • Case study

    Client · 2025

    Kicker

    .eyebrow · .mono-label

    JetBrains Mono in caps. Labels a section or a fact. A lime chip in light mode, glowing lime text in dark mode.

  • Designing progress, with confidence.

    Display headline

    .display + <em>

    Inter Light with tight tracking. The <em> part is the dimmed second voice.

    • Design systems
    • Research
    • Workshops

    Tags

    .tag-list

    Outlined pills for skills, tools or scope. Information only, never interactive.

  • Divider

    data-rule="top|bottom"

    A hairline drawn left to right as it scrolls into view. Colour and weight via --rule-c and --rule-w.

  • Case studyExample projectCover image coming

    Cover

    <Cover client hue cover? />

    Case study image in a glass card. Without an image it falls back to a typographic card tinted in the project’s hue.

  • Header · Footer

    Site frame. The brand plus three links; the footer carries legal pages and this page.

  • Work mosaic

    Selected work as a grid: the lead tile full width, two beneath it. Each tile carries the cursor-trail shader.

  • Contact

    Closing section on Home and About: display headline over a dither field, then the button group.

  • Protected

    Shows email, phone or address without putting them in the HTML. Decoded in the browser to keep simple scrapers out.

Motion and effects

Components never animate themselves. They opt in with a data attribute, and one script handles each kind of motion for the whole site, so timing stays consistent. Interface motion is short and eases out: colour changes in 140 ms, movement in 220 ms. Effects load after the page is idle and switch off for reduced motion.

Timing and easing

TokenLightDark
--easecubic-bezier(0.22, 0.61, 0.36, 1)same
--motion-color140ms var(--ease)same
--motion-position220ms var(--ease)same
  • Designing progress, with confidence.

    data-split

    Headline lines slide up out of a mask, one after another.

  • data-unmask

    Images open from the left edge while settling from a slight zoom.

  • data-rise

    Tiles and text fade up 18px with a short stagger.

  • data-rule

    Dividers draw from left to right.

  • data-dither

    Canvas with a drifting 1-bit dither field and an accent light that follows the pointer.

  • data-shader

    Cover image with a cursor trail that warps the pixels along the direction of travel.