Surfaces

The site is dark by default. A surface is a subtree that flips that — a light plane inside the dark page, or a dark island inside a light one. Surfaces work by re-pointing the generic colour tokens, so components adapt without knowing which ground they landed on.

Light grounds, dark content

The house rule for every light section: the ground goes light, the cards and panels on it stay dark and look exactly as they do on the dark pages. In practice a light section means the section header inverts to ink, and everything else gets surface-ink plus an opaque backing.

surface="paper" — warm cream

Ink on paper

Secondary copy at 9.1:1

surface="white" — the same ladder, brighter

Ink on white

Secondary copy, slightly higher contrast

Both previews above use the real classes — nothing here is a mock-up of the colours.

surface-ink — dark islands

.surface-ink restores the :root values for something that must stay dark inside a light section: the service illustration screens, case-study cards, the CTA card. Everywhere else it is a true no-op, which is what keeps the dark pages identical.

Ink island

White text again, because the tokens are back to their :root values.

tsx
// A card that paints its own dark background on a light ground
<div className="surface-ink">
  <CTACard />
</div>

The paper ladder

Measured on the rendered page against the panel fill, not estimated. The alpha ladder is compressed upward, not mirrored: dark mode's 72/40/30 measures 5.5/3.6/3.0 on cream, so tertiary and muted would both fail AA. Paper runs 78/68/60. The perceived gap between steps is narrower than in dark mode because the usable contrast range on a light ground genuinely is narrower — don't stretch it back out by lowering muted.

TokenPaper valueContrast
--surface-section / --background#faf9f3the ground
--foreground#14121017.7:1
--foreground-secondaryrgba(20,18,16,.78)9.1:1
--text-tertiaryrgba(20,18,16,.68)6.3:1
--text-mutedrgba(20,18,16,.60)4.8:1
--accent--accent-deep #0b5fd45.5:1
--surface#ffffffcard fill
--surface-hovercolor-mix(--foreground 9%, --background)badge chips

Two things that will bite

The default text colour must live in @layer base

<body> carries text-foreground, which computed to white against :root, and inheritance passes that computed value down — so re-pointing the variable alone leaves every unclassed text node white on cream. Both surface classes therefore need a default color: var(--foreground). But declaring it unlayered is worse: unlayered rules beat every layered one regardless of specificity, so it would override any text-* utility on the same element — a surface-ink text-background pill silently renders white. In base it beats inheritance and utilities still beat it.

Translucent card fills need an opaque backing

--gradient-card-surface is a 5% wash of --foreground ending on transparent. On the dark page the section band shows through, which is the intent; over cream that same transparency lets the paper tint the card. Layering the identical wash over the opaque --surface-ink-panel reproduces the dark-page composite exactly — see .surface-paper .card-service and .surface-paper .card-carousel-item.

The two ladders are a mirror set

.surface-paper and .surface-ink re-point the same 18 tokens. Adding one to either without the other leaves an ink island resolving a paper value — a silent light-on-light bug. There is no pure-CSS way to express "reset these to :root", so the duplication is deliberate and hand-maintained.

card-ghost

Stands in for a carousel's non-active cards. Opaque and mixed from --foreground against --background, so it resolves per surface with no variants — soft tan on paper, soft charcoal on the dark page. It replaced dimming the neighbours with opacity, which only worked while the ground was as dark as the card.

page-container

The house measure. Every section's content sits in one of these — the width lives once, in --container-page, so changing it is a single edit rather than three dozen. It carries the px-4 sm:px-6 lg:px-8 gutter itself, and sits in @layer components so a call site that needs different padding can still say so with a utility.

tsx
:root { --container-page: 1360px }

<div className="page-container">…</div>

article-column

The one reading column shared by case studies and blog posts: 1040px, centred at every width. Both routes put their hero, their body and (case studies) the client reviews in it, so the title, the excerpt, the prose and the reviews all share one left edge. There is no sidebar (removed Oct 2026, with article-grid): the contents live in ContentsPill, docked at the bottom of the screen. Image sliders inside it bleed to the screen's right edge (CaseStudyImageCarousel), so the article clips horizontal overflow (overflow-x-clip).

tsx
<article className="overflow-x-clip">
  <div className="page-container">
    <div className="article-column">…article…</div>
  </div>
</article>

Where they're used

On the home page: Case Studies on paper, Services on white, then a cream run carrying About and Tools as ink panels plus the closing CTA. Every other page stays dark, so the surface classes are opt-in per call site — never global.