Resinen
Foundations

Themes

5 themes, and each one paints the weather its name promises. Switch with the control in the top bar — it writes a single attribute on <html>, and the sky behind this page changes with it.

The skies

A theme is a palette and an atmosphere. Each tile sets its own data-theme and runs the real <Atmosphere> inside it — live canvas, not an image. Components stay flat on top: the sky is the page's ground, never a surface's skin, which is how 37 components keep their contrast guarantee.

Daybreak

Daybreak — a calm high sky

intensity 0.34 · 3 gated fields
Horizon Sunrise

Horizon Sunrise — the sun's path across water

intensity 0.55 · 4 gated fields
Stormy Clouds

Stormy Clouds — banks drifting at three speeds

intensity 0.62 · 4 gated fields
Night Aurora

Night Aurora — ribbons over a star field

intensity 0.52 · 4 gated fields
Midnight

Midnight — true black and stars

intensity 0.7 · 2 gated fields

Readability is not left to taste. Every scene is drawn over its theme's --ground and then veiled with it again, so a pixel can travel at most intensity × alpha from the page's base colour. check-atmosphere.mjs recomputes that worst case from the same numbers the canvas draws with and fails the build if body text drops below 4.5:1 — or if a sky is so veiled it has become invisible. Stars are the one exemption: sub-2px point lights are not backgrounds, so they are capped by radius instead of by contrast.

The palettes

Each tile paints itself with literal values from the token export, so every theme is visible at once regardless of which one is active.

Daybreaklight · data-theme="daybreak"

Full sun on open water. A bright sand ground, deep-sea ink, and colour spent freely rather than rationed.

Primary action
Horizon Sunriselight · data-theme="horizon"

First light. Warm paper ground, near-black ink, a terracotta accent held to small areas.

Primary action
Stormy Cloudslight · data-theme="storm"

Overcast. A cool grey ground a shade deeper than paper white, with a steel accent.

Primary action
Night Auroradark · data-theme="aurora"

Blue-black with a cold green accent. Every status colour flips to dark ink, because the lifted variants are too bright to carry white.

Primary action
Midnightdark · data-theme="midnight"

True black with white stars. Built for one page: the night jungle, where the light you are walking toward should be the brightest thing on screen.

Primary action

Every semantic token, every theme

Components reference only this table, never a primitive. The four surface tokens are what replace an elevation ramp — depth is a step on the neutral scale plus a hairline, not a shadow. That is why each ramp has eleven steps.

TokenDaybreakHorizon SunriseStormy CloudsNight AuroraMidnightRole
--ground#FFF8EB#FAF8F5#EBEDEF#0B1016#000000Page floor
--plane#FFFFFF#FFFFFF#F5F6F7#121A22#11141BSurfaces
--plane-alt#FFF2DC#F2EFE9#FFFFFF#1A242E#1C212BHover, and the raised variant
--plane-sunk#FCEBCF#F2EFE9#DCDFE3#070C11#07080DInset — track, well, code block
--ink#16203A#141110#10151A#EAF0F4#FFFFFFPrimary text
--ink-muted#4B5A75#6B6154#5B646E#8A9CAA#A8B2C4Secondary text
--ink-faint#7E8CA6#8A8072#78818B#65798A#6E7A90Placeholder text
--linergba(22, 32, 58, 0.12)rgba(20, 17, 16, 0.10)rgba(16, 21, 26, 0.12)rgba(234, 240, 244, 0.11)rgba(255, 255, 255, 0.12)Hairline
--line-strongrgba(22, 32, 58, 0.30)rgba(20, 17, 16, 0.24)rgba(16, 21, 26, 0.26)rgba(234, 240, 244, 0.22)rgba(255, 255, 255, 0.26)Control borders
--accent#0B6FD4#B8431C#2F6189#35D29A#F5A800The one accent
--accent-ink#FFFFFF#FFFFFF#FFFFFF#0B1016#000000Text on the accent
--signal#C62828#9B2B24#9B2B24#EE7268#EE7268Destructive

Note what Night Aurora does with status colours: every one of them flips -ink to near-black, because the lifted variants are too bright to carry white text. A flat token system can't express that without hardcoding a different pairing per component.

The contrast gate

The build checks 20 pairs per theme — 100 in total — and refuses to emit if any text pair falls below 4.5:1. Translucent layers are composited first, so a tinted badge is measured against the tint over its real surface rather than against the ideal colour.

ThemePairRatioRequiredStatus
Daybreakplaceholder text on surface3.39:13:1pass
Stormy Cloudsplaceholder text on surface3.65:13:1pass
Horizon Sunriseplaceholder text on surface3.88:13:1pass
Night Auroraplaceholder text on surface3.89:13:1pass
Midnightplaceholder text on surface4.26:13:1pass
Daybreaklink on page4.69:14.5:1pass
Daybreaklink on surface4.95:14.5:1pass
Daybreakprimary button4.95:14.5:1pass

Its first run on this palette rejected ink-faint at 2.46:1 — placeholder text at the 400 step of the ramp. Placeholder text is still text, and that failure is one of the most common real accessibility defects in shipped software. Moving to the 500 step fixed it.

Adding a fourth

A new key under themes in tokens.json, then pnpm tokens. No component changes, because no component has ever referenced a primitive. The theme menu picks it up too — its list comes from generated metadata, not a hand-maintained array.

"themes": {
  "$order": ["horizon", "storm", "aurora", "highnoon"],
  "highnoon": {
    "$label": "High Noon",
    "$scheme": "light",
    "ground": "{primitive.color.white}",
    "ink":    "{primitive.color.cool-950}",
    "accent": "{primitive.color.steel-700}",
    ...
  }
}