Design System

The values the site is built from, rendered from the same stylesheet the site uses. Nothing here is a mockup, so nothing here can be out of date.

How To Read This

Two pages. This one holds the values, the other holds what is built from them, because one page carrying both is a scroll, not a reference. Each section shows its tokens as they render, with the one rule that governs them. Read the first column of a row as the name to type, in the form it is declared in. Reasons sit beside each rule in the stylesheet itself.

Type Scale

Ten sizes. A token is named for its desktop size, and everything from 24px up scales down to a mobile floor between 360px and 1280px, so headings shrink smoothly. Labels and body copy stay fixed because they already sit at their floor. The count is how many rules use each token, and the ratio is the step up from the size above it.

12px is the floor, and there is no token below it. Every step is 1.12x or wider except the first: 12px is chrome (the header bar and the footer) and 13px is a label inside content, a spatial step, not a typographic one.

--text-1212px · 12 rules
All of the chrome: the header bar and the footer
--text-1313px · 37 rules · 1.08x
Every eyebrow, every label inside content, the globe's caption, the map's place names, its key and its attribution, and the footer's copyright line
--text-1616px · 41 rules · 1.23x
Body, prose paragraphs, captions, table cells, callouts, the footer's links
--text-1818px · 19 rules · 1.12x
Card title, copy text, story text, overlay links, excerpt
--text-2424px · 15 rules · 1.33x
Section heading and title, the homepage subtext and the detail tagline, which are the same thing on two pages, tile value, impact value, prose h3, blockquote, step index and title
--text-3232px · 3 rules · 1.33x
Prose h2, CTA title, connect email
--text-4040px · 1 rule · 1.25x
Story title, and nothing else
--text-4848px · 2 rules · 1.20x
Post title, brand specimen
--text-5656px · 4 rules · 1.17x
Page and project title
--text-7272px · 1 rule · 1.29x
The homepage headline, and nothing else

Uppercase Labels

Two families, and the family is the tracking. Size has nothing to do with it. A section label a reader scans to find their place gets 0.14em and is always 13px. Every other uppercase label gets 0.08em, at 13px inside content and 12px across the chrome. Three rules sit outside both, listed below, because a family of two only means anything if the exceptions are named.

0.14em · always 13pxEyebrow, Card Industry, Story Group, Brand Typename
0.08em · 13px, in contentTile Label, Detail Meta, Pager Label, Table Head
0.08em · 12px, chromeWordmark, Meta Strip Label, Dropdown Label, Footer Label
None · 16pxSwatch Hex
0.04em · 12px
0.02em · 24pxBrand Specimen

The three exceptions, each with its reason written beside it in the stylesheet. The swatch hex renders a project's own brand color as a number in tabular figures, and tracking would undo the alignment that buys. The wordmark's initials drop to 0.04em, because at the wordmark's own 0.08em the hyphen in S A-P opens into a gap and the pair stops reading as one. A brand specimen sets its characters at 0.02em, which is type being shown, not type doing a job.

Tracking is the one family on this page with nothing to type. The four tracked values above are literals wherever they are set, so a new label copies a number out of a neighboring rule rather than naming a family, and a third family could arrive without anyone deciding on one.

The crumb and the project preview label are not in the chrome row above. Both are destinations a reader clicks rather than labels naming something else, so neither takes tracking or uppercase.

Some of the rows above are chrome, shown here on paper because that is where this page is. Their contrast in this row differs from production, where they sit on the dark bar.

Color

Two tiers. The scale below holds sixteen neutral steps and says nothing about what any of them is for. The roles under it say what a color is for and point at a step. Nothing else in the stylesheet names a step directly, and that rule is what makes the second tier worth having.

A step is named for its oklch lightness times a thousand, so 650 is 0.650 and the distance between two steps is the difference between their names. The site is neutral by design; a project's own brand color arrives per entry from its content and never enters the scale.

