> ## Documentation Index
> Fetch the complete documentation index at: https://substrate.docs.unknowncreatives.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# The Scheme Axis: Light, Dark, and Everything Between

> Light and dark are the two endpoints of one continuous scheme axis. Substrate evaluates intent tracks at the current position and re-solves APCA contrast against the actual surface.

Light and dark in Substrate are not two token sets. They are the two ends of a single continuous axis, `scheme`, that runs from `0` (light) to `1` (dark). Every position in between is valid and produces solved values, which is what makes `dimmed` (`scheme: 0.65`) a real mode rather than a hand-authored third palette.

Because there is only one axis and one solve, the two ends cannot drift apart. There is no dark override file to forget to update.

***

## What moves along the axis

Three things change as `scheme` moves from 0 to 1:

1. **Surface lightness.** The base page surface is derived from the brand at the current position, and elevated surfaces are derived from that base.
2. **Intent hue and chroma**, but only if the intent declares a scheme track. A plain intent keeps constant hue and chroma at every position; a tracked intent interpolates between its stops. Track stops are pinned at continuous positions, so evaluation at `0.65` is an interpolation, not a snap to an endpoint.
3. **APCA polarity.** Near the light end, the solver searches for a dark foreground against a light surface. Near the dark end it searches for a light foreground. The target Lc is the same magnitude; the direction flips.

What does *not* change is the contrast policy. Foreground targets Lc 75, borders Lc 50, and focus rings Lc 60 at every scheme position — see [High Contrast](/modes/high-contrast) for the one axis that does scale those targets.

<Note>
  Lightness is never authored. An intent declares hue and chroma; the solver owns L. That is why moving along the scheme axis cannot break contrast — each position gets its own binary search for a lightness that hits the target against the surface actually in play.
</Note>

***

## Moving along the axis

Set `scheme` and re-sync. There is no attribute to toggle:

```ts theme={null}
import {
  syncPrefsToCssVars, updateAllVars, defaultPreferences, BRAND_REGISTRY,
} from '@substrate/engine';

const brand = BRAND_REGISTRY[0];
const prefs = defaultPreferences();  // scheme: 0

prefs.scheme = 1;            // dark
syncPrefsToCssVars(prefs);
updateAllVars(brand, prefs);
```

A toggle is just a two-value special case of a continuous control:

```ts theme={null}
prefs.scheme = prefs.scheme === 0 ? 1 : 0;
syncPrefsToCssVars(prefs);
updateAllVars(brand, prefs);
```

To follow the operating system instead of an in-app control, read the media query and write the axis:

```ts theme={null}
const mql = window.matchMedia('(prefers-color-scheme: dark)');

function follow() {
  prefs.scheme = mql.matches ? 1 : 0;
  syncPrefsToCssVars(prefs);
  updateAllVars(brand, prefs);
}

follow();
mql.addEventListener('change', follow);
```

***

## What the runtime writes

`syncPrefsToCssVars` writes the raw axis values to the root element, including `--scheme` itself, so CSS that composes from continuous scalars reacts immediately. `updateAllVars` runs the APCA solve and writes the per-intent primitives — five numeric properties per intent:

```css theme={null}
--ucs-brand-hue
--ucs-brand-chroma
--ucs-brand-fg-l       /* solved: foreground Lc 75 against the context surface */
--ucs-brand-border-l   /* solved: border Lc 50 */
--ucs-brand-surface-l  /* derived accent surface */
```

These are **primitives, not finished colors**. The generated CSS composes `oklch()` values from them with `calc()`, which is what lets a single set of numbers drive an entire cascade. An element selects which intent's primitives to use through its `data-mode` role:

```css theme={null}
[data-mode~="brand"] {
  --intent-hue:       var(--ucs-brand-hue);
  --intent-chroma:    var(--ucs-brand-chroma);
  --intent-fg-l:      var(--ucs-brand-fg-l);
  --intent-border-l:  var(--ucs-brand-border-l);
  --intent-surface-l: var(--ucs-brand-surface-l);
}
```

***

## Baked light and dark artifacts

Alongside the runtime path, the pipeline bakes static per-mode files for the moments before JavaScript runs and for native platforms. These hold resolved hex values rather than primitives — from `generated/brands/acme/css/tokens.light.gen.css`:

```css theme={null}
:root {
  --ucs-surface-surface: #f4f5f8;
  --ucs-surface-text: #0b0b0d;
  --ucs-surface-border: #717174;
  --ucs-brand-surface: #6022f3;
  --ucs-brand-text: #652dfb;
  --ucs-brand-border: #9084ff;
}
```

Each brand gets `tokens.light.gen.css`, `tokens.dark.gen.css`, `tokens.highContrast.gen.css`, and `tokens.darkHighContrast.gen.css`. There is no `tokens.dimmed.gen.css` — a mid-track position has no endpoint to bake, so dimmed exists only through the runtime or a native kernel port.

On top of those, each brand's CSS carries the no-JS snapshot: the same four presets emitted on `[data-brand="<slug>"]` under `prefers-color-scheme` and `prefers-contrast` media queries. That snapshot covers the page surface at first paint; scoped surfaces and continuous positions need the runtime. See [Modes Overview](/modes/overview#the-no-js-floor).

***

## Authoring intent across the axis

Most intents need nothing beyond a hue and chroma — the solver handles both ends. When a color needs to shift character between the endpoints, declare a scheme track in the brand YAML:

```yaml theme={null}
intents:
  neutral:
    hue: 210
    chroma: 0.02
    scheme-end: { hue: 220, chroma: 0.04 }
```

`scheme-end` is the shorthand for a two-stop track; a full `scheme-track` list gives you stops at arbitrary positions. Letting chroma rise toward the dark end is a common way to keep a color's perceived character stable, since colors read as less saturated against dark surfaces. Full grammar in [Core Concepts](/core-concepts#color-intent) and the [Color config reference](/brand-config/color).

***

<CardGroup cols={2}>
  <Card title="Modes Overview" icon="sliders" href="/modes/overview">
    The full seven-axis preference vector and the five named presets.
  </Card>

  <Card title="High Contrast" icon="circle-up" href="/modes/high-contrast">
    The contrast axis that scales the Lc policy independently of scheme.
  </Card>
</CardGroup>
