The system

Every token, component and state, rendered at real size.

This page links the same four stylesheets the site uses, so it renders the actual components rather than a drawing of them. Every hex value and contrast ratio below was read out of css/tokens.css when the page was generated, none of them are typed by hand, so this cannot disagree with the code.

Primitives

Raw values. Never reference these from a component: they exist only to feed the semantic layer below. The neutrals carry a ~2% warm bias at the accent's hue, so the greys read as the yellow's family rather than as an unrelated scale.

Neutrals

--neutral-950 #0D0D0C 1.00:1 on the panel
--neutral-900 #141412 1.05:1 on the panel
--neutral-850 #1C1C19 1.14:1 on the panel
--neutral-800 #24241F 1.25:1 on the panel
--neutral-750 #262622 1.28:1 on the panel
--neutral-700 #33332D 1.53:1 on the panel
--neutral-650 #4E4E48 2.32:1 on the panel
--neutral-500 #6E6E64 3.78:1 on the panel
--neutral-400 #7C7C74 4.62:1 on the panel
--neutral-300 #9C9C92 7.02:1 on the panel
--neutral-200 #C6C6BB 11.29:1 on the panel
--neutral-050 #F5F5F0 17.78:1 on the panel

Bone

--bone-000 #F7FAF1 1.12:1 on the page
--bone-050 #E8EFD8 1.00:1 on the page
--bone-100 #DEE7C8 1.09:1 on the page
--bone-150 #D2E0B2 1.18:1 on the page
--bone-200 #C9D7AA 1.29:1 on the page
--bone-300 #B1C18C 1.63:1 on the page
--bone-600 #666E55 4.53:1 on the page
--bone-700 #585E4A 5.70:1 on the page
--bone-800 #404437 8.46:1 on the page
--bone-900 #131410 15.65:1 on the page

Yellow

--yellow-300 #FFF04D 1.00:1 on the page
--yellow-400 #FFE800 1.06:1 on the page
--yellow-500 #E6D100 1.32:1 on the page
--yellow-700 #8A7D00 3.54:1 on the page
--yellow-800 #6E6300 5.15:1 on the page

Support

--red-400 #FF6B6B 2.35:1 on the page
--green-400 #6BE39A 1.36:1 on the page

Semantic colour

What components read. Each shows the token it points at and its contrast against the ground it is meant to sit on: the page for most, the dark panel for the panel set, the accent fill for the ink that goes on it. There is no third component tier; that layer exists for multi-brand theming and would be structure maintained forever and never used here.

Surfaces

--color-page #E8EFD8 var(--bone-050) 1.0:1 on the page
--color-surface #F7FAF1 var(--bone-000) 1.1:1 on the page
--color-raised #DEE7C8 var(--bone-100) 1.1:1 on the page
--color-sunken #D2E0B2 var(--bone-150) 1.2:1 on the page

Lines

--color-line #C9D7AA var(--bone-200) 1.3:1 on the page
--color-line-strong #B1C18C var(--bone-300) 1.6:1 on the page
--color-line-interactive #666E55 var(--bone-600) 4.5:1 on the page

Text

--color-text #131410 var(--bone-900) 15.7:1 on the page
--color-text-body #404437 var(--bone-800) 8.5:1 on the page
--color-text-muted #585E4A var(--bone-700) 5.7:1 on the page
--color-text-dim #666E55 var(--bone-600) 4.5:1 on the page

Accent

--color-accent #FFE800 var(--yellow-400) 1.1:1 on the page
--color-accent-hover #FFF04D var(--yellow-300) 1.0:1 on the page
--color-accent-active #E6D100 var(--yellow-500) 1.3:1 on the page
--color-link #6E6300 var(--yellow-800) 5.2:1 on the page
--color-link-hover #131410 var(--bone-900) 15.7:1 on the page
--color-accent-quiet #6E6300 var(--yellow-800) 5.2:1 on the page
--color-on-accent #131410 var(--bone-900) 14.8:1 on the accent fill

The contrasting panel

--color-panel #0D0D0C var(--neutral-950) 1.0:1 on the panel
--color-panel-text #F5F5F0 var(--neutral-050) 17.8:1 on the panel
--color-panel-body #C6C6BB var(--neutral-200) 11.3:1 on the panel
--color-panel-muted #9C9C92 var(--neutral-300) 7.0:1 on the panel
--color-panel-dim #7C7C74 var(--neutral-400) 4.6:1 on the panel
--color-panel-line #262622 var(--neutral-750) 1.3:1 on the panel
--color-panel-frame #4E4E48 var(--neutral-650) 2.3:1 on the panel
--color-accent-on-panel #FFE800 var(--yellow-400) 15.6:1 on the panel

Focus

--color-focus #131410 var(--bone-900) 15.7:1 on the page

Selection

--color-selection #FFE800 var(--yellow-400) 1.1:1 on the page
--color-selection-text #131410 var(--bone-900) 14.8:1 on the accent fill

Typography

A 1.25 major-third scale. Bricolage Grotesque carries display, IBM Plex Sans reads, IBM Plex Mono labels. The two largest steps are fluid and clamp with the viewport.

Axel Bruusgaard Jewett
--size-900
clamp(2.25rem, 7vw, 4rem)
h1 · hero
Selected Case Studies
--size-800
clamp(1.75rem, 3.5vw, 2.25rem)
h2 · page titles
Morescope: Making Target Setting Easier
--size-700
1.625rem
h3 · section titles
Healthcare: Staff Training
--size-600
1.3125rem
h4 · card titles
UX designer committed to building clear products.
--size-500
1.0625rem
lead paragraphs
Redesigned the SBTi target-setting flow for an ESG platform.
--size-400
0.9375rem
body · button label
Mobile AR training tool for healthcare staff.
--size-300
0.8125rem
small UI · tags
SBTi targets dashboard overview
--size-200
0.75rem
meta · captions
ESG PLATFORM · 2025
--size-100
0.6875rem
eyebrows · labels