The Neutral Scale
--neutral-000Ink on paper, and only when the page is printed
--neutral-090Below the page's own darkest ink, and only ever a device bezel
--neutral-150The text on a call to action, and nothing else
--neutral-220The dark page, and the primary text on paper
--neutral-260Chrome, and a panel standing on the dark ground
--neutral-310The default line on the dark ground, the overlay link fill, and text on hover on paper
--neutral-340The overlay link fill on hover
--neutral-470Secondary text on paper, and an outline on hover on the dark ground
--neutral-650Secondary text on the dark ground, and an outline on hover on paper
--neutral-860The line on paper
--neutral-900Text on hover on the dark ground
--neutral-920The call to action fill
--neutral-940Raised fills on paper, the band a diagram panel stands in
--neutral-955The panel a diagram sits in
--neutral-1000The sheet a page is printed on, and only then
--neutral-970The paper ground, primary text on the dark ground, and the pill on hover
Ground-Relative Roles

Eight roles that .paper rebinds. Every component that reads them follows onto the light ground without being told, and that is the whole mechanism. Both grounds have exactly one muted text level. There was a dimmer step below it; it was measured at 1.46:1 where it was used, which fails WCAG AA, and lifting it far enough to pass would have put it a hundredth away from muted. So there is no third level, and that is the finding rather than the compromise.

--groundThe surface a component stands on. Dark 220, paper 970.
--raisedA panel standing on the ground, one step above it. Dark 260, paper 940.
--frame-bgThe panel a diagram sits in: a framed story row, an annotated plate, a numbered sequence. Paper 955, and no value on the dark ground, because all three live in a case study body and a body is always paper.
--lineEvery border a component draws. Dark 310, paper 900.
--on-groundPrimary text and the focus ring. Dark 970, paper 220.
--on-ground-mutedSecondary text. Dark 650, paper 470.
--on-ground-hoverWhere muted text goes when you point at it, a step short of the top. Dark 900, paper 310.
--line-hoverWhere a resting hairline goes on hover, doubled rather than run to white. Dark 470, paper 650.
Fixed Roles

These do not rebind, and that is the point. Chrome is the frame around the page, not something standing on it. The call to action is the one thing a reader should recognize in the same color wherever it appears, so a filled pill is the same pill on both grounds.

--chromeChrome that stands on the page: the menu overlay, the entry dropdown, the footer, the floating scroll-up, and a bar control under the pointer. Always 260. Every one of those has an edge of its own, which is the condition for using it.
--chipThe overlay link fill. Always 310.
--chip-hoverThat fill on hover. Always 340.
--seamThe header bar's own ground. Always 220, which is the page's value, so the bar seams into the page instead of sitting a step above it. It stayed a fixed token rather than becoming --ground because a paper band must never be able to turn the bar white.
--ctaThe filled pill. Always 920.
--cta-hoverThat pill on hover. Always 970.
--on-ctaText on the pill. Always 150.
--bezel-inkThe ink of a phone bezel in a step sequence. Always 090, below the dark page's own darkest ink, because a device is the same object on either ground.

The two grounds do not mirror each other. Secondary text is 0.65 on a 0.22 ground, a lightness gap of 0.43. On paper it is 0.47 on 0.985, a gap of 0.515. The paper version is the more legible of the two, so muted text looks quieter on the dark page than it does in a case study.

Spacing and Rhythm

One base unit, 8px. Anything that moves the page down is a multiple of it. Line heights are snapped to the same grid, so a paragraph in one column lands level with an image in the next.

