Digital Design, 14 October 2026
Easel 3: where we start
One foundation for Figma and code: what is proposed, how it works, and what we decide together.
Kurt, 14 October 2026. For the design team.
Part 1
Where Easel is today
What has drifted between Figma and code, and why it keeps happening.
Easel has drifted, in Figma and in code
In Figma
- Buttons of the same size come in several different heights.
- Lacking an icon-only button component.
- The page gutter changes can be inconsistent.
In code
- The Button component in Storybook is fairly well built and consistent.
- It has no icon-only button either. The carousel needed one, so devs bent the tertiary Button into shape with Tailwind overrides. This creates an unexpected dependency.
- Gutters vary wildly across pages and components.
Nobody is doing a bad job. Keeping Figma and code in step is nobody’s job, so every team makes a sensible local call and the system drifts.
Every inconsistency is a decision someone has to make again
First by a designer, then by an engineer, and soon by an AI agent that copies whatever it finds.
Easel’s foundation grew as we needed it. Now we know enough to simplify it
Figma
Components, text styles and variables
Code
React components and Storybook, in one repo
One foundation, agreed together
- Type
- Spacing
- Page gutter
- Colour
- Radius, shadows and the rest
Growing one need at a time is fine. Now the four of us agree one foundation first, and engineering can follow. It is built for what comes next: AI agents that work from our tokens, and fluid values that fit every screen.
Part 2
Tokens and patterns
A token is a decision with a name
A value: 16px
A number in a file. Nobody can tell whether it is the side of the page, the gap in a card or a guess.
A token: page-gutter
The same number, named for its job. Change it once and every page and every section updates.
The name also says what to use and when. A designer picks it without measuring, an engineer builds it without guessing, and QA has one right answer to check, so all three go faster.
In Figma a token is a variable. A pattern is how we agree to use it
Variable
Figma’s word for a token. A variable can have more than one value, each in its own mode: one for each screen width, or one for each colour scheme.
Pattern
A decision we have agreed and written down, with the reason. For example: every section takes its side padding from page-gutter, so content lines up down the page.
An AI agent can read a token, but not why we chose it. The pattern is where a designer, an engineer and an agent all read the reason.
Tokens come in three layers
Component tokens
One component’s own colours, used only inside that component.
- button-primary-background
- button-primary-foreground
- button-primary-border
- button-secondary-background
- button-secondary-foreground
- button-secondary-border
Semantic tokens
A job anywhere on the page. Designers and engineers use these every day.
- background-primary
- foreground-primary
- foreground-secondary
- border-primary
Primitive tokens
The values. Only other tokens use these.
- black
- black-300
- white
- white-alpha-60
- grey-400
- brand-c-red
In Figma, each layer is a variable collection, and each variable is an alias of one in the layer below. With its own tokens, a button can change without moving every link too. The names are placeholders until we agree them as a team.
Designers, engineers and agents share one set of names
Figma variable
foreground-primary
CSS
--foreground-primary
Tailwind
text-foreground-primary
An agent's brief
‘Use foreground-primary.’
An agent reads the Figma file and the codebase. When both use the same names, it copies the system instead of guessing at hex values.
Part 3
The proposal
Fewer values, one set of names, and sizes that grow smoothly with the screen instead of jumping at breakpoints.
Fewer values to keep in step, and fewer frames to draw
62→12type sizes
39→16text styles
62→3line heights
7→2widths to draw
The look & feel is good. The system underneath it is what we are building, and every number here is a draft to tune together.
What the proposal covers, and where each part stands
Type
12 sizes and 16 roles, each size set at 390, 1440 and 2560. Drafted, with the 2560 sizes still to tune.
Spacing
One scale of 15 steps, and fluid steps that grow with the type.
Page gutter
One token: 16 at 390 and 48 at 1440, then growing like the rest of the spacing.
Widths we draw
390 and 1440, plus any width where the layout changes, such as the switch to the mobile view. 2560 is checked.
Colour
Tokens named for their job, so one component works in any section. The names are placeholders.
Everything else
Radius, shadows, motion and sizes.
Planned
Part 4
Type
Too big on large screens today, and far harder to manage than it needs to be.
Type that jumps at breakpoints can only be right for one screen size at a time
Type was made bigger for people viewing on large screens. Now others find it too big.
The same text on a 2560px screen, today and as proposed
Page title 68px
Body text 24px
Page title 54px
Body text 18px
Each size grows smoothly to 1440, then more gently to 2560
Two layers, and the same name everywhere
12 sizes
A font size that grows with the screen. Roles are built from these, so you choose a role, not a size.
16 roles
The text styles you choose: a typeface, a size and a line height. Weight, italic and link are options on a role.
One name
| Where | Name |
|---|---|
| Figma | Heading XL |
| Code | easel-heading-xl |
| A brief | ‘Use heading-xl’ |
Every new name starts with easel-, so we can see what uses the new system, and count it.
A first draft: the larger headings
Role390 · 1440 · 2560
heading-xxl48 · 56 · 60Post-War and Contemporary Art
heading-xl36 · 48 · 54Post-War and Contemporary Art
heading-l28 · 32 · 34Upcoming auctions
heading-m-sans24 · 28 · 3020th/21st Century Evening Sale
heading-m-serif24 · 28 · 3020th/21st Century Evening Sale
Every role, name and size is open to change, and now is the best time.
The smaller headings and body text
Role390 · 1440 · 2560
heading-s-sans18 · 18 · 20From free valuations to market advice
heading-s-serif20 · 24 · 26From free valuations to market advice
heading-xs-sans14 · 16 · 17From free valuations to market advice
heading-xs-serif16 · 20 · 22From free valuations to market advice
body16 · 16 · 18Our specialists are here to answer your questions
body-s14 · 14 · 16Our specialists are here to answer your questions
body-xs12 · 12 · 14Our specialists are here to answer your questions
Built from today’s text styles. Say which roles you use, which you never touch and what is missing.
Labels and actions
Role390 · 1440 · 2560
label14 · 14 · 1618 Sep – 2 Oct · Online
label-s12 · 12 · 1418 Sep – 2 Oct · Online
action-l16 · 16 · 18Browse auction
action14 · 14 · 16Browse auction
This draft drops Action M, which matched Action in size, line height and capitals. More roles may merge the same way.
The whole system has three line heights: 100%, 120% and 140%
100% for a single line, 120% for headings, labels and actions, and 140% for body text. Today there are 62.
Each is a share of the font size, so it follows the type at any width.
In code, one class replaces four
Today
<h1
class="text-4xl md:text-4xl-lg
2xl:text-4xl-xl 3xl:text-4xl-2xl"
></h1>One class for each breakpoint, and a jump at each one.
Proposed
<h1 class="easel-heading-xl"></h1>--easel-text-4xl: calc(
2.25rem + clamp(0rem, (100vw - 390px) * 0.01143, 0.75rem) +
clamp(0rem, (100vw - 1440px) * 0.00536, 0.375rem)
);Read the variable as: start at 36px, grow by up to 12px between 390 and 1440, then by up to 6px more by 2560. Plain CSS, with nothing new to install.
Keeping each largest size within 2.5 times its smallest keeps browser zoom working
Accessibility guidelines (WCAG) ask that text can be zoomed to 200%. The vw part of a clamp() doesn’t grow when someone zooms, so a scale that stretches too far can stop zoom from enlarging the text.
Within 2.5 times, 200% zoom always works. Every line in the fitted scale stays at twice its smallest size or less.
Part 5
Spacing and the page gutter
One scale where every step looks different, and spacing that grows with the type.
Figma’s spacing scale has 16 steps. The proposal has 15, each visibly different
- 4
- 8
- 12
- 16
- 20
- 24
- 28
- 32
- 36
- 40
- 48
- 56
- 60
- 64
- 72
- 80
- 4
- 8
- 12
- 16
- 20
- 24
- 32
- 40
- 48
- 64
- 80
- 96
- 128
- 192
- 256
Dark bars are on one scale only. Above 48, today’s steps are too close to tell apart: 56 to 60 is a 7% change. Each proposed step is a clear jump from the one before.
The steps we drop are little used, and each has a near neighbour
| Step today | Uses in live code | Nearest proposed step |
|---|---|---|
| 28 | 38 | 24 or 32 |
| 36 | 21 | 32 or 40 |
| 56 | 20 | 48 or 64 |
| 60 | 16 | 64 |
| 72 | Fewer than five | 64 |
Together they are under 5% of the 2,131 spacing classes in live code. Moving one to its neighbour changes it by 4 to 8px, and that is fine.
A scale gives us the values. It doesn’t say which one to use
Imagine two of us each design a large section
One of us spaces it at 80 and the other at 96. Both values are on the scale, so neither is wrong, and a review cannot say which to keep. The site ends up with both.
A pattern makes that decision once
Say we agree that large sections use 80, and we write down the reason. Now we both use 80, and choosing 96 needs a good reason.
The numbers are only an example. When we make the same decision again and again, we make it once and call it a pattern.
Fixed spacing jumps at a breakpoint. A fluid step grows with the type
| Screen width | 390 | 768 | 1024 | 1280 | 1440 | 2560 |
|---|---|---|---|---|---|---|
Today, p-4 md:p-6 |
16 | 16 | 24 | 24 | 24 | 24 |
| A fluid step, 16 → 24 | 16 | 18.9 | 20.8 | 22.8 | 24 | 27 |
Type will grow smoothly between breakpoints. If the space around it only jumps, the two drift out of proportion between them. With a fluid step we set one value and get movement at every width, with no changes to design at each breakpoint.
So fluid is the default, and a fixed step is the exception. 83% of spacing on the live site is fixed at every width today, so this is a change of direction.
Gentle steps by default, and strong steps where a design asks for more
Gentle: one step up
12 → 16, 16 → 20, 20 → 24, 24 → 32, 32 → 40, 40 → 48 and 48 → 64.
A little movement for the spacing that is fixed today. 4 and 8 stay fixed.
Strong: where live code grows today
| 390 → 1440 | Uses | What it is |
|---|---|---|
| 16 → 24 | 17 | Cards and gaps |
| 24 → 48 | 15 | Section padding |
| 40 → 64 | 16 | Section spacing, with four endpoints today |
| 80 → 128 | 4 | Large sections |
Every step then grows by another eighth from 1440 to 2560, fixed ones included, because body text grows from 16 to 18. So 16 → 24 reaches 27, and a fixed 16 reaches 18.
The question of naming
| px | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 64 | 80 | 96 | 128 | 192 | 256 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Number | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24 | 32 | 48 | 64 |
| T-shirt | 2xs | xs | sm | md | lg | xl | 2xl | 3xl | 4xl | 5xl | 6xl | 7xl | 8xl | 9xl | 10xl |
Numbers
Tailwind’s unit, where 1 is 4px. Engineering’s classes use it today: p-4 is 16px. A new size is a new number, and nothing else changes.
T-shirt sizes
With md anchored to 16, the names run from 2xs to 10xl. You cannot read the px value from a name, so each one has to be learnt. In our type scale, 16 is sm.
Five of seven other systems number their spacing
| System | How a step is named | 16px, or the base step |
|---|---|---|
| Tailwind | A multiple of 4px | 4 |
| Radix Themes | Steps 1 to 9 | --space-4 |
| IBM Carbon | Steps 01 to 13 | $spacing-05 |
| Atlassian | A percentage of an 8px base | space.200 |
| GitHub Primer | The px value | --base-size-16 |
| Utopia | T-shirt sizes, with s as the base |
s |
| Nord Health | T-shirt sizes, with m as the base |
m |
A fluid step is two steps of the scale
| 390 → 1440 | Kind | T-shirt pair | Number pair |
|---|---|---|---|
| 12 → 16 | Gentle | sm-md |
3-4 |
| 16 → 20 | Gentle | md-lg |
4-5 |
| 16 → 24 | Strong | md-xl |
4-6 |
| 24 → 32 | Gentle | xl-2xl |
6-8 |
| 24 → 48 | Strong | xl-4xl |
6-12 |
| 40 → 64 | Strong | 3xl-5xl |
10-16 |
One size at 390 and one at 1440, and the browser works out every width between. A single name for each fluid step, such as md, stops working once a size has both a gentle and a strong version.
To discuss: numbers, t-shirt sizes, or both?
What happens when we need a size between two steps
- With numbers, it is a new number: 28 is
7. - With t-shirt sizes, we rename every step above it, add an in-between name such as
lg-plus, or keep the scale closed and use the nearest step.
What happens when a designer needs another fluid step
- With pairs, any two steps make one: 24 → 48 is
6-12orxl-4xl. - With named fluid steps, the set is short and closed, and a new one has to be agreed.
In code, a fluid step is one variable
Today
<div class="p-4 md:p-6"></div>Two classes, and a jump at 1024.
Proposed
<div class="p-md"></div>--easel-space-md: clamp(1rem, 0.8143rem + 0.7619vw, 1.5rem);1rem: never smaller than 16px0.8143rem + 0.7619vw: grows with the screen, from 16 at 390 to 24 at 14401.5rem: never larger than 24px
p-md stands in for whichever name we choose. A tool writes the clamp() from the two sizes and the two widths.
In Figma, a fluid step is one variable with a mode for each width
| Spacing | 390 | 1440 | 2560 |
|---|---|---|---|
| space-md | 16 | 24 | 24 |
| space-lg | 24 | 32 | 32 |
| page-gutter | 16 | 48 | 54 |
Apply the variable, then switch a frame’s mode to see any width. Still in test: whether steps keep growing above 1440 by the same share as body text, so 24 becomes 27 at 2560.
The page gutter is four values to remember, in Figma and in code
| Screen width | Gutter | Class |
|---|---|---|
| Under 390px | 8px | px-2 |
| 390–1023px | 16px | sm:px-4 |
| 1024–1279px | 40px | md:px-10 |
| 1280px and up | 48px | lg:px-12 |
<header class="px-2 sm:px-4 md:px-10 lg:px-12"></header>These are the global header’s values, in production today. Newer React pages such as Calendar don’t match them, and Figma isn’t consistent either, so design is as much to blame as engineering.
The gutter becomes one token: 16, 48, then growing with the text
| Screen width | Today | Proposed | To discuss |
|---|---|---|---|
| 390 | 16 | 16 | 16 |
| 1024 | 40 | 35.3 | 35.3 |
| 1440 | 48 | 48 | 48 |
| 1920 | 48 | 50.6 | 68.6 |
| 2560 | 48 | 54 | 96 |
--easel-page-gutter: clamp(1rem, min(0.2571rem + 3.0476vw, 2.5179rem + 0.5357vw), 3.375rem);<header class="px-gutter"></header>It matches today at 390 and 1440, the two widths we draw. Above 1440 it grows by the same share as the text and the other spacing, 12.5% by 2560, so one rule covers everything. Growing to 96 would frame the page more on a large monitor, at the cost of a second rule.
Above 1440, everything keeps growing, more gently
| At screen width | 390 | 1440 | 2560 |
|---|---|---|---|
| Page title | 36 | 48 | 54 |
| Body text | 16 | 16 | 18 |
| A fluid step, 16 → 24 | 16 | 24 | 27 |
| Page gutter | 16 | 48 | 54 |
Type grows at about half its earlier rate. Spacing and the gutter follow the type, by the same share as body text: 12.5% by 2560. Still in test: try ‘Space follows type’ in the prototype, with ‘Gutter follows type’ on.
From 1440 to 2560, every type size grows by 1 to 6px
| Size | 1440 | 2560 | Change |
|---|---|---|---|
| 5xl | 56 | 60 | +4 |
| 4xl | 48 | 54 | +6 |
| 3xl | 32 | 34 | +2 |
| 2xl sans | 28 | 30 | +2 |
| 2xl serif | 28 | 30 | +2 |
| xl sans | 18 | 20 | +2 |
| Size | 1440 | 2560 | Change |
|---|---|---|---|
| xl serif | 24 | 26 | +2 |
| lg sans | 16 | 17 | +1 |
| lg serif | 20 | 22 | +2 |
| sm | 16 | 18 | +2 |
| xs | 14 | 16 | +2 |
| 2xs | 12 | 14 | +2 |
Body text is sm. Drafted, with the 2560 sizes still to tune.
From 1440 to 2560, every space grows by an eighth, as body text does
| At 1440 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 64 | 80 | 96 | 128 | 192 | 256 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| At 2560 | 4.5 | 9 | 13.5 | 18 | 22.5 | 27 | 36 | 45 | 54 | 72 | 90 | 108 | 144 | 216 | 288 |
The page gutter follows the same rule, 48 to 54. A fluid step grows from its 1440 size, so 24 → 48 reaches 54.
In test. The 2560 values fall between steps, which is fine because 2560 is checked, not drawn.
Fluid steps grow to 1440, then every step grows by an eighth
Each token at the widths we check
| Token | 390 | 1024 | 1440 | 1920 | 2560 |
|---|---|---|---|---|---|
| Fixed 16 | 16 | 16 | 16 | 16.9 | 18 |
| Fixed 24 | 24 | 24 | 24 | 25.3 | 27 |
| 16 → 24 | 16 | 20.8 | 24 | 25.3 | 27 |
| 24 → 48 | 24 | 38.5 | 48 | 50.6 | 54 |
| 40 → 64 | 40 | 54.5 | 64 | 67.4 | 72 |
| 80 → 128 | 80 | 109 | 128 | 134.9 | 144 |
| Gutter | 16 | 35.3 | 48 | 50.6 | 54 |
Designers choose the 390 and 1440 values. Every other width is worked out, in code and in Figma’s modes.
Part 6
Widths and layout
Which frames we draw once type and spacing grow without us.
We design two widths and check the others
- 390
- 768If the layout changes
- 1024If the layout changes
- 1280If the layout changes
- 1440
- 1920If the layout changes
- 2560Check
Type and spacing no longer need a frame at every breakpoint. A layout still does: if the layout changes at a breakpoint, we design that breakpoint. If nothing changes there, there is nothing to design.
To agree with QA: they test a build against designs today, so drawing fewer frames needs their agreement.
Some layouts change at a breakpoint. Others follow the space they have
At a breakpoint
A switch between a layout made for a phone and one made for a desktop, such as navigation that becomes a menu. We choose the width, and we draw both sides of it.
By the space available
How many cards fit in a row. The browser can work that out from one rule, at every width, with no breakpoint.
A RAM grid lets the browser choose the number of columns
grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));- Lot 1
- Lot 2
- Lot 3
- Lot 4
- Lot 5
- Lot 6
- Lot 7
- Lot 8
ch sets a line length in characters, at any type size
.article p {
max-inline-size: 65ch;
}A ch is the width of a zero in the current font. A paragraph stays at a readable length whatever size its type becomes, so fluid type needs no matching fluid widths.
Text widths need a scale too, set in characters
Today
A text box in Figma is, say, 438px wide. A developer copies the number, and the reason for it is lost. Live code has 323 one-off widths and heights, in 189 different values. max-w-[330px] alone appears 13 times.
Proposed
A short scale of line lengths in characters: one for body text, at about 65, and narrower ones for headings. A width then follows from the content and its type size, at every screen width.
Still to work out: the values, and how many we need.
Above 2560, the page only gets wider
Type and spacing stop growing at 2560, so a bigger screen spreads the same layout further apart. Text sections stretch and leave a lot of empty space.
The prototype compares four ways to treat a wide screen
| Option | Above 2560 | What we found |
|---|---|---|
| Bleed | Today. Every section runs edge to edge | Text sections stretch |
| Page | The whole page stops at 2560, backgrounds included | Colour bands stop short, with white either side |
| Content | Backgrounds and full-width pictures reach the edges. Content stops | Keeps the full-bleed feel. Text sections are still wide |
| Two widths | As Content, and text sections stop at a narrower width | The one we prefer so far |
Two widths, or more: 2560 for pictures and grids, narrower for text
Stops at 2560
Grids, carousels and lists of auctions. A carousel starts on the left edge of the content and runs off the right edge of the screen. A hero video or a banner picture reaches the edge, and stops growing at the height it has at 2560.
Stops at a narrower width
Sections that are mostly text: a centred banner, three steps, a list of questions. In a half-and-half banner the picture reaches the edge, and the text starts where the text sections start.
The prototype uses 1680, the 1440 design at the larger type. It could as well be 1440, and some content may need a third width. That needs design experiments, not a number agreed today.
To decide: on screens wider than 2560, can pages jump between contained and edge to edge?
Why it happens
Pages take the new widths one at a time, as each is rebuilt. Until they all have, a visitor on a very wide screen sees content in a container on one page and edge to edge on the next. Below 2560 nothing changes.
Do we care?
Few visitors have a screen that wide, so we could accept it while pages move over. Or we hold back the containers until every page can take them. The header and footer are shared, so they can go either way on every page at once.
Part 7
Semantic colour
Tokens named for their job rather than their colour, so one component works in any section.
The decision for today: start now, or park it?
Today a colour variable names a colour. A semantic one names its job
Today
| Layer in Figma | Fill |
|---|---|
| Heading | black |
| Caption | black-300 |
<h2 class="text-black">…</h2>
<p class="text-black-300">…</p>Says what it looks like. Right in a light section, and wrong the moment the section turns dark.
Easel 3.0
| Layer in Figma | Fill |
|---|---|
| Heading | foreground-primary |
| Caption | foreground-secondary |
<h2 class="text-foreground-primary">…</h2>
<p class="text-foreground-secondary">…</p>Says what it’s for. The section decides what it looks like.
One card, three sections
Live auction, London
Post-War and Contemporary Art
- Figma mode
- Light
- Code
data-color-scheme="light"
Live auction, London
Post-War and Contemporary Art
- Figma mode
- Dark
- Code
data-color-scheme="dark"
Live auction, London
Post-War and Contemporary Art
- Figma mode
- Red
- Code
data-color-scheme="red"
In Figma, you select the section and switch its mode. In code, the section sets data-color-scheme. The card inside is the same component each time.
On red, 60% white measures 3.9:1, under the 4.5:1 body text needs, so the Red mode uses 80% (6.1:1).
In Figma, each scheme is a mode
| Colour | Light | Dark | Red |
|---|---|---|---|
| Semantic | |||
| background-primary | white | black | brand-c-red |
| foreground-primary | black | white | white |
| foreground-secondary | black-300 | white-alpha-60 | white-alpha-80 |
| border-primary | grey-400 | white-alpha-40 | white-alpha-40 |
| Component: button | |||
| button-primary-background | brand-c-red | white | white |
| button-primary-foreground | white | black | brand-c-red |
| button-primary-border | brand-c-red | white | white |
| button-secondary-background | transparent | transparent | transparent |
| button-secondary-foreground | black | white | white |
| button-secondary-border | black | white-alpha-60 | white-alpha-80 |
In code, each mode is one block of variables
[data-color-scheme="light"] {
--background-primary: var(--color-white);
--foreground-primary: var(--color-black);
--button-primary-background: var(--color-brand-c-red);
--button-primary-foreground: var(--color-white);
--button-primary-border: var(--color-brand-c-red);
}
[data-color-scheme="red"] {
--background-primary: var(--color-brand-c-red);
--foreground-primary: var(--color-white);
--button-primary-background: var(--color-white);
--button-primary-foreground: var(--color-brand-c-red);
--button-primary-border: var(--color-white);
}The classes stay the same
<section data-color-scheme="red" class="bg-background-primary">
<h2 class="text-foreground-primary">Post-War and Contemporary Art</h2>
<Button variant="primary">View lots</Button>
</section>// Inside Button, written once
<a class="bg-button-primary-background text-button-primary-foreground border-button-primary-border" />Designers choose a section’s scheme, not the colour of everything inside it
How four other design systems name their tokens
| System | How a name is built | Quieter text | A primary button’s background |
|---|---|---|---|
| Atlassian | Property, role, emphasis, state | color.text.subtle |
color.background.brand.bold |
| GitHub Primer | Property, role, emphasis | fgColor-muted |
button-primary-bgColor-rest |
| Nord Health | Property, then a word from weakest to strong | --n-color-text-weak |
--n-color-accent |
| Reshaped | Property, role, then faded or highlighted | --rs-color-foreground-neutral-faded |
--rs-color-background-primary |
All four put the property first: text, background or border. Only Primer names the button in the token itself.
What we would take from each
Atlassian
One order for every name: property, role, emphasis, then state. A hover colour is the same name with hovered on the end.
GitHub Primer
A third layer of component tokens, used only inside the component. Each strong background comes with a named foreground, fgColor-onEmphasis.
Nord Health
A small set: about 60 colour tokens. Light, dark and high contrast are each one short file that gives the same names new values.
Reshaped
Names that come in pairs: background-primary and on-background-primary. Its Tailwind classes drop the repeat, so the class is bg-primary.
To decide: all four use a word such as muted, subtle, weak or faded for quieter text. None uses ‘secondary’, which we also want for the secondary button.
Part 8
Everything else
Type, spacing and colour are the start. The same treatment applies to the rest.
Radius, shadows, motion and sizes need a system too
Radius
21 one-off values in code, in 11 different sizes.
Shadows
11 one-off shadows. Six are variants of one 24px blur.
Motion
16 one-off easing curves. 12 are the same curve, all in the header.
Widths and heights
323 one-off sizes, in 189 different values.
Aspect ratios
15 one-off ratios, in six values.
And more
Possible scales too: background blur, letter spacing, font weights, border widths, opacity, durations, layers and icon sizes.
A one-off is a value typed by hand in place of a step from a scale. Counted in engineering’s code on 12 October 2026. Nothing is proposed for these yet.
Part 9
The path
How this reaches Figma and code without a big change that slows every squad down.
Eight steps, none of them a big bang
- Agree the numbers. Settle the 2560 sizes. Check the three widths against visitor data.
- Agree the names. Design and engineering sign off one set of names.
- Add new beside old. New variables in Figma, new classes in code. Nothing changes on screen.
- Pilot one page. Build it with the new roles. Compare before and after on big screens.
- Move shared packages. Start with the one Typography lives in. New work uses the roles.
- Move each area. One package at a time, at each team’s pace.
- Flag the old. Mark the old sizes as deprecated, with a warning on new uses.
- Remove the old. Once no package uses them.
This is the rollout for type. Each step ships on its own, and the round repeats for spacing, colour and the rest.
Progress is a count, not an opinion
| Measure | Today | Goal |
|---|---|---|
| Sizes in the Tailwind config | 62 | 12 |
| Line heights in the Tailwind config | 62 | 3 |
| Typography variants that differ from Figma | 4 of 22 | 0 |
| Files that use the old size classes directly | 43 | 0 |
| One-off font sizes | 13 | 0 |
Font sizes forced with !important |
10 | 0 |
Counted on engineering’s main branch on 30 September 2026. Because every new name starts with easel-, we can count how much of the site has moved.
What we decide today
| Decision | Starting position | Today? |
|---|---|---|
| Where we start | Type first, then spacing and the gutter. Colour: start now or park it | Yes |
| Which widths we draw at | 390 and 1440, with 2560 checked, plus any breakpoint where the layout changes | Yes |
| The gutter | 16 and 48, then growing with the text to 54. Or growing faster, to 96 | Yes, or after a week with the prototype |
| Screens wider than 2560 | 2560 for pictures and grids, narrower for text | A first view |
| Above 2560, pages that differ | Accept it for now, or wait | Yes |
| Spacing names | Numbers or t-shirt sizes | A first view |
| The type and spacing numbers | The 12 sizes as drafted, and the 15-step scale | An owner and a date |
| How we agree and record a token or a pattern | Who proposes one, who decides, and where it is written down | An owner and a date |
From an agreed date, new design work uses the agreed tokens
So the four of us are consistent before we ask engineering to be.
Two things to name today, not solve
Ways of working across squads
There is no shared process today, and nobody maintains the system. We can name the gap and agree to return to it.
Why QA approves work that is still wrong
If seven frames do not prevent that, fewer frames with clearer rules may do better. What do you see in your squads?