Design System

Design System

The rules this site is built on, rendered by the site itself. Every specimen below is a real element under a real rule, and every value beside it is read out of the stylesheet when the site is built.

Each component is taken apart into its parts, its states and its specification. The last section lists what is not clean.

Colour

Four colours, one value each. Red carries emphasis, links and the signature fields. White is the light ground, off-white rather than stark white so pages read as editorial. Black is the dark ground, the logo, the nav and the icons. Text black is the copy on the light ground, softer than the black for reading. There is no accent beyond these, no gradient, and no second shade of any of them.

The stylesheet does not yet hold to this. 8 values are in use where there should be 4, and the audit at the end of this page lists every one.

  • Red#CD2C2CEmphasis, links, and the signature fields.
  • Black#080808Dark fields, the logo, nav and icons. The footer's black.
  • White#f5f5f7The light field, and text on red or black.
  • Text black#2c2c38Copy on the light field: softer than the black, for reading.

How colour is applied

There is no global scheme. A section declares three custom properties, and everything inside and around it takes its colour from them.

  • --bg-color The field the section is painted on.
  • --text-color Copy, headings and every icon fill.
  • --link-color Links inside the section’s own content.

theme/script.js reads those three off whichever section is crossing the middle of the viewport and applies them to the page, the header, the nav and the footer. The section owns the colour and the page borrows it, so the chrome recolours as the reader scrolls.

That is why the chrome is written in currentColor and inherit. The focus ring, the icons and the section menu’s marker have to work on red and on off-white without a second set of rules for each. The change runs over 0.5s.

The site’s moods

  • The red field. The home hero and the work index open on it.#CD2C2C · #f5f5f7 · #f5f5f54.83:1 text · 4.82:1 link
  • The default page. Every long-form read sits on it: About, Blog, Books, Career.#f5f5f7 · #1b1b27 · #CD2C2C15.65:1 text · 4.83:1 link
  • The darkest field. The footer, and the screens that need to go quiet.#0f0f0f · #f5f5f7 · #CD2C2C17.6:1 text · 3.64:1 link
  • The softer dark. Near-black rather than black, for a warmer field.#1d1d1f · #f5f5f7 · #CD2C2C15.46:1 text · 3.2:1 link
  • Red again, with off-white text. Used where the hero’s link colour is not wanted.#CD2C2C · #f5f5f5 · #CD2C2C4.82:1 text · 1:1 link

A client’s colours are not this system

A case study is painted in its client’s own brand for as long as it is on screen, so the work sits in the client’s world. Those colours belong to the client. They are content the work pages carry, 135 of them, and they are no part of this brand.

The system owns the mechanism. A new case study declares the same three properties rather than introducing styling of its own.

Typography

One family. The stack starts at ui-sans-serif and falls through the platform’s own faces, so the page renders as San Francisco on a Mac with no font file to load and no third party to contact.

Pull quotes alone are set in the system serif, ui-serif, 'Iowan Old Style', Charter, Georgia, serif: New York in Safari and Iowan Old Style in other browsers on a Mac, with no font file either.

Eight roles, and no other content size. Each role is a class on the element, so it looks the same on every page. Desktop is the base; a phone takes body copy up to 25px and the display roles down to fit.

Weights

  • 300 Titles and headings.
  • 400 Copy: body and statements.
  • 500 Everything that is not copy: meta, links in copy, the nav on desktop, the back link, the section menu, the previous and next row, buttons, the copyright line.
  • 200 The hero’s subline, the one lighter line on the site.

The roles

Grid, rhythm and restraint

.type-title. Large title. Page titles and the title of every full-screen section.

PropertyDesktop(max-width: 55.99rem)
font-sizevar(--type-title-size)as desktop
line-height0.963as desktop
font-weight300as desktop
letter-spacing-0.03emas desktop

Human Intelligence

.type-title.type-hero. Hero. The home hero only: the large title, sized so the longest headline holds one line.

PropertyDesktop(max-width: 55.99rem)
font-sizecalc((100vw - 14rem) / 7.5)72px
line-height172px
font-weight300as desktop
letter-spacing-0.03emas desktop

Learn the fundamentals

.type-heading. Content heading. A heading inside the copy, a card title, a group heading.

PropertyDesktop(max-width: 55.99rem)
font-size42px30px
line-height0.963as desktop
font-weight300as desktop
letter-spacing-0.02emas desktop