--space-18px
--space-216px
--space-324px
--space-432px
--space-648px
--space-864px
--space-1296px
--space-5--space-7
40px and 56px, declared and used, and left off the bars above only because the ramp reads better without them. Nine steps exist, not the seven drawn here.
--lh-body32px
Body copy is 18 on 32, exactly four units.
--lh-title48px
A story title is 40 on 48, six units.
--lh-caption24px
A caption is 16 on 24, three units.
--measure30em, about 67 characters at any size
One reading column for the whole case study, so the eye returns to the same line start all the way down. Every block of prose takes it: copy text, a section's body, a step description, a figure note. About 67 characters at every size, which is the reason it is in em and not in pixels. It was 620px, one number for all three reading sizes, and a pixel cap cannot hold a line length: measured, it ran 58 characters at 24px, 78 at 18 and 87 at 16, so the smallest type on the site had the longest lines. The mean glyph in this face is close to 0.445em, so 30em is about 67 characters wherever it is used. Display type and code are not reading text and keep a pixel cap. It binds only on a wide screen, because below about 1400 the column it sits in is already narrower than the cap; above that the page has no maximum and a half of 2560 is 134 characters a line. Headings are out, because text-wrap: balance does that job for display type by another route.
--measure-max40em, about 90 characters
The ceiling, for the blocks that square up to their column instead of holding the measure: the summary's two columns and a process step. Those sit where the column is the page's right edge with no image beside it to draw it, so a line stopping short of the column reads as the layout stopping short. They fill it, and they stop at 40em, which this stylesheet already calls the point where prose stops being readable. The ceiling is the answer to the sentence above: the page has no maximum, so a column is 36em at 1440, 42.7em at 1680 and 67em on a 2560 monitor, and a block told only to fill will take all of it. Below about 1600 it never binds, so the squaring-up happens on every screen anyone reads this on. The case study body is deliberately not in the list; it is the only sustained read on the page and keeps --measure.
--cover-ratio16:9
Every surface that shows a project cover holds this one ratio. Cover art is authored at 2048x1152 to match.

Breakpoints

Seven widths, and not one of them is a device. Each is the measurement where something stops working: a column narrower than the label above it, a grid about to strand one item, a tile under the size its artwork needs. The width is written where that crossing was measured, rounded away from it so the next change to the type scale does not land back on the edge.

They are not a scale and they do not nest. A rule takes the one width its own problem happens at, which is why two of these carry a single block and one carries ten. Two more exist as the opening half of a band, min-width: 601px and min-width: 769px, and are counted with the width they close against.

min-width: 1280px1 block
The career row takes a third column, which is the only thing on the site that waits this long for room.
max-width: 1100px1 block
Two four-up figure grids drop straight to two columns. Auto-fit cannot skip a count, so left alone they pass through three and leave the fourth on a row of its own. Measured: at 1024 the key gets 864px and four 13rem tracks need 928.
max-width: 1024px2 blocks
Three-up card grids and the header dropdown go to two. Also the top of the band where a set of exactly three holds three across instead.
max-width: 900px2 blocks
A step row and a run sequence stop sitting side by side. The step title is capped at 260px and the description is the remainder, so they cross at about 864: below it the paragraph is narrower than the one or two words heading it.
max-width: 768px10 blocks
The large one. Page gutters drop to 24, the bar compacts to initials, two-column layouts become one, and a row of tiles pins to two whatever its desktop count says.
max-width: 600px5 blocks
The bottom of the three-across band. At 601, the last width that holds it, a set of three is 176px an item; below 600 it would go under the 160px floor flat art needs, so sets of three and five stack instead.
max-width: 480px2 blocks
The last of the grids give up: three-up cards, the bento and the menu overlay go to one column.

A grid of equal peers never leaves exactly one item on the last row.

Three go three across or one down, never two and a widow. A row of five takes three, so the last row holds two together rather than one alone. An index that wraps is exempt: its count is set by a filter, and no fixed rule could be right for every one.

Nothing here reads prefers-color-scheme. The body is a fixed light band inside dark chrome and there is no second theme to switch to, so a width is the only thing any of these rules asks about.

Corner Radius

Four values and a rule for which is which. Anything that does not fit one of the four is a question about the component, not a reason for a fifth value.

--radius-sm3px
Text level, inside a line of copy. Inline code, and nothing else today.
--radius-md6px
One thing: a control, or a single piece of media. Buttons, overlay links, swatch chips, card and mini card media, gallery media, story images and frames, attributions, callouts, the menu toggle.
--radius-lg10px
A panel that holds other components. The detail hero, tiles, the impact band, the process list, the portrait, blocks, the pager, the brand mark, a framed story row.
--radius-pill999px
Shape is the affordance. Filter chips, tool chips, pill buttons, the attribution icon, the byline dot, the scroll to top button.

