diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx
index 5ec7e507..e5a31d15 100644
--- a/apps/docs/content/docs/index.mdx
+++ b/apps/docs/content/docs/index.mdx
@@ -33,7 +33,7 @@ spacing, radii, and depth behind every component.
Learn what an identity owns and when to author your own.
- Use semantic surfaces and intent roles instead of palette values.
+ Use semantic surfaces and role-based colours instead of palette values.
Follow the system setting or force light and dark scopes.
diff --git a/apps/docs/content/docs/overview/color.mdx b/apps/docs/content/docs/overview/color.mdx
index ec874623..cc759bba 100644
--- a/apps/docs/content/docs/overview/color.mdx
+++ b/apps/docs/content/docs/overview/color.mdx
@@ -1,6 +1,7 @@
---
title: Colour
-description: Use semantic surfaces, content roles, and intents instead of palette values.
+description:
+ Use semantic surfaces, content roles, and the shared role contract instead of palette values.
---
Luke UI names colours by their job. A theme maps those roles to values for each identity and colour
@@ -28,22 +29,58 @@ import { vars } from '@luke-ui/react/theme';
Choose the role that describes the element's purpose. Do not copy a resolved colour into application
CSS, because it will not adapt when the identity or colour mode changes.
-## Intent colours
+## Semantic roles
-Intent roles cover `neutral`, `accent`, `danger`, `info`, `success`, and `warning`. `neutral`,
-`accent`, and `danger` are action intents: they render hover and pressed states across `subtle` and
-`solid` surfaces, plus matching text, border, and on-solid values where needed. `info`, `success`,
-and `warning` are static feedback intents: they render only a `subtle` surface, `border`, and
-`text`, with no hover, pressed, solid, or on-solid state. Components choose these values for their
-own states. Custom UI can use the same public roles.
+Six roles cover the meanings a component needs beyond plain surfaces and text: `neutral`, `accent`,
+`info`, `success`, `warning`, and `danger`. Every role offers the same capabilities, so a role's
+meaning never limits what it can do:
+
+- `neutral`: no accent or status meaning, distinct from ordinary surfaces.
+- `accent`: preferred, selected, or emphasised. Not a promise of a brand colour.
+- `info`: noteworthy factual context.
+- `success`: completed, valid, or a positive result.
+- `warning`: needs attention but has not failed and is not dangerous.
+- `danger`: harm, failure, invalidity, or a critical condition.
+
+Components choose the role that fits their own tone or state. Custom UI can use the same six roles.
+
+### Background and foreground
+
+Each role has a `subtle` and a `solid` background ramp, and each ramp has `rest`, `hover`, and
+`pressed` states:
+
+```tsx
+import { vars } from '@luke-ui/react/theme';
+
+vars.color.background.warning.subtle.rest;
+vars.color.background.warning.solid.hover;
+```
+
+`rest` is an explicit state rather than an implied default. A token path cannot be both a string
+leaf and the parent of `hover` and `pressed`, so resting values need their own name. `solid.pressed`
+deliberately reuses the `solid.hover` colour — there is no third, deeper solid rung. A component
+carries a pressed look through depth, finish, or a transform instead of inventing another solid
+colour.
+
+Foreground gives each role a resting and a stronger interactive colour, plus a colour guaranteed to
+read against that role's solid backgrounds:
+
+```tsx
+vars.color.foreground.warning.rest;
+vars.color.foreground.warning.hover;
+vars.color.foreground.warning.onSolid;
+```
+
+There is no foreground `pressed`. A press is carried by the background ramp and other cues, so text
+and icons reuse `hover` for a pressed control.
```tsx
import { vars } from '@luke-ui/react/theme';
Payment details need review.
@@ -51,11 +88,22 @@ import { vars } from '@luke-ui/react/theme';
```
+### Borders
+
+Each role also has one state-free border, for example `vars.color.border.warning`. The token
+describes meaning; the component decides when to apply it, such as only while invalid or as a
+permanent outline.
+
+A semantic border is measured for contrast but not guaranteed to reach 3:1 — see
+[contrast validation](/theming/authoring#contrast-validation). Never rely on one alone to
+communicate a required state such as selected, invalid, or checked. Pair it with text, an icon, or
+another cue that is separately contrast-gated.
+
## Disabled and loading states
Use the component's disabled or loading API when it has one. Those states account for interaction
@@ -63,6 +111,35 @@ and accessibility as well as colour. Disabled is a state, not a palette: interac
with reduced opacity, and only field text recolors, through the public `text.disabled` role. The
public `loadingSkeleton` role is available when you are building an equivalent custom element.
+## Migrate from `color.intent`
+
+`color.intent` was the previous asymmetric contract. `neutral`, `accent`, and `danger` rendered a
+full interactive ramp, while `info`, `success`, and `warning` rendered only a static subtle kit. It
+has been removed: there are no `color.intent` paths and no `--luke-color-intent-*` variables left to
+fall back on. If you are upgrading from a release that had them, this table is the old-to-new
+reference — move each usage to its `color.background` / `color.foreground` / `color.border`
+equivalent:
+
+| Old path | New path |
+| ------------------------------------------- | ---------------------------------------- |
+| `color.intent..surface.subtle` | `color.background..subtle.rest` |
+| `color.intent..surface.subtleHover` | `color.background..subtle.hover` |
+| `color.intent..surface.subtlePressed` | `color.background..subtle.pressed` |
+| `color.intent..surface.solid` | `color.background..solid.rest` |
+| `color.intent..surface.solidHover` | `color.background..solid.hover` |
+| `color.intent..surface.solidPressed` | `color.background..solid.pressed` |
+| `color.intent..text` | `color.foreground..rest` |
+| `color.intent..textHover` | `color.foreground..hover` |
+| `color.intent..onSolid` | `color.foreground..onSolid` |
+| `color.intent..border` | `color.border.` |
+
+CSS variable names followed the same shape, so `--luke-color-intent-danger-surface-solid-hover`
+became `--luke-color-background-danger-solid-hover`.
+
+The current contract also carries capabilities `color.intent` never had: a foreground and border for
+`neutral`, and interactive backgrounds, hover foregrounds, and on-solid foregrounds for `info`,
+`success`, and `warning`.
+
## Continue learning
diff --git a/apps/docs/content/docs/overview/styling.mdx b/apps/docs/content/docs/overview/styling.mdx
index ac42625d..a37e5e8c 100644
--- a/apps/docs/content/docs/overview/styling.mdx
+++ b/apps/docs/content/docs/overview/styling.mdx
@@ -163,7 +163,7 @@ selectors or implementation attributes.
Use Box and Sprinkles for responsive structure.
- Choose semantic surfaces and intent roles for custom UI.
+ Choose semantic surfaces and roles for custom UI.
Browse every public semantic CSS variable.
diff --git a/apps/docs/content/docs/overview/theme.mdx b/apps/docs/content/docs/overview/theme.mdx
index e7d97b90..8e1c75c8 100644
--- a/apps/docs/content/docs/overview/theme.mdx
+++ b/apps/docs/content/docs/overview/theme.mdx
@@ -66,7 +66,7 @@ generated palette, or component recipe selectors.
Compare identity, colour mode, custom themes, and public tokens.
- Learn the semantic roles behind surfaces, content, and intents.
+ Learn the semantic roles behind surfaces, content, and borders.
Use the type scale supplied by the active theme.
diff --git a/apps/docs/content/docs/theming/authoring.mdx b/apps/docs/content/docs/theming/authoring.mdx
index 5419d500..c214da7c 100644
--- a/apps/docs/content/docs/theming/authoring.mdx
+++ b/apps/docs/content/docs/theming/authoring.mdx
@@ -64,35 +64,40 @@ only, and one category is not checked.
### Guaranteed at 4.5:1
-Every text/surface pair the compiler emits is a hard gate: `color.text.primary` and
-`color.text.secondary` against all four elevation surfaces (`canvas`, `recessed`, `floating`,
-`overlay`), `color.text.primary` against the neutral subtle trio, each action or border/text
-intent's `text` (and accent's `textHover`) against the base surfaces and its own subtle trio,
-feedback intents' `text` against the base surfaces and their subtle surface, and every intent's
-`onSolid` text against its solid, solidHover, and solidPressed surfaces.
+Every text/surface pair the compiler emits is a hard gate, 86 checks in total:
+
+- `color.text.primary` and `color.text.secondary` against all four elevation surfaces (`canvas`,
+ `recessed`, `floating`, `overlay`): 8 checks.
+- For each of the six semantic roles (`neutral`, `accent`, `info`, `success`, `warning`, `danger`),
+ its foreground `rest` and `hover` against `canvas`, `recessed`, and that role's own `subtle` rest,
+ hover, and pressed backgrounds: 60 checks.
+- Each role's `onSolid` foreground against its `solid` rest, hover, and pressed backgrounds: 18
+ checks.
### Guaranteed at 3:1
-Two non-text pairs are also hard gates: `color.border.focus` and `color.border.control` must both
-reach 3:1 against the canvas and recessed surfaces. `border.control` is a dedicated solved colour
-for this boundary, not a scale-step alias that happens to pass.
+Four more pairs are hard gates, at the WCAG 2.2 non-text ratio: `color.border.focus` and
+`color.border.control`, each against the canvas and recessed surfaces. `border.control` is a
+dedicated solved colour for this boundary, not a scale-step alias that happens to pass. That brings
+the total to 90 hard gates across both ratios.
If any hard gate fails, `defineTheme` throws `ThemeContrastError`. Its `failures` array identifies
each colour mode and token pair, so the stylesheet is never written with an invalid combination.
### Measured only, cannot fail the build
-The five `color.intent.*.border` pairs (`accent`, `danger`, `info`, `success`, `warning`) are
-checked against the base surfaces at 3:1, but only as advisory diagnostics. A miss is recorded and
-reported, but never thrown. These borders use the same subtle scale step as ordinary separators, so
-an author can choose a neutral character whose intent borders read as gentle separators rather than
-assertive boundaries. Diagnostics record whether each check is a hard gate or advisory-only
-(`ContrastCheck.hard`), so tooling can tell the two apart without guessing from the token path.
+Each role's border (`color.border.neutral`, `color.border.accent`, and so on for all six roles) is
+checked against the canvas and recessed surfaces at 3:1, but only as advisory diagnostics — 12
+checks. A miss is recorded and reported, but never thrown. These borders use the same subtle scale
+step as ordinary separators, so an author can choose a neutral character whose role borders read as
+gentle separators rather than assertive boundaries. Diagnostics record whether each check is a hard
+gate or advisory-only (`ContrastCheck.hard`), so tooling can tell the two apart without guessing
+from the token path.
-Because these pairs are not guaranteed to reach 3:1, do not rely on an intent border alone to
-communicate an error, warning, or success state. Pair it with text, an icon, or a
-guaranteed-contrast surface change so the state is still legible for a theme whose intent border
-happens to measure under 3:1.
+Because these pairs are not guaranteed to reach 3:1, never rely on a role border alone to
+communicate a required state such as selected, invalid, or checked. Pair it with text, an icon, or a
+guaranteed-contrast surface change so the state stays legible for a theme whose role border happens
+to measure under 3:1.
### Never checked
@@ -103,13 +108,13 @@ separator, never as the sole signal for state or meaning.
Separately from contrast validation, `compileTheme` (and so `defineTheme`) can throw
`ThemeGenerationError` when a role that must guarantee on-solid text cannot reach an accessible
-solid colour. For a single-value accent, `defineTheme` holds the hue and chroma while searching for
-a lightness that clears the same on-solid gate the generator enforces internally. An explicit
-`{ light, dark }` accent skips that adaptation and is used verbatim for each side. If its authored
-lightness sits in an on-solid dead zone (no near-white or near-black text reaches 4.5:1 against it),
-`compileTheme` throws `ThemeGenerationError` naming the failing role and mode. Author an explicit
-`{ light, dark }` accent when you need exact control over its lightness. Otherwise, author a single
-value and let `defineTheme` adapt it.
+solid colour. Every one of the six roles is gated this way. For a single-value accent, `defineTheme`
+holds the hue and chroma while searching for a lightness that clears the same on-solid gate the
+generator enforces internally. An explicit `{ light, dark }` accent skips that adaptation and is
+used verbatim for each side. If its authored lightness sits in an on-solid dead zone (no near-white
+or near-black text reaches 4.5:1 against it), `compileTheme` throws `ThemeGenerationError` naming
+the failing role and mode. Author an explicit `{ light, dark }` accent when you need exact control
+over its lightness. Otherwise, author a single value and let `defineTheme` adapt it.
## Next steps
diff --git a/apps/docs/content/docs/theming/token-reference.mdx b/apps/docs/content/docs/theming/token-reference.mdx
index fd1d156a..c7b5340f 100644
--- a/apps/docs/content/docs/theming/token-reference.mdx
+++ b/apps/docs/content/docs/theming/token-reference.mdx
@@ -4,8 +4,8 @@ description: Public semantic CSS variables for building custom Luke UI elements.
---
`vars` is Luke UI's typed public token contract. Each path resolves to a stable `--luke-*` CSS
-variable. For example, `vars.color.intent.danger.surface.solidHover` resolves to
-`var(--luke-color-intent-danger-surface-solid-hover)`.
+variable. For example, `vars.color.background.danger.solid.hover` resolves to
+`var(--luke-color-background-danger-solid-hover)`.
Use these variables when a component API or layout utility cannot express a custom element. They
adapt to the active identity and colour mode. Generated palette values, component selectors, and
@@ -20,8 +20,8 @@ other implementation details are not part of the public styling API.
/>
Choose tokens by purpose. A card might use `surface.floating`, `radius.surface`, and
-`depth.resting`. A warning message might use the warning intent's subtle surface and text. Avoid
-carrying resolved colour values into application CSS.
+`depth.resting`. A warning message might use the warning role's subtle background and foreground.
+Avoid carrying resolved colour values into application CSS.
## Tokens
diff --git a/apps/docs/src/examples/box/responsive-layout.tsx b/apps/docs/src/examples/box/responsive-layout.tsx
index 3209d819..e611bf26 100644
--- a/apps/docs/src/examples/box/responsive-layout.tsx
+++ b/apps/docs/src/examples/box/responsive-layout.tsx
@@ -19,7 +19,7 @@ function Item() {
justifyContent="center"
padding="400"
style={{
- backgroundColor: vars.color.intent.neutral.surface.solid,
+ backgroundColor: vars.color.background.neutral.solid.rest,
flex: 1,
minBlockSize: '4rem',
}}
@@ -27,7 +27,7 @@ function Item() {
Item
diff --git a/apps/docs/src/examples/overview/intent-colours.tsx b/apps/docs/src/examples/overview/intent-colours.tsx
deleted file mode 100644
index 325399b5..00000000
--- a/apps/docs/src/examples/overview/intent-colours.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Box } from '@luke-ui/react/box';
-import { Button } from '@luke-ui/react/button';
-import { Text } from '@luke-ui/react/text';
-import { vars } from '@luke-ui/react/theme';
-import { useState } from 'react';
-
-const intents = {
- neutral: {
- border: vars.color.border.decorative,
- label: 'Neutral',
- surface: vars.color.intent.neutral.surface.subtle,
- text: vars.color.text.primary,
- },
- accent: {
- border: vars.color.intent.accent.border,
- label: 'Accent',
- surface: vars.color.intent.accent.surface.subtle,
- text: vars.color.intent.accent.text,
- },
- success: {
- border: vars.color.intent.success.border,
- label: 'Success',
- surface: vars.color.intent.success.surface.subtle,
- text: vars.color.intent.success.text,
- },
- info: {
- border: vars.color.intent.info.border,
- label: 'Info',
- surface: vars.color.intent.info.surface.subtle,
- text: vars.color.intent.info.text,
- },
- warning: {
- border: vars.color.intent.warning.border,
- label: 'Warning',
- surface: vars.color.intent.warning.surface.subtle,
- text: vars.color.intent.warning.text,
- },
- danger: {
- border: vars.color.intent.danger.border,
- label: 'Danger',
- surface: vars.color.intent.danger.surface.subtle,
- text: vars.color.intent.danger.text,
- },
-} as const;
-
-type Intent = keyof typeof intents;
-
-export default function IntentColoursExample() {
- const [intent, setIntent] = useState('info');
- const selectedIntent = intents[intent];
-
- return (
-
-
- {(Object.keys(intents) as Array).map((option) => (
-
- ))}
-
-
-
- {selectedIntent.label}
-
-
-
- );
-}
diff --git a/apps/docs/src/examples/overview/role-colours.tsx b/apps/docs/src/examples/overview/role-colours.tsx
new file mode 100644
index 00000000..bf997435
--- /dev/null
+++ b/apps/docs/src/examples/overview/role-colours.tsx
@@ -0,0 +1,66 @@
+import { Box } from '@luke-ui/react/box';
+import { Button } from '@luke-ui/react/button';
+import { Text } from '@luke-ui/react/text';
+import { vars } from '@luke-ui/react/theme';
+import { useState } from 'react';
+
+const roles = {
+ neutral: 'Neutral',
+ accent: 'Accent',
+ info: 'Info',
+ success: 'Success',
+ warning: 'Warning',
+ danger: 'Danger',
+} as const;
+
+type Role = keyof typeof roles;
+
+export default function RoleColoursExample() {
+ const [role, setRole] = useState('info');
+
+ return (
+
+
+ {(Object.keys(roles) as Array).map((option) => (
+
+ ))}
+
+
+
+
+ Subtle
+
+
+
+
+ Solid
+
+
+
+
+ );
+}
diff --git a/apps/docs/src/lib/token-reference-generator.test.ts b/apps/docs/src/lib/token-reference-generator.test.ts
index 9d222447..1a8cc47f 100644
--- a/apps/docs/src/lib/token-reference-generator.test.ts
+++ b/apps/docs/src/lib/token-reference-generator.test.ts
@@ -5,7 +5,9 @@ test('generates documented leaf token mappings from the public contract', () =>
const reference = generateTokenReference();
expect(reference).toContain("'color.surface.canvas': 'var(--luke-color-surface-canvas)';\n");
- expect(reference).toContain('Semantic colours for surfaces, content, borders, loading');
+ expect(reference).toContain(
+ 'Semantic colours for surfaces, content, borders, and loading, plus the six shared semantic roles',
+ );
expect(reference).toContain("'motion.easing.exit': 'var(--luke-motion-easing-exit)';\n");
expect(reference).not.toContain('MapLeafNodes');
});
diff --git a/apps/docs/src/styles/app.css b/apps/docs/src/styles/app.css
index d414ff60..185ea756 100644
--- a/apps/docs/src/styles/app.css
+++ b/apps/docs/src/styles/app.css
@@ -21,11 +21,11 @@
--color-fd-card: var(--luke-color-surface-resting);
--color-fd-card-foreground: var(--luke-color-text-primary);
--color-fd-border: var(--luke-color-border-decorative);
- --color-fd-primary: var(--luke-color-intent-accent-surface-solid);
- --color-fd-primary-foreground: var(--luke-color-intent-accent-on-solid);
+ --color-fd-primary: var(--luke-color-background-accent-solid-rest);
+ --color-fd-primary-foreground: var(--luke-color-foreground-accent-on-solid);
--color-fd-secondary: var(--luke-color-surface-raised);
--color-fd-secondary-foreground: var(--luke-color-text-primary);
- --color-fd-accent: var(--luke-color-intent-neutral-surface-subtleHover);
+ --color-fd-accent: var(--luke-color-background-neutral-subtle-hover);
--color-fd-accent-foreground: var(--luke-color-text-primary);
--color-fd-ring: var(--luke-color-border-focus);
--color-fd-overlay: var(--luke-color-surface-overlay);
diff --git a/docs/STYLING.md b/docs/STYLING.md
index 0e04ba0d..24ca82c0 100644
--- a/docs/STYLING.md
+++ b/docs/STYLING.md
@@ -28,8 +28,8 @@ element. Neither step injects styles at runtime.
- `theme/foundation.ts`: the internal typed theme-foundation shape `defineTheme` normalises into and
the curated colour, radius, and typography defaults.
- `theme/color.ts`: OKLCH colour math, sRGB gamut mapping, and WCAG contrast.
-- `theme/contrast-policy.ts`: the WCAG ratios, solver headroom and search step, and intent role
- groups the generator, the compiler's validation matrix, and the semantic map all read.
+- `theme/contrast-policy.ts`: the WCAG ratios, solver headroom and search step, and the canonical
+ semantic role list the generator, the compiler's validation matrix, and the semantic map all read.
- `theme/scale.ts`: the private 12-step family generator (`generateFamily`), including the
constrained step-9 solid-anchor search, the per-role capability guarantees, and
`passesOnSolidGate`, the on-solid accessibility gate.
@@ -57,11 +57,11 @@ token pair when a generated pair misses WCAG 2.2 AA contrast. A single-value acc
adapted per mode through a lightness search; it throws when no lightness in the vibrant band is
accessible. The raw `ThemeFoundation` object and `buildTheme` are internal only.
-Every colour token is generated from a private 12-step scale per role (neutral, accent, danger,
-info, success, warning) plus a mode-aware elevation surface set, then mapped onto the public colour
-contract. See [THEME_COLOUR_GENERATION.md](THEME_COLOUR_GENERATION.md) for the pipeline, the border
-and accent contrast policies, and what changed when this generator replaced the original per-token
-solver.
+Every colour token is generated from a private 12-step scale per role (neutral, accent, info,
+success, warning, danger) plus a mode-aware elevation surface set, then mapped onto the public
+colour contract. Every role gets the same background, foreground, on-solid, and border capabilities.
+See [THEME_COLOUR_GENERATION.md](THEME_COLOUR_GENERATION.md) for the pipeline, the border and accent
+contrast policies, and what changed when this generator replaced the original per-token solver.
The semantic contract includes `font.100` through `font.900` size steps. Each step groups its font
size, line height, letter spacing, and per-font Capsize trims so components cannot combine unrelated
diff --git a/docs/THEME_COLOUR_GENERATION.md b/docs/THEME_COLOUR_GENERATION.md
index 1855e244..73b5c526 100644
--- a/docs/THEME_COLOUR_GENERATION.md
+++ b/docs/THEME_COLOUR_GENERATION.md
@@ -10,10 +10,12 @@ Per colour mode, `compileTheme` (in `build-theme.ts`):
1. Resolves the source colours and the canvas anchor (`background`, split from `neutral`'s
hue/chroma character — see `define-theme.ts`).
-2. Generates six private 12-step OKLCH families (`neutral`, `accent`, `danger`, `info`, `success`,
- `warning`) with `scale.ts`'s `generateFamily`. Each family carries steps 1-12 plus a `contrast`
- on-solid colour, and guarantees only the capabilities its role's `FamilyRequirements` declares
- (see the capability matrix in `scale.ts`).
+2. Generates six private 12-step OKLCH families (`neutral`, `accent`, `info`, `success`, `warning`,
+ `danger`) with `scale.ts`'s `generateFamily`. Each family carries steps 1-12 plus a `contrast`
+ on-solid colour. Every role now guarantees the same capabilities (see `scale.ts`'s
+ `FAMILY_REQUIREMENTS`) — the public contract gives all six roles identical background,
+ foreground, on-solid, and border slots, so there is no role that can opt out of a capability
+ another role emits.
3. Derives the mode-aware elevation surfaces (`canvas`/`recessed`/`floating`/`overlay`) with
`elevation.ts`'s `generateSurfaces`. `surfaces.canvas` is always exactly the resolved
`background` — canvas IS the background, not a derived value.
@@ -75,9 +77,9 @@ advisory group:
the sole resting boundary of a form control, so it cannot use the softer step-7 aesthetic.
- `color.border.decorative` is not checked. It is a deliberately subtle, Radix-style separator below
3:1.
-- Every intent border (`color.intent.*.border`) is an advisory 3:1 check. A miss does not gate the
- build. `color.border.focus` is the other hard gate. The keyboard-focus ring stays build-time
- validated at ≥3:1.
+- Every semantic role border (`color.border.`, all six roles) is an advisory 3:1 check. A miss
+ does not gate the build. `color.border.focus` is the other hard gate. The keyboard-focus ring
+ stays build-time validated at ≥3:1.
Component invariant (tracked separately,
[#247](https://github.com/lukebennett88/luke-ui/issues/247)): any component that uses an advisory
@@ -103,8 +105,8 @@ Accent adaptation is forgiving but never sacrifices the AA on-solid guarantee:
readable text. It asks whether the near-white or near-black on-solid colour the generator would
choose clears the AA text ratio plus the search headroom across both solid states the engine emits:
step 9 and its step-10 hover. There is no third, deeper pressed state to test.
-`surface.solidPressed` reuses step 10 and carries the press through depth, finish, and transform
-instead.
+`background..solid.pressed` reuses step 10 and carries the press through depth, finish, and
+transform instead.
`defineTheme`'s `adaptAccent` pre-conditioner calls that same function rather than keeping its own
copy. That gives two guarantees:
@@ -118,14 +120,16 @@ generator's tone-faithful window. It can rescue accents, such as a mid-lightness
generator alone would report as unsatisfiable.
`contrast-policy.ts` declares the shared thresholds: the 4.5 text ratio, the 3:1 non-text ratio, the
-search headroom, and the search step. It also declares the intent role groups used by both the
-validation matrix and the semantic map. Previously, separate role lists allowed a role added only to
-the map to emit an ungated colour, while a role added only to the compiler threw an internal error.
+search headroom, and the search step. It also declares `SEMANTIC_ROLES`, the one canonical role list
+used by family generation, the capability matrix, the semantic map, and the validation matrix.
+Previously, separate role lists allowed a role added only to the map to emit an ungated colour,
+while a role added only to the compiler threw an internal error. One list makes both sides move
+together.
## `loadingSkeleton`
-`color.loadingSkeleton` maps to the neutral family's step 7 (previously a lighter neutral step), for
-better perceptibility of the loading state against typical surfaces.
+`color.loadingSkeleton` maps to the neutral family's step 8, for better perceptibility of the
+loading state against typical surfaces.
## Alpha is deferred
diff --git a/packages/@luke-ui/react/src/icon/icon.stories.tsx b/packages/@luke-ui/react/src/icon/icon.stories.tsx
index a0b8aca1..726a4ee0 100644
--- a/packages/@luke-ui/react/src/icon/icon.stories.tsx
+++ b/packages/@luke-ui/react/src/icon/icon.stories.tsx
@@ -20,8 +20,8 @@ const baseArgs = {
const iconSizes: Array> = ['xsmall', 'small', 'medium', 'large'];
const colors = {
- accent: vars.color.intent.accent.text,
- danger: vars.color.intent.danger.text,
+ accent: vars.color.foreground.accent.rest,
+ danger: vars.color.foreground.danger.rest,
primary: vars.color.text.primary,
secondary: vars.color.text.secondary,
} as const satisfies Partial, string>>;
diff --git a/packages/@luke-ui/react/src/icon/icon.visual.test.tsx b/packages/@luke-ui/react/src/icon/icon.visual.test.tsx
index 743c0e17..cc51a88a 100644
--- a/packages/@luke-ui/react/src/icon/icon.visual.test.tsx
+++ b/packages/@luke-ui/react/src/icon/icon.visual.test.tsx
@@ -48,7 +48,7 @@ test('sizes and glyphs', async () => {
for (const appearance of visualAppearances) {
test(`semantic content inheritance: ${appearance.theme} ${appearance.mode}`, async () => {
const scene = renderVisual(
-