Built for change. Grounded in creative purpose.

.type-statement. Editorial statement. The copy under a title. Set as the footer copy is.

PropertyDesktop(max-width: 55.99rem)
font-size28px25px
line-height40px35px
font-weight400as desktop
letter-spacing-0.01emas desktop

Body copy at its resting size, with the leading and colour it takes from the field around it.

.type-body. Body copy. Running text.

PropertyDesktop(max-width: 55.99rem)
font-size28px25px
line-height40px35px
font-weight400as desktop
letter-spacing-0.01emas desktop

6 September 2026 · Practice · 7 of 15

.type-label. Number or label. Meta: dates, authors, categories, numbers, captions. The one role set under body, at the nav’s size.

PropertyDesktop(max-width: 55.99rem)
font-size20pxas desktop
line-height28pxas desktop
font-weight500as desktop
letter-spacing-0.015emas desktop

You’ve got to show up.

.type-quote. Large quotation. A pull quote, and its attribution at the same size, muted.

PropertyDesktop(max-width: 55.99rem)
font-sizeclamp(40px, 4vw, 56px)as desktop
line-height1.05as desktop
font-weight300as desktop
letter-spacing-0.02emas desktop

Craft matters to me, but so does the environment around it.

.quote. Pull quote. A quotation in the system serif, the one serif on the site, with .quote-attribution under it in the sans at the label’s size. The recommendations on /about/.

PropertyDesktop(max-width: 55.99rem)
font-familyui-serif, 'Iowan Old Style', Charter, Georgia, serifas desktop
font-sizeclamp(1.5rem, 3vw, 2.5rem)as desktop
line-height1.25as desktop
font-weight400as desktop
letter-spacing-0.01emas desktop

A ninth role, a condensed feature heading, is reserved and not set: the site carries no condensed face, and one is a new dependency waiting on a choice.

Page anatomy

Where every part of a page sits, drawn to scale from the stylesheet: a 1440 by 900 desktop window and a 390 by 844 phone. The header, the nav, Digiguru, the back link and the footer bar are the same on every page; the content column is what a page fills.

Desktop page anatomy at 1440 by 900.Logo, page titleNav, DigiguruBack linkSection menuTitle, 16rem downText, 47rem wide© and PrivacySocial, back to top7rem gutter7rem gutterEdge blur, top and bottom 12vh

Phone page anatomy at 390 by 844.Logo, titleBack linkTitleText, the full columnNav, fixed to the bottom1.5rem gutter

PartDesktopPhone
Gutter140px each side30px each side
Header140px tall, logo and page title left, nav and Digiguru rightLogo and title left, Digiguru top right
NavIn the header, top rightA 40px bar fixed along the bottom
Back linkFixed 140px down, 140px inFixed 80px down, 30px in
Section menuFixed at the left from 85rem, 200px wide at 1440; the column shifts clear of itNot shown
Page title16rem down on a long pageThe page's own top
Running textAt most 47rem (940px)The full column
Footer barFixed, 140px: copyright left, social and back to top rightAt the foot of the footer, stacked
Edge blurTop and bottom 108px (12vh)Top and bottom 101px (12vh)

Space and measure

Five steps. Things that belong together sit close; groups stand apart. Every page takes its vertical rhythm from them.

TokenValueUsed for
--space-tight1remA title and its meta line.
--space-paragraph3remOne paragraph and the next.
--space-headingcalc(var(--space-paragraph) + 0.3rem)A heading and its own text: the same visible gap as one paragraph and the next.
--space-itemvar(--space-beside)One entry in a list and the next.
--space-group5rem, 8rem from min-width: 56remThe title block and the content, one section and the next, a listing’s header and its list.

The measure

--measure: 47rem. Every paragraph and list wraps at it, on every page: about 75 to 85 characters of 28px copy. Titles and headings are not held to it.

The top of a page

Every long page starts its title at the same height on desktop, padding-top: 16rem, a group’s gap under the back link.

The gutter

1.5rem below the desktop breakpoint and 7rem above it. Lists indent to 0 1.5rem 0 9rem, so their markers hang outside the text column.

The section

A full-bleed section is min-height: 100vh with scroll-snap-align: start, so the page advances one screen at a time. Every section-* class must also carry section, or it loses both and script.js cannot find it for the background fade.

Long-form pages carry snap-scroll-disabled on the body, which turns the snapping off so they scroll as one continuous read: this one, /blog/, /books/ and /career/.

