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

# Color Vision Deficiency: Runtime Hue Redistribution

> Substrate compensates for protan, deutan, tritan, and achromat vision at runtime with continuous severity — redistributing every intent hue onto a discriminable arc before contrast is solved.

Around 8% of men and 0.5% of women have some form of color vision deficiency. When intents are distinguished only by hue — `danger` red against `success` green being the classic pair — those users lose the distinction entirely.

Substrate treats CVD as a **runtime user preference**, not a brand configuration. There is no CVD array in the brand YAML and no alternate token set to generate. The user's vision is described by two values, and the kernel redistributes intent hues to suit it before any contrast math runs.

<Warning>
  Read [Scope and limits](#scope-and-limits) before relying on this. Hue redistribution is an **aid, not a guarantee**, and it is never a substitute for non-color cues.
</Warning>

***

## The preference

CVD is a small struct on the preference vector: a type and a continuous severity.

```ts theme={null}
cvd: { type: 'deutan', severity: 0.8 }
```

The four compensable types — plus `none` — are the kernel's own identifiers, and they are spelled the same way in TypeScript, Swift, and Kotlin:

| Type       | Affects                                              | Compensation                                |
| ---------- | ---------------------------------------------------- | ------------------------------------------- |
| `protan`   | Reduced/absent long-wavelength (red) sensitivity     | Hue redistribution onto the blue-yellow arc |
| `deutan`   | Reduced/absent medium-wavelength (green) sensitivity | Hue redistribution onto the blue-yellow arc |
| `tritan`   | Reduced/absent short-wavelength (blue) sensitivity   | Hue redistribution onto the red-green arc   |
| `achromat` | Little or no color perception                        | Chroma reduction plus pattern overlays      |

Use these exact identifiers. The clinical names — protanopia, deuteranopia, tritanopia — do not appear in the API.

**Severity is continuous**, from `0` (normal vision) to `1` (full dichromacy or monochromacy). It is a first-class dimension, not a switch: `severity: 0.4` applies a proportionally weaker transform than `1.0`, which matters because most CVD is partial. Setting `type: 'none'` or `severity: 0` is the identity transform.

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

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

prefs.cvd = { type: 'protan', severity: 1 };
syncPrefsToCssVars(prefs);
updateAllVars(brand, prefs);
```

Because CVD is one axis among seven, it composes with the others for free. Dark plus deutan plus elevated contrast is simply three assignments to the same vector — there is nothing to nest and no combined artifact to generate.

The runtime can also persist and restore the setting, and it defers to the OS when the OS is already handling color:

```ts theme={null}
import { applyCvdFromStorage, persistCvd, applyForcedColorsCheck } from '@substrate/engine';

applyCvdFromStorage(prefs, onChange);   // restore a saved preference
persistCvd(prefs);                       // save the current one
applyForcedColorsCheck(prefs, onChange); // forced-colors: active disables compensation
```

***

## How compensation works

The transform order is fixed: **warmth → cvd → apca**. Color is adjusted for the user *first*, and contrast is solved against the already-compensated colors. Compensating after solving would invalidate the contrast guarantee.

### Chromatic types

For `protan`, `deutan`, and `tritan`, the kernel redistributes hues onto a **CVD-safe arc** — the range of hues that remain discriminable under that deficiency:

| Type     | Safe arc    | Axis                                 |
| -------- | ----------- | ------------------------------------ |
| `protan` | 70° → 290°  | Blue-yellow (\~220° wide)            |
| `deutan` | 70° → 290°  | Blue-yellow (\~220° wide)            |
| `tritan` | 310° → 130° | Red-green (\~180°, wraps through 0°) |

The redistribution applies to **every intent in the map**, not just the semantic ones. `brand`, `neutral`, and any custom intent you declare are all repositioned; nothing is exempt. Intents are ordered by their angular offset from the confusion-zone midpoint and mapped proportionally into the safe arc, subject to a minimum separation of **15°** between adjacent hues so that neighbors don't collapse into each other.

This is a redistribution, not a lookup table. There are no hard-coded "red becomes amber" mappings — where a hue lands depends on the arc, the other intents present, and the severity.

### Achromat

Monochromacy can't be solved by moving hues, so `achromat` takes a different path: chroma is reduced proportionally to severity, reaching zero at `severity: 1`. Hue is left alone because it no longer carries information.

Since color is then unavailable as a channel, the runtime sets `data-cvd-achromat` on the root element and the shipped CSS uses it to apply per-intent **pattern overlays**, so filled and subtle surfaces stay distinguishable by texture:

```css theme={null}
[data-cvd-achromat] .ucs-action[data-mode~="filled"][data-ucs] {
  background-image: var(--intent-pattern, none);
  background-repeat: repeat;
}
```

Each intent carries a `--ucs-{intent}-pattern` primitive alongside its numeric ones, mapped to `--intent-pattern` by the role selector.

### Simulation

For design review, the runtime can render the page *as a CVD viewer would see it* rather than compensating for them. This sets `data-cvd-sim` on the root and applies a Machado simulation matrix as an SVG color filter. It is a devtool for checking your work, and the inverse of compensation — never enable both as a production path.

***

## Scope and limits

The kernel carries these caveats in its own source, and they belong in front of anyone shipping this feature.

<Warning>
  **Hue redistribution helps dichromats most.** It is weak for *anomalous trichromats* — people whose color perception is compressed rather than absent, and who are **the majority of people with CVD**. Treat compensation as an aid, not a guarantee.
</Warning>

* **Never rely on color alone.** Information must not be conveyed by color by itself; pair it with text, icons, patterns, or position. This is [WCAG 2.2 §1.4.1 Use of Color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html) (Level A), and relatedly §1.4.11 Non-text Contrast (Level AA). Non-color cues are a requirement for *all* users, not a fallback that only monochromacy triggers.
* **The 15° minimum separation is an engineering heuristic**, not a canonical or standardized threshold. It was chosen to work well in practice, and it has no normative backing.
* **Validate empirically.** Run the Machado simulation for each CVD type against your actual palette and check pairwise perceptual distance (ΔE) between the colors that carry meaning. Do not assume the heuristic separation is sufficient for your specific set of intents.

The simulation basis is solid and citable: pre-computed 3×3 physiological matrices at 11 severity steps, from Machado, Oliveira & Fernandes (2009), *IEEE Transactions on Visualization and Computer Graphics* 15(6), interpolated for intermediate severities.

***

## No media query for this

Operating systems expose `prefers-color-scheme`, `prefers-contrast`, and `prefers-reduced-motion` — but there is no CSS media query for color vision deficiency, and no plan for one. CVD compensation therefore has no no-JS fallback: it requires the runtime and an explicit user preference, which is why persisting the choice matters. Give users a control, save it, and restore it on load.

***

<CardGroup cols={2}>
  <Card title="Modes Overview" icon="sliders" href="/modes/overview">
    The full preference vector CVD is one axis of, and what the no-JS floor covers.
  </Card>

  <Card title="High Contrast" icon="circle-up" href="/modes/high-contrast">
    The contrast solve that runs after CVD compensation.
  </Card>
</CardGroup>
