> ## 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.

# Defining Color Intent in a Substrate Brand Config

> Colors live in an open intents map as OKLCH hue and chroma. Lightness stays solver-owned, and every intent — conventional or brand-specific — gets identical treatment.

Substrate treats color as *intent*, not as a palette. You never author a hex value, and you never author a lightness. You declare what a color's perceptual character is — its hue and its chroma — and the APCA solver computes the lightness that achieves the target contrast against whatever surface the color actually lands on, at whatever position the user's scheme slider sits. APCA attribution and use restrictions: see [About APCA](/reference/apca-solver#about-apca).

## The intents map

Colors live under a top-level `intents:` key. It is an **open map**: only `brand` and `neutral` are required, and everything else is the brand's own decision. Each intent carries two OKLCH parameters:

* **`hue`** — the color angle, 0 to 360
* **`chroma`** — colorfulness, 0 (achromatic) to roughly 0.4, with the real ceiling depending on hue and display gamut

An example foundation config for a fictional brand, Acme (`src/brands/acme/config.global.yaml`):

```yaml theme={null}
intents:
  brand:
    hue: 277        # Acme Indigo
    chroma: 0.268
  neutral:
    hue: 251        # cool-tinted neutrals
    chroma: 0.012
  danger:
    hue: 11         # ruby red
    chroma: 0.228
  warning:
    hue: 71
    chroma: 0.171
  success:
    hue: 143
    chroma: 0.214
  info:
    hue: 256
    chroma: 0.188
  # Acme's signature gradient palette — custom intents, same treatment
  acme-cyan:
    hue: 228
    chroma: 0.147
  acme-mint:
    hue: 165
    chroma: 0.112
```

<Note>
  Intent names are your identifiers, so they round-trip verbatim — the kebab-case-to-camelCase normalization described in the [config overview](/brand-config/overview) does not touch the direct children of `intents`. `acme-cyan` stays `acme-cyan`.
</Note>

## Conventional names, not reserved ones

There is no built-in-versus-custom split in Substrate. The pipeline reads whatever intents a brand declares; it never consults a fixed list to decide what a brand "should" have. Custom intents produce the same solver primitives, the same baked per-mode tokens, and the same platform output as any other.

What does exist is a convention. Across the engine's bundled demo brands, these names recur and carry consistent meaning:

| Intent    | Conventional meaning                                                            |
| --------- | ------------------------------------------------------------------------------- |
| `brand`   | Brand identity, primary actions, key interactive elements. **Required.**        |
| `neutral` | Surfaces, borders, and body text — the backbone of layout chrome. **Required.** |
| `danger`  | Destructive actions, validation failures, critical alerts                       |
| `warning` | Cautionary states and non-blocking alerts                                       |
| `success` | Confirmation and positive feedback                                              |
| `info`    | Informational messages and assistive hints                                      |
| `beta`    | Pre-release and experimental surfaces                                           |

<Warning>
  Two kinds of naming apply here. `brand` and `neutral` are **contractual** — the loader refuses a config without them (and `brand` is the identity intent; there is no `primary`). Every other name is **convention**: the bundled demo configs and these docs use `danger`, but your brand may call its destructive intent `error` or `destructive` and get identical solver treatment. What actually breaks expectations is assuming a conventional intent exists without declaring it — nothing ships an `error` (or even a `danger`) intent by default; your `intents:` map is the only source of truth for which intents exist.
</Warning>

Brands lean on custom intents heavily. A fictional Aurora Airways family might declare `aurora-navy` and `aurora-burgundy` at the family level, while its Rewards sub-brand adds loyalty-tier intents like `tier-gold`, `tier-silver`, `tier-platinum`, and `tier-accent` — each getting the full solver treatment.

## Intents that vary across the scheme axis

An intent's hue and chroma can be a function of the scheme position rather than a constant — useful when a color's perceived character drifts at luminance extremes and you want to compensate. The grammar has two tiers.

<Tabs>
  <Tab title="scheme-end">
    The shorthand: give a second hue and chroma for the dark end. It desugars to stops at positions 0 and 1.

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

  <Tab title="scheme-track">
    The full form: a list of stops, each pinned at a continuous position `at` between 0 (light) and 1 (dark). A stop can sample another intent at the same position with `from-intent` instead of naming its own values, and `blend` selects the interpolation space (`oklch`, the default, or `oklab`).

    ```yaml theme={null}
    intents:
      accent:
        blend: oklab
        scheme-track:
          - { at: 0, hue: 210, chroma: 0.02 }
          - { at: 0.6, from-intent: brand }
          - { at: 1, hue: 220, chroma: 0.06 }
    ```

    `scheme-end` and `scheme-track` are mutually exclusive on one intent.
  </Tab>
</Tabs>

These three grammar keys keep their kebab spelling everywhere, because the kernel's track evaluator consumes them verbatim. Scheme tracks are a supported capability that none of the engine's bundled demo brands currently uses — they all author constant hue and chroma per intent.

## Lightness: the one exception

Lightness never appears in an intent, at any tier. The solver owns it at every scheme position, which is what lets a single declaration produce correct foregrounds on light surfaces, dark surfaces, and every point between.

The exception is gradients. A gradient stop references an intent by name and *does* pin a lightness, because a gradient is a designed artifact rather than a contrast-solved one:

```yaml theme={null}
gradients:
  aurora:
    type: linear
    angle: 120
    stops:
      - intent: brand
        lightness: 0.50
        at: 0
      - intent: acme-pink
        lightness: 0.72
        at: 100
```

## What each intent produces

For every declared intent, the runtime writes a set of `--ucs-*` solver primitives that downstream CSS composes with `calc()`:

* `--ucs-{intent}-hue` and `--ucs-{intent}-chroma` — the evaluated character at the current scheme position, after the warmth and CVD user-compensation transforms
* `--ucs-{intent}-fg-l`, `--ucs-{intent}-border-l`, `--ucs-{intent}-surface-l` — solved lightness for each channel against the actual surface
* `--ucs-{intent}-pattern` — the non-color redundancy channel

The generated brand CSS registers these with `@property` so they animate and inherit predictably. The shape of a brand's generated `css/brand.gen.css`, here for a custom `aurora-navy` intent:

```css theme={null}
@property --ucs-aurora-navy-hue {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

@property --ucs-aurora-navy-fg-l {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}
```

Alongside the live primitives, the pipeline bakes a resolved surface/text/border triptych per intent per mode, as the no-JavaScript floor. The shape of `generated/brands/<brand>/<sub-brand>/css/tokens.light.gen.css`:

```css theme={null}
:root {
  --ucs-surface-surface: #eff7f9;
  --ucs-surface-text: #080c0d;
  --ucs-surface-border: #6d7374;
  --ucs-brand-surface: #bf0039;
  --ucs-brand-text: #c7003f;
  --ucs-brand-border: #ff5674;
}
```

Selectors are `[data-brand="aurora-rewards"]` for brand scoping and `[data-mode~="brand"]` for intent selection on an element carrying `data-ucs`. Substrate emits no theme attribute and no per-color variable family of its own — the `--ucs-*` primitives above are the whole vocabulary.

<Tip>
  Because lightness is solved rather than authored, adding a mode or shifting the scheme slider requires no new color declarations. If a color reads wrong in dark mode, the fix is almost always the intent's chroma — or a `scheme-end` — rather than a per-mode override.
</Tip>

<CardGroup cols={2}>
  <Card title="APCA Contrast Solving" icon="circle-half-stroke" href="/core-concepts">
    How the solver searches the lightness axis, and the fixed Lc policy it targets.
  </Card>

  <Card title="Brand Config Overview" icon="sliders" href="/brand-config/overview">
    The rest of the schema: elevation, typography, shape, motion, space, flexibility, and presets.
  </Card>
</CardGroup>