Breakpoints

A media query’s rem is 16px, not the 20px this site sets on :root. Media queries are measured against the browser’s initial font size and are unaffected by that declaration, so 56rem is 896px and not 1120px. The pixel column below is the real width.

QueryActual widthWhat changes
30rem480pxThe newsletter form stops stacking and sits on one row.
40rem640pxThe books grid and the career rows take their wider layout.
56rem896pxThe theme’s own breakpoint. Nav moves to the header, the gutter goes to 7rem, type goes up a step, Digiguru becomes a panel instead of a page.
85rem1360pxThe in-page section menu appears beside the copy.
94rem1504pxThe footer copy shifts clear of the section menu.

Motion

  • --transition-speed: 0.5s The site’s single duration.
  • --reveal-fade: 900ms How long a line of text takes to fade in as it arrives.
  • --reveal-duration: 700ms How long it takes to rise into place, from 30px below.
  • 300ms Between one line and the next, so a section arrives in sequence.
  • --reveal-media-duration: 1200ms A picture settling into its frame.
  • cubic-bezier(0.4, 0, 0.2, 1) Anything that travels.
  • ease-in-out Anything that only changes colour.

Hover is filter: brightness(1.2) rather than a second colour, so a link brightens against whatever field it is sitting in and needs no hover value per section.

Anything travelling with the Digiguru panel animates transform. The panel slides on the compositor, while right, margin and width are recalculated on the main thread and fall behind it as the page reflows, even at an identical duration and curve.

Section imagery is driven by GSAP rather than CSS. theme/script.js holds the page at zero on load and staggers it in, then fades each section’s media as it reaches the middle of the viewport, writing opacity inline. Several rules therefore carry no transition of their own on purpose, because a CSS transition on top would chase values GSAP is already tweening frame by frame.

prefers-reduced-motion: reduce keeps the fades and drops the travel.

Hover this link to see brightness(1.2)

Edges, focus and elevation

Focus

One ring for the whole site: outline: 2px solid currentColor at outline-offset: 2px, on :focus-visible, so it appears for the keyboard and never on a mouse click. currentColor is what lets it track the field behind it. It is never removed, only redrawn.

Tab to this link to see the ring

Elevation

The site has one raised surface and it is a picture frame. figure.media-surface takes a border, a radius and a shadow. Nothing else is lifted off the page, and there are no cards, pills or containers with fills of their own.

PropertyBase(min-width: 56rem)
border-radius1rem1.25rem
border0.15rem solid #1b1b270.3rem solid #1b1b27
box-shadow0 0.5rem 1.5rem rgba(0,0,0,0.35)0 1rem 2.5rem rgba(0,0,0,0.35)

Hairlines

Content is never divided by a line. Space divides it, and a line is kept only where it outlines something you use. Form controls take 1px solid rgba(27, 27, 39, 0.18) at 0.25rem, and the Digiguru panel marks its edge with a 1px column of rgba(29, 29, 31, 0.12). There is no elevation scale and no second border weight.

Header and navigation

One <nav>, one implementation, on every route. What changes across the breakpoint is where it sits. Its metrics never change.

Anatomy

The header and navigation at both breakpoints. Below 896px the logo sits top left, the Digiguru sparkle top right, and the four nav links run along a fixed bar at the bottom of the window. Above 896px the logo and page title sit top left and the links and sparkle sit together at the top right. Below 896pxlogosparklenav, fixed to the bottom896px and abovelogo, page titlenav and sparklesection menu, 1360px up

  • header The frame. Fixed and full-height, with pointer-events off so only its children are clickable.
  • .logo The CR—G mark. Anchored to the page gutter and never moved by anything.
  • header .page-title The page title beside the mark. A paragraph, so no heading comes before the page’s h1. Empty on the home page.
  • nav The link row. A fixed bar at the bottom of the window on a phone, in the header above 896px.
  • nav a One link. Four of them, plus Home, which is hidden above the breakpoint and below 352px, where the mark is the way home.
  • .nav-newsletter The longest label in the row, and so the one that sets how much width the row needs on a phone.
  • .nav-ask The Digiguru sparkle. A button, because it opens a panel rather than going anywhere, and the busy indicator while a question is in flight.

States

