Under the hood

This portfolio is also a product.

Building it felt like a design project from the start. Every colour, typeface, and spacing value is a named decision stored in a single token file. The CSS follows ITCSS — specificity only increases where it has to. Components have one job and a consistent naming convention. JavaScript is an enhancement, not a dependency. If you've landed here, you're probably the kind of person who checks the source before closing the tab.

Design tokens

The building blocks of every decision.

85 named custom properties in one file. No hex values, hardcoded sizes, or magic numbers anywhere else in the codebase.

Colour palette

Base, text, accent, and semantic feedback tokens. Contrast ratios shown where the token is used as foreground text.

Base

--color-bg #f5f2ed
--color-surface #ede9e3
--color-border #d6d0c8

Text

--color-text-primary #1a1614 16:1 AAA on bg
--color-text-secondary #635e5a 4.5:1 AA on surface
--color-text-nav #3d3a37

Accent

--color-accent #1a6b52 4.5:1 AA on bg
--color-text-on-accent #f5f2ed 10:1 AAA on teal
--color-accent-light #e6f0ec
--color-error #b83c28

Feedback — four semantic statuses, each with bg / border / text roles

--color-feedback-success-bg
--color-feedback-success-border
--color-feedback-success-text
--color-feedback-error-bg
--color-feedback-error-border
--color-feedback-error-text
--color-feedback-warning-bg
--color-feedback-warning-border
--color-feedback-warning-text
--color-feedback-attention-bg
--color-feedback-attention-border
--color-feedback-attention-text

Typography

Four font families, each with a specific role.

The long view Fraunces --font-heading
Designed with care Urbanist --font-body
asks why Caveat --font-accent
--color-accent Cascadia Code --font-mono

Type scale — core nine sizes, rendered in Fraunces.

Type scale --text-5xl · 48px
Type scale --text-4xl · 36px
Type scale --text-3xl · 30px
Type scale --text-2xl · 24px
Type scale --text-xl · 20px
Type scale --text-lg · 18px
Type scale --text-base · 16px
Type scale --text-sm · 14px
Type scale --text-xs · 12px

Spacing scale

8pt scale — every margin, padding, and gap is a multiple of 8px. Bar width equals the exact token value.

--space-1 · 8px
--space-2 · 16px
--space-3 · 24px
--space-4 · 32px
--space-5 · 40px
--space-6 · 48px
--space-8 · 64px
--space-10 · 80px
--space-12 · 96px
--space-16 · 128px
--space-20 · 160px
--space-24 · 192px

Border radius

Five steps from tight corners to full pill.

--radius-sm
4px
--radius-md
6px
--radius-lg
8px
--radius-xl
12px
--radius-full
9999px

Motion and easing

Five named easing curves. Each bar fills with its own curve — the acceleration character is the point. Paused if you prefer reduced motion.

--ease-out cubic-bezier(0.23, 1, 0.32, 1)
--ease-in-out cubic-bezier(0.645, 0.045, 0.355, 1)
--ease-out-quint cubic-bezier(0.22, 1, 0.36, 1)
--ease-in-out-quart cubic-bezier(0.76, 0, 0.24, 1)
--ease-emphasized cubic-bezier(0.2, 0, 0, 1)

Breakpoints

Four breakpoints, five window size classes. Band widths are proportional to the pixel ranges. Scroll on narrow screens.

Component library

Reusable pieces with one job each.

26 component CSS files. Each is self-contained, named by role rather than appearance, and works without JavaScript unless stated.

General UI

Button

.btn

Three variants covering the full range of visual emphasis. 44px minimum touch target on mobile, 40px desktop, scale(0.97) on active.

.btn--primary .btn--secondary .btn--ghost

Navigation

.nav__link

Desktop: underline scaleX(0→1) on active. Mobile: bottom sheet at translateY(100%), backdrop at 50% opacity — z-stack: overlay 300 / sheet 400 / toggle 500.

Desktop inline nav and mobile bottom sheet in one component. Initialised progressively from nav.js — works without JS as plain links.

.nav__link--active .nav__sheet

Case Study Card

.card

Project Name

40% improvement

Short outcome-led description of what was achieved and for whom.

6 min read

Case study card with metric chip, outcome subtitle, and read-time CTA. Lifts on hover with teal accent border and shadow.

.card__metric-chip .card__cta

Case study system

Annotated Image

.cs-annotated-image
Desktop: hover or focus a pin to see its annotation. Mobile: tap a pin to open the modal.

Full-width image with interactive numbered callout pins. Green pins mark improvements; red mark problems. Desktop shows inline tooltips on hover; mobile uses a centred modal on tap.

.cs-annotated-image__callout--positive .cs-annotated-image__callout--negative

Two-Column Editorial

.cs-two-col

Default — text left, media right

Research in practice