Space

One 4px-based scale for components, patterns and layout alike. Do not add steps, combine existing ones instead. On this page the scale carries the structure, because surfaces sit at 1.12:1 against the page and lines at 1.29:1, so a card is barely a box.

--space-0   0
--space-1   0.25rem
--space-2   0.5rem
--space-3   0.75rem
--space-4   1rem
--space-5   1.5rem
--space-6   2rem
--space-7   2.5rem
--space-8   3rem
--space-9   4rem
--space-10   5rem

Radius

--radius-sm
6px
--radius-md
10px
--radius-lg
14px
--radius-full
999px

Button radius is a ratio, not a number. A radius becomes a pill the moment it reaches half the element's height, 29px is a squircle at 72px tall and a pill at 47px. Each button size sets its own radius at ~40% of its height. If you add a size, do the same.

Button variants

Three levels of emphasis. One primary per view. No destructive variant and no loading state: the site has no destructive actions and nothing asynchronous, so both would be dead code. Add them the day something deletes or waits.

Primary .button.button--primary the accent fill. One per view.
Secondary .button.button--secondary outlined. Uses --color-line-interactive at 3.8:1 so the outline is legal as the only cue.
Ghost .button.button--ghost no chrome until hover. Lowest emphasis.

Button sizes

Four sizes, all the same shape. Measured heights with borders included: 44px, 56px, 64px, 75px, every size meets the 44px touch minimum.

--sm .button.button--primary.button--sm 44px tall · radius 16px, secondary actions
(default) .button.button--primary 56px tall · radius 21px, most buttons
--lg .button.button--primary.button--lg 64px tall · radius 24px, hero
--hero .button.button--primary.button--hero 75px tall · radius 29px, one per page, maximum

Button states

Rest--color-accent
Hover--color-accent-hover
Active--color-accent-active
Focus--focus-ring · two rings, visible on any surface
Disabled40% opacity, pointer-events off

The dark panel

A section-level device, not a card style. One or two moments per page: a featured case, a framed screenshot, a bio block. Everywhere and it stops being an accent and becomes a dark site with olive gaps.

Everything inside inverts automatically

Text, headings and rules all swap to the light scale with no extra classes. This paragraph is --color-panel-body at 11.3:1 on the panel.


This link is --color-accent-on-panel: the neon measures 15.6:1 here, so it keeps its colour. On the page it is 1.1:1, and links there use the darkened yellow at 5.2:1 instead.

Selected work Contact me ESG Platform

The yellow fill works on either ground: dark ink on it measures 14.8:1 wherever it sits. Only links change job.

Components

Card .card Sits at 1.05:1 against the page, defined by its padding, not its edge.
ESG Platform Featured
Tag .tag / .tag--accent
Meta .meta
Arrow link .link-arrow Arrow is a ::after, so translations never lose it.
Quiet link .link-quiet
Neutral notice.
Accent notice.
Success.
Danger.
Notice .notice Unused today. Exists so a contact form has patterns ready.
How should I address you?
Field .field / .input
Add the part after the @, for example axel@gmail.com.
Field, invalid .field--invalid Never colour alone: the border thickens and the message carries the reason.
Textarea .textarea

Toggle

Two equal options, not an on/off switch. Built as two real buttons inside a role="group", each carrying aria-pressed, so a screen reader announces which language is active rather than leaving it to colour alone. It is wired to i18n/no.json.

Default .toggle > .toggle__option[aria-pressed] Sits beside the nav links.
Small .toggle.toggle--sm Tight headers, footers.
Large .toggle.toggle--lg Standalone, with full language names.

Card sizes

Padding and title size move together, so a large card reads as the same component at a bigger scale rather than a stretched small one. .card--media drops the container padding so an image can sit flush to the edges.

Morescope

Redesigned the SBTi target-setting flow.

ESG Platform 2025

Morescope: Making Target Setting Easier

Redesigned the SBTi target-setting flow for an ESG compliance platform.

ESG Platform 2025

Morescope: Making Target Setting Easier

SBTi updated its framework mid-project, making the existing module

ESG Platform 2025

unworkable. I mapped the gap between the old and new requirements, then redesigned the full target-setting flow.

View case study

.card.card--sm
Dense grids, related links.
.card
Default. The work grid.
.card.card--lg
A featured case, or one card standing alone.
.card.card--media
Image flush to the edges, content padded inside.

Rules

RuleWhy
Structure comes from space, not linesSurfaces are 1.12:1 and lines 1.29:1 against the page. A card is barely a box, so spacing and type size carry the layout.
--color-line is never the only cue on a control1.29:1. WCAG 1.4.11 wants 3:1 for interactive elements: use --color-line-interactive at 4.53:1.
Button radius scales with heightA fixed radius turns into a pill once it passes half the height.
The accent changes job on the page15.6:1 on the dark panel, 1.06:1 on the page. Fills keep the neon anywhere; links use --color-link at 5.2:1 instead.
A heading that is a link keeps the heading colourOtherwise the accent wins and every case title turns yellow.
The dark panel is section-levelUsed everywhere it stops being a contrast and becomes the page.
Never reference a Tier 1 token from a componentBreaks the one-way primitive → semantic flow the system depends on.
Focus is never removedIt is the only cue a keyboard user gets. Two rings, so it reads on the page and on the panel.