Icons

One box and one weight. Every icon is a 24 unit viewBox drawn at 16px, which puts a stroke of 2 on screen at 1.33px: heavier than the 1px keyline beside it, lighter than the 700 weight label beside it. Nothing here is an icon font and nothing is a raster.

Box16 × 16
Drawn in a 24 unit viewBox so a glyph borrowed from any 24px set drops in without redrawing.
Stroke2, set in CSS
Set once on the shared rule, never as an attribute on the element. An attribute is a value per icon, and a value per icon is twenty-four chances to pick a different one.
ColorcurrentColor
An icon never names a color. It takes the color of the control it sits in, so a hover or a change of ground moves the label and the icon together.

The direction an arrow points is the thing it does, so the set is small and each one means one thing.

Backarrow left
Up one level, to the index a page belongs to.
Forwardarrow right
On to the index, or to the next page in a pair.
To toparrow up
Back to the start of this page, and nothing else.
Externalarrow out
Leaves the site. Always paired with a new tab.
Disclosechevron down
Opens a list or a panel in place. It rotates; there is no up chevron.

Every arrow is drawn, and none is a character. A text arrow sets in the weight of the type around it, so it can never match a drawn icon beside it, and on a site carrying this many icons the two always end up side by side. No exception for an arrow inside a sentence.

Diagrams

Six drawn figures on this site: an annotated screenshot, a numbered sequence, a flow of boxes, a plain panel, the Flield sitemap and the HomeBase operations map. They are markup and not exported plates, because a 22px label baked into a 2048px image arrives at about 4px on a phone, and a diagram is all label. Six of anything drifts, so they share one set of parts.

Panelthe figure itself
Filled with --frame-bg, --radius-lg, --space-4 of padding, and no border. A diagram owns its panel, so its section never carries {framed}: two panels a few grays apart is one container too many.
Cella node inside it
Filled with --ground, a 1px --line keyline and --radius-md. Never --raised, which is the trap below.
Core cellthe one everything points at
Filled with --on-ground-muted and its text inverted to --ground, keylined in its own fill. 6.5:1, so every line on it clears the floor. One per figure or it stops meaning anything.
Type13, 16 and 18
A title is 18 at 700. A band label is 13 at 700 and 0.14em, the same label .story__group sets. A micro label is 13. A note or a line of body is 16 at --lh-caption, muted. Nothing else.
Numbersmarker or ordinal
A number that points at something drawn elsewhere is a marker and takes a filled pill, because a key has to look like what it keys. A number that only counts is an ordinal and takes a plain numeral. Pick by which one it is, not by which figure it is in.

Depth is a fill, one even step lighter at every nesting. A keyline is definition; the fill is what carries the level.

  • A case study section, --neutral-940
  • A diagram panel standing on it, --frame-bg at 955
  • A cell inside the panel, --ground at 970

Fifteen steps between each, which is close enough to read as one level and far enough to read at all. Three surfaces inside the top 3% of the scale leave a panel with no presence against the page it stands on.

--raised is the trap in that ladder. The name reads as a surface that comes forward, and on the dark ground it is one. Inside .paper it resolves to --neutral-940, the section ground itself and a step darker than the panel holding it, so a cell painted with it recedes. A chip inside a cell may take it on purpose, since a chip sits back into its card where a card lifts off its panel.

All six hold to one short list, and a build that breaks it fails instead of shipping. Three type sizes, two weights, one keyline at 1px --line, and no cell painted --raised.

Motion

Five durations, each a token, so a new rule picks a name and not a number. A tint, a thing moving, a panel opening, a section arriving. Every name says what moves and leaves the milliseconds to the token. Under a reduced-motion preference all four collapse to effectively instant.

Two easings carry them. --ease is for a control answering a pointer, where the movement is short and a curve would read as lag. --ease-out is for a thing arriving and settling, which spends most of its distance early and lands slowly.