StateDeclarationWhy
Restcolor: var(--text-color) !importantTakes the section in view, so the row recolours as the reader scrolls.
Hoverfilter: brightness(1.2)A filter, so one rule works on every field without a hover colour per section.
Dimmedopacity: 0.4Every link while another is hovered. The row fades back rather than the hovered one lighting up.
Heldopacity: 1The link being hovered, and the one for the page you are on.
Focusoutline: 2px solid currentColorThe site ring, on :focus-visible. Keyboard only.

Specification

PropertyBase(min-width: 56rem)
positionfixedstatic
height2rem3rem
justify-contentspace-evenlycenter
max-widthnone16rem
font-size0.7rem1rem
padding0.5rem0 0.8rem
transitionopacity 0.45s cubic-bezier(0.4, 0, 0.2, 1), color var(--transition-speed) ease-in-out, filter var(--transition-speed) ease-in-outopacity 0.45s cubic-bezier(0.4, 0, 0.2, 1), color var(--transition-speed) ease-in-out

Behaviour

Hovering any link dims every link in the row to 0.4 and holds the hovered one at 1. The row fades back rather than the target lighting up, which is why the transition is on opacity and runs at 0.45s rather than the site’s 0.5s.

Home is in the markup at every width and hidden above the breakpoint, where the logo already goes home. Every label reads in full rather than abbreviating:Newsletter is the longest, and the row below 896px sets its type a step smaller than the theme's, which holds five links and the sparkle from 333px up without changing the row's spacing. Narrower than that the row runs past the edge, which is accepted rather than squashed.

The nav and the social row never change width or spacing. Not when the panel opens, not at any breakpoint. What moves is their position, translated by the panel’s width. If a layout needs a wider minimum to hold them at full size, it gets one. Neither row is ever squashed to make something else fit.

In-page section menu

The second level of navigation, inside one page. It is the rail on the left of this page, and it appears at 1360px, where there is room for it beside the copy. Below that width it is not shown, and the page is complete without it.

Anatomy

The section menu: two group headings each with a chevron, the first one open showing its indented entries, and the current entry marked by a short rule to its left. group heading, openentry at rest, 0.45entry current, 1, with its markerentry at restgroup heading, shut, 0.6its panel, clipped to 0fr and inertrail: fixed, left 7rem, top 11remwidth clamp(10rem, 13vw, 20rem)

  • .section-menu The rail. Fixed at the page gutter, scrolls inside itself when the list is longer than the space.
  • .section-menu-group An accordion group. One open at a time.
  • .section-menu-toggle A group heading. A button, because it opens the group as well as going to it.
  • .section-menu-chevron Two borders of a square, turned. Points right when the group is shut and down when it is open.
  • .section-menu-panel The drawer. A grid whose single row animates from 0fr to 1fr, which is what height: auto cannot do.
  • .section-menu a One entry, pointing at one section.
  • .section-menu-rule The current marker. A rule that grows out of nothing and pushes the label across with it.
  • .section-menu-label The word. It carries the focus ring, so the ring does not box the full-width row.

States

StateDeclarationWhy
Entry at restopacity: 0.45Present but quiet. The page is the thing being read.
Entry hoveredopacity: 0.8Not full strength, so hover never reads as current.
Entry currentopacity: 1Set by JavaScript on the section crossing the middle of the viewport.
Marker at restwidth: 0Zero, so it takes no space until it is earned.
Marker currentwidth: 1.5remGrows and carries the label across, so it reads as one movement rather than two.
Group shutgrid-template-rows: 0frClipped to nothing, and marked inert so its links leave the tab order with it.
Group opengrid-template-rows: 1frAnimatable, which height: auto is not.
Chevron shuttransform: rotate(-45deg)Pointing right.
Chevron opentransform: rotate(45deg)Pointing down, on the same curve as the drawer.
Heading shutopacity: 0.6Three weights of heading: shut, open, and open with the reader inside it.
Heading openopacity: 0.8Open, but the reader is somewhere else.
Heading activeopacity: 1The group holding the section in view.

Specification

PropertyBase(min-width: 85rem)
displaynoneblock
positionnot setfixed
topnot set11rem
leftnot set7rem
widthnot setclamp(10rem, 13vw, 20rem)
max-heightnot setcalc(100vh - 16rem)
font-sizenot set1rem
font-weightnot set300

Behaviour

The current entry is the one whose section is crossing the middle of the viewport, not the nearest by centre. A section taller than the window has its centre off screen, so comparing centres would mark a neighbour a screen away. theme/script.jsresolves the section behind the chrome the same way.