Short narrative copy alongside a supporting visual. Used for pairing context with evidence, prototypes, or research artefacts.

Optional caption for the supporting visual.

Flipped — media left, text right

Iteration in context

The --flip modifier reverses the layout on desktop. Mobile always stacks text first, then media, regardless of variant.

Editorial two-column layout for pairing narrative copy with a supporting visual. Two-column on desktop, stacked on mobile.

.cs-two-col--flip

Before / After

.cs-before-after
Before
Original design showing the problem state.
After
Redesigned state showing the improvement.

Side-by-side comparison for redesigns and iterations. Semantic label badges use feedback error and success tokens. Two columns at 905px, stacked below.

.cs-before-after__item--before .cs-before-after__item--after

Decorative Image

.image-block
Decorative — max 40% desktop, full width mobile

Illustrations and SVGs that support narrative without dominating. Never stretched, never annotated, always object-fit contain.

.image-block--decorative

Decorative Image Wide

.image-block
Decorative wide — max 65% desktop, full width mobile

Wider landscape variant for panoramic illustrations that need more horizontal breathing room.

.image-block--decorative-wide

Editorial List

.cs-editorial-list
  1. Discover

    Research, interviews, and problem framing to understand what matters and why.

  2. Define

    Synthesise findings into a clear problem statement and design direction.

  3. Deliver

    Iterate through prototypes and validation, shipping what solves the real problem.

Stacked editorial list for findings, process phases, or ranked themes. Content aligns flush to the container and fills the available width.

.cs-editorial-list--numbered

Metric Grid

.cs-metric-grid
  • 35%

    Search accuracy

    Improvement across 5 million annual searches.

  • 30%

    Feedback rate

    More users completing the feedback flow at zero dev cost.

  • 97 hrs

    Saved monthly

    Freed from the support team by better platform design.

Outcome metrics with value, label, and supporting body text. Teal accent underline on each value. Responsive from 1 to 3 columns.

.cs-metric-grid--four

Pull Quote

.pull-quote

Good defaults are worth more than a hundred options.

— Sam Blake

High-impact statement that interrupts the reading rhythm. 3px teal left accent bar, Fraunces display type at 30–36px.

.pull-quote__text .pull-quote__cite
Architecture

How the site holds together.

Six structural decisions that keep the codebase coherent as it grows.

CSS architecture — ITCSS

Styles are layered from abstract to specific. Settings and variables sit at the foundation. Components build on them. Layouts use both. Specificity flows in one direction and never fights itself.

Design tokens

Every visual decision is a named CSS custom property in _variables.css. No hex values, hardcoded sizes, or arbitrary spacing exist anywhere else in the codebase. One file to update; the whole site responds.

Vanilla HTML and Vite

No framework overhead. Vite handles multi-page entry points, fingerprinted asset paths, and fast dev server rewrites. Each page is a straightforward HTML file registered in rollupOptions.input.

Progressive enhancement

Content renders and works before JavaScript loads. Navigation functions as pure HTML. The typewriter, annotated image interactions, and scroll reveals are layered on top — remove them and the page degrades gracefully.

Accessibility patterns

Every interactive element is keyboard-navigable with a visible focus ring. Touch targets meet the WCAG 2.2 44px minimum. aria-current, aria-label, and screen-reader text are part of every component from day one.

Responsive design

Mobile-first breakpoints at 600, 905, 1240, and 1440px — chosen for real device densities, not round numbers. Hover states are gated behind (hover: hover) and (pointer: fine) to prevent false activation on touch screens.

Design principles

The rules behind the decisions.

Six principles that shaped every component, layout, and interaction on this site.

  1. Content first

    Layout serves content, never the other way around. Every structural decision starts with what needs to be communicated, not what looks interesting.

  2. Semantic HTML

    Heading levels reflect document hierarchy. Lists are lists. Quotes are blockquotes. Markup describes what things are, not how they look.

  3. Progressive enhancement

    The page renders and works before JavaScript arrives. Scripts add behaviour; they do not create it.

  4. Reduced motion support

    Every animation has a reduced-motion counterpart. User preference is respected at the system level, not worked around at the component level.

  5. Accessibility by default

    WCAG 2.2 AA is a constraint, not a milestone. Contrast ratios, focus states, and ARIA labels are part of every component from the first commit.

  6. Lean systems

    Reuse earns its keep or it does not happen. Three similar elements share one component when the abstraction is genuinely simpler. Otherwise, write the thing twice.

By the numbers

Built lean, on purpose.

  • 85

    Design tokens

    Every colour, spacing value, radius, and easing curve named in one file.

  • 28

    Components

    Purpose-built CSS components with one responsibility each.

  • 12

    Pages

    From the home page through three case studies to this one.

  • 0

    JS frameworks

    Vanilla HTML, CSS custom properties, and progressive enhancement only.