Two more tokens hold a gap, not a duration. One is a time, pacing one thing against the next inside a single arrival: --step-reveal at 60ms for the parts of a revealing block, and for the headline, the blurb and the actions on the homepage. The other is not a time at all. --settle-span answers scroll position, so it is a share of a card's travel into view, and a millisecond would mean nothing there.

--dur-tint150ms · 37 uses
A color or a border arriving. Buttons, chips, tiles, the overlay links, the bar's four controls, the wordmark, the footer links, the FAQ chevron, the pager label, the image viewer's controls.
--dur-move180ms · 16 uses
Something changing position. The card and mini card lift, the attribution lift, the card arrow, both chevrons, the menu toggle bars, a zoomable image under a pointer and the shell that scales around it.
--dur-panel220ms · 25 uses
Something opening or resolving. The menu overlay, the entry dropdown, the FAQ answer, the bar's height, the wordmark's box between its two widths, the project preview label between its two, the image viewer arriving.
--dur-reveal420ms · 18 uses
A block arriving, and the only duration here that answers the page instead of a pointer. Long because it runs once and nothing is waiting on it. A section reached by scrolling fades up; a cover already on screen at load moves up instead; the about page's map fades in over the panel it is built on. Nothing fades in on arrival, because a fade over something a reader can already see reads as a flash, and a cover is the content rather than its decoration.
--dur-draw1100ms · 1 use
A cover's strokes drawing on, once, as its card arrives. Longer than a reveal because a line is read along its length, not seen at once

A sixth kind of movement takes no duration at all, because it answers scroll position and not a clock.

  • The bar turning solid
  • The hero text drifting against the covers rising through it
  • The covers settling into place, every one below the fold
  • The rule beside a section number drawing itself across

Each follows a finger exactly and reverses on the way back up. A browser with no scroll timeline gets the finished state and nothing else.

Covers arrive level. Same distance, same curve, same moment, so a row rises into its place as a row. --settle-span at 60% is the share of a card's travel into view that the settle uses, which puts the movement in the part of the travel a reader is watching and leaves the last 40% at rest.

The homepage's top row is the exception, and the reason is arithmetic rather than taste. A scroll-driven settle has to start somewhere other than its end, so a card already on screen at load would be offset at rest: measured, 11.8px and 24px low, showing 38% and 35.3% of themselves against the 40.6% the layout gives them. Scrolling carries that row off the screen rather than into it, so the offset would be the only state of it most readers saw. The top row runs the same distance and the same curve on a timer instead, once, finished in 250ms of its 420.

Which covers those are is a question about the fold and not about the markup. Two of them from 769 up, where the grid is two across; one below that, where the second cover measures 0% visible at load and settles against scroll like every other cover on the site.

Distance is --settle-from, 8% of a card's own height rather than a pixel count, which is what lets one value serve both grids at every width: 36.6px on the homepage at 1440, 27.2px on the three-up index, 24.1px on a phone, and no media query anywhere.

A fixed distance would need a breakpoint, and no breakpoint fits, because these covers do not get smaller as the screen does. At 768 the homepage goes one across and its cover becomes the largest on the site at 720 by 491, which is the same width where the work index drops to a 196px tile.

Two earlier versions sequenced the row, first with different travel per column and then with equal travel and offset starts. Both drew a diagonal, and a diagonal claims the covers have an order. They do not. The second version was the better built of the two and still left one cover 11px below its neighbor for a whole scroll, which reads as a grid out of alignment rather than as depth.

Scroll-driven rules write translate or scale, never transform itself. That is a rule, not a preference.

Such an animation is filled in both directions, so it holds the property it writes for the life of the page, and an animation outranks a normal declaration in the cascade. Written to transform, one of these closes the property to every hover on the same element. Independent transform properties compose, so the scroll keeps one and the states keep the other.

Typeface

Manrope --font-sans
Fractional creative direction
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 & . , : ; ! ? ( ) - ’ "
Regular 400 Medium 500 Semibold 600 Bold 700 Extrabold 800