Clicking an entry slides to it over 620ms on an ease-out curve rather than jumping, and lifts scroll-snap-type for the duration, because mandatory snapping would otherwise arrive at the destination before the animation could run. A reader who has asked for reduced motion gets a jump instead.

A group heading opens its own drawer and shuts the others. The first heading on a page goes to the top rather than to a section of its own, because everything above it is the way in. Clicking a heading never shuts it, since the reader asked to go somewhere.

When the reader is on a screen the menu does not name, nothing is marked and every group closes, rather than a drawer standing open claiming otherwise.

Form controls

One shape for every control: the same height, hairline, radius and type. The specimens below carry the real classes from /books/, where the pattern is defined.

Anatomy

Live specimens. Hover them, then tab to them. The working pair is on /books/.

  • .books-field Label and control in one column, so the label always sits with what it names.
  • .books-field-label A visible label, not a placeholder standing in for one.
  • .books-search-field The text input.
  • .books-select The select’s positioning context, so the arrow can be drawn over it.
  • .books-topic-select A real <select> with appearance: none, keeping its native keyboard behaviour.
  • .books-select-arrow Two borders of a square, turned. pointer-events: none, so the control still opens on a click anywhere.

States

StateDeclarationWhy
Restborder: 1px solid rgba(27, 27, 39, 0.18)A hairline in the ink colour. No fill, so the control sits on the page rather than on a card.
Hoverborder-color: rgba(27, 27, 39, 0.4)The hairline darkens. The only thing that changes.
Focusborder-color: rgba(27, 27, 39, 0.55)Darker again, so the control reads as live before any ring is drawn.
Focus visibleoutline: 2px solid #1b1b27The ring, for the keyboard only. A mouse click gets the border change and nothing else.

Specification

PropertyValue
padding0.6rem 0.75rem
border1px solid rgba(27, 27, 39, 0.18)
border-radius0.25rem
font-size1rem
font-weight300
line-height1.5
backgroundtransparent

Behaviour

Safari colours a select’s options from the control’s own background, so a transparent background produces unreadable options in dark UI. The option colours are therefore named explicitly rather than inherited.

The newsletter form is Kit’s embed rather than the site’s markup, the one place the site styles someone else’s HTML. Its field and button are reshaped to these same values rather than to a second pattern.

Media surfaces

figure.media-surface is how work imagery is presented: contained rather than cropped, so the whole frame is visible at any viewport shape.

CR—G set in white on red, with a KitKat bar in place of the dash and the words Have a break.
  • figure.media-surface The box. Centres its contents and takes the section’s field behind it.
  • img, video Sized by one shared rule, so swapping a still for a clip involves no movement.
  • object-fit: contain Contained, never cropped. The whole frame stays visible at any viewport shape.

The border steps up at the desktop breakpoint, from 0.15rem solid #1b1b27 to 0.3rem solid #1b1b27, because the frame is bigger and a hairline stops holding it. Its colour is #1b1b27 rather than currentColor, because it frames the picture and should not change with the section.

Nothing autoplays from a third party. Videos and X embeds are click-to-load facades with locally served posters, so opening a page contacts nobody.

The Digiguru panel

Digiguru opens from the sparkle in the navigation. It is on this page, so open it and the rest of this section is happening beside you.

Anatomy

The panel closed and open. Closed, the page fills the window. Open, the body narrows by exactly the panel's width and the page reflows into the remaining column, with the panel beside it rather than over it. Closedpage fills the windowOpenpanelpage reflows, never covered

  • .nav-ask The trigger. A button in the nav, and the busy indicator while a question is in flight.
  • .ask-craig The panel. Fixed, full height, and translated on transform so it slides on the compositor.
  • --ask-craig-panel-width One property sizing the panel and narrowing the body by the same amount, so the two cannot disagree.
  • body.ask-craig-open The single state class. Nothing is measured in JavaScript.

Specification

TokenValueNote
--ask-bg#f5f5f7.section-light’s field, verbatim.
--ask-ink#1d1d1fIts text.
--ask-accent#cd2c2cThe signature red.
--ask-mutedrgba(29, 29, 31, 0.62)Derived from the ink, so the panel has no colours of its own.
--ask-linergba(29, 29, 31, 0.12)The one divider in the panel.
--ask-gutter1remIts internal spacing.

Behaviour

