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
The grid carries a radial centre-fade — full strength toward section edges, gone around the middle where content sits. 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.
// Preferred — via SectionFrame
<SectionFrame surface="paper" panel="ink">
<AboutSection />
</SectionFrame>
// Or directly, for a card that paints its own background
<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.
| Token | Paper value | Contrast |
|---|---|---|
--surface-section / --background | #faf9f3 | the ground |
--foreground | #141210 | 17.7:1 |
--foreground-secondary | rgba(20,18,16,.78) | 9.1:1 |
--text-tertiary | rgba(20,18,16,.68) | 6.3:1 |
--text-muted | rgba(20,18,16,.60) | 4.8:1 |
--accent | --accent-deep #0b5fd4 | 5.5:1 |
--surface | #ffffff | card fill |
--surface-hover | color-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.
panel-container
Lines a standalone card up with the outer edges of a SectionFrame panel. Two different insets are in play inside a frame: the panel sits --spacing-4 in from the 1200px box, while section content sits at the wider px-4 sm:px-6 lg:px-8 scale so it clears the panel's rounded corners. A card that reads as a sibling of the panels — the CTA closing out a stack of them — has to use the panel's inset, or it lands 16px narrow on each side.
<div className="panel-container">
<CTACard variant="compact" />
</div>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.