The panel pushes the page. It never overlays it. The body narrows by exactly the panel’s width and the page reflows into the remaining column, so the site stays wholly visible and usable. The two regions scroll independently, and the page’s scrollbar sits beside the panel rather than at the window edge. Below 1200px the panel becomes its own full-screen page instead. This is the standing rule for any panel, drawer or sidebar on this site.

The panel holds one fixed palette rather than tracking the section beside it, because text you are part-way through reading should not change colour underneath you. Those values are .section-light verbatim.

Width rather than padding on the body: a padding-right would keep the body the full width of the window and its scrollbar with it. Narrowing the box is what brings the scrollbar inside.

The laws

Decisions, taken once and binding on everything after. They are what make the site behave as one thing rather than a set of pages that resemble each other.

  1. A section owns the colour. The page borrows it.Nothing is painted with a fixed colour. A section sets --bg-color, --text-color and --link-color; theme/script.js reads them off whichever section is crossing the middle of the viewport and applies them to the page, the header, the nav and the footer. That is why chrome uses currentColor and inherit rather than a value of its own. It has to work on a red hero and a white page without a second set of rules.
  2. Panels push content. They never overlay it.Digiguru narrows <body> by its own width and the page reflows into the remaining column, scrolling inside that box so its scrollbar sits beside the panel rather than at the window edge. Below 1200px the panel becomes its own full-screen page instead. This is the standing rule for any panel, drawer or sidebar on this site.
  3. Anything that travels with the panel animates transform.The panel slides on the compositor. right, margin and width are recalculated on the main thread and visibly fall behind it while the page reflows, even at the same duration and curve. Same property, same pipeline, or it desyncs.
  4. The nav and the social row never change width or spacing.Not when the panel opens, not at any breakpoint. Both keep the theme’s own metrics everywhere; what moves is their position, translated by the panel’s width. If a layout needs a wider minimum to hold them at full size, it gets one. Neither row is ever squashed to make something else fit.
  5. Nothing on the left edge moves when the panel opens.The logo and the copyright are anchored to the page gutter and stay there.
  6. Repeated elements are site-wide components.The header, navigation, footer, footer media, Digiguru and the page transitions have one shared implementation and behave the same on every route. A page-specific variation is a defect, not a feature.
  7. One stylesheet, and it is public/theme/style.css.The theme’s own rules and everything the site adds on top of them, in one file, organised by what each section owns. There is no override sheet. If a rule needs to beat an earlier one it says so in a comment and sits below it, in the section that owns it. src/styles/bundle.css compiles it with three others into the single asset the pages link.
  8. Every section-* class also carries `section`.Without it the section loses min-height: 100vh and scroll-snap-align, and script.js cannot find the element for the background fade.
  9. A focus ring is never removed, only redrawn.2px solid currentColor at 2px offset, on :focus-visible so it appears for the keyboard and not for a mouse click. currentColor is what lets it track the section behind it. Controls that suppress it draw their own: the section menu moves the ring onto the label so it does not box an empty rail.
  10. Nothing contacts a third party on page load.Videos and X embeds are click-to-load facades with locally served posters. Fonts are the system stack, so there is nothing to fetch.

What is not clean

What the stylesheet actually contains, generated rather than remembered. The counts move when the sheet does.

  1. The palette is three colours. The site’s own moods use 8.Every value below is a shade of one of the three, close enough that none of them is distinguishable from it on screen. Each is cost with no effect.
    In useShould beApartWhere
    #0f0f0f#0808081.04:1.section-dark
    #1b1b27#0808081.18:1.section-light
    #1d1d1f#0808081.19:1.section-home-about
    #f5f5f5#f5f5f71:1.section-hero.section-medium
  2. .section-medium sets its link colour to its own background.#CD2C2C on #CD2C2C is 1:1, so a link there is invisible. It has already caused a real defect: the footer copyright link carries color: inherit to escape it.
  3. Red on a dark field is below AA for body copy..section-dark at 3.64:1 and .section-home-about at 3.2:1. Both clear the 3:1 large-text threshold, so a heading is fine and a sentence is not. Off-white links on those fields would close it.
  4. Lists are a weight heavier than the copy around them.p declares font-weight: 300. No rule anywhere declares one for ol or ul, so a list falls back to the browser’s 400 at the same size as the paragraph above it. One declaration closes it.
  5. There is no semantic layer.A mood names its own hex values rather than resolving through --surface, --ink and --accent, so changing the red is several edits rather than one. The system is small enough that this has not cost anything yet.