You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/theming/gradient-finish.md

476 lines
28 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
title: Gradient Finish — Design and Architecture
type: reference
audience: human + agent
authority: E1 — the finish doctrine («la rampa huye de la tinta») and its decision record
status: current — v1 shipped 2026-07-15 (Button + Badge; anchored ramp + guard)
source: distilled from the 2026-07-14/15 design cycle (three analyses + coherence review + reference-framework research + measured rectification); operational plan at docs/process/gradient-finish-plan-2026-07.md
---
# Gradient Finish — Design and Architecture
> **Status: shipped v1 (2026-07-15).** `<Button color="affirm" gradient>` and
> `<Badge variant="solid" gradient>` paint a same-hue ramp DERIVED from the
> instance's own palette slots, ANCHORED to the ink's shadow side, scaled by
> ONE theme dial (`--gradient-finish-lift`). The a11y invariant is executable:
> [`gradient-finish-guard.test.ts`](../../src/uix/eidos/gradient-finish-guard.test.ts).
> Live lab: `web/routes/temas/gradientes`. Chronicle: [`changelog.md §42`](./changelog.md).
This chapter records **what** the gradient finish is, **why** every piece is
shaped the way it is, and **which alternatives were rejected with what
evidence**. It exists because the design went through a measured rectification
mid-flight — the kind of episode a reference framework documents rather than
buries (see D6).
---
## 1. The problem
Gradient-filled CTAs are one of the most common visual treatments in modern
product UI — and one of the least safely shipped. The reference landscape,
researched against official docs (2026-07-14):
| System | Mechanism | Ink over the gradient |
| --- | --- | --- |
| Mantine | `variant="gradient"` + `gradient={{from,to,deg}}` (open pair) | **Hardcoded white** — its own `autoContrast` engine is explicitly bypassed in the gradient branch |
| Bootstrap | `.bg-gradient` — one global white-alpha overlay, off by default | Inherits the solid's pairing *because* the overlay is same-hue and capped at 15 % |
| Chakra / Tailwind | background props / utilities, author-composed | Author's problem, no guarantee |
| Material 3 / Radix / Spectrum / Fluent / Apple | **Refuse gradients on components** | Their contrast guarantees are defined against ONE flat fill — the assumption a gradient breaks |
Two structural facts fall out of that table and shaped everything below:
1. **Nobody computes a legible ink over a multi-stop background.** They force
white, delegate, constrain the gradient to same-hue subtlety, or abstain.
2. The only architecture that *avoids* the ink problem (Bootstrap) does it by
making the gradient a **treatment over the base fill** rather than a color
in its own right — keeping the base solid's ink pairing valid.
UIX generalizes insight #2 into doctrine, with the guarantee made
constructive and executable instead of hoped-for.
---
## 2. D1 — A gradient is a FINISH (material), never a color identity
**Decision.** The gradient enters the system as a **finish of the saturated
fill**, opted into via a `gradient` prop that stamps `data-gradient`. It is
NOT a value of the `color` prop / `data-color` axis, NOT a variant, and NOT a
separate `background` prop.
**Why.** The `color` axis carries the strongest invariant in the theming
system: every value (9 roles, 33 donor scales, custom) resolves **all ten
palette slots mechanically** — fill, tints, border, text, hover, contrast —
and **every variant can express every value**. A gradient cannot honor that
contract by type: it is an `<image>`, valid only where backgrounds paint;
`border-color`, `color`, tint slots cannot take it. The first design (a
`color="gradient-aurora"` value riding the `data-color` axis like the 33
scales) was fully developed and then rejected in a coherence review because
every consequence of the type mismatch needed a patch:
- hand-authored companion tokens (`-ink`, `-edge`, `-soft`) bolted onto a
prop whose other values derive everything mechanically;
- variants that could not express the value (`variant="outline"` rendering
no trace of the chosen "color" — same pixels as the finish model, but there
they *broke* the axis contract; here they are honest inertness);
- a collision with the state layer (§38) needing special-casing;
- per-gradient worst-stop ink tooling per mode;
- the `data-on` surface-inversion machinery dragged in as a dependency.
The framework already has the correct taxonomy, and the finish slots into it:
| Axis | Question it answers |
| --- | --- |
| `color` | **who** the instance is (identity) |
| `variant` | **how it wears** the identity |
| `data-depth` / frost / **gradient finish** | **what material** its surface is |
| state layer | **how it responds** to the pointer |
Two precedents complete the argument. Mantine models gradient as a *variant*
only because its gradient is an **open** `{from,to}` pair that cannot live in
an enum; a treatment with a **derived** recipe doesn't need a variant branch —
and Mantine's variant branch is precisely where its ink guarantee dies. And
the named gradients (`--gradient-{name}`, the config "open cage") remain a
**separate decorative axis** (scenography, `Image`'s shimmer, the future
`Surface`), untouched by this feature — see §10.
**Rejected alternatives.** (a) color-axis value — type mismatch, patches
enumerated above; (b) variant — needs a resolver branch, duplicates the 6
variants × finish matrix, and follows Mantine into the ink trap; (c) separate
`bg`/`background` prop — two identity axes competing on one element
(`color="teal" bg="gradient-x"`: whose text? whose hover?), plus a new morfo
attr + forward + TSC axis for no gain.
---
## 3. D2 — The layered fill
**Decision.** A finished fill is **base + layers**:
```
background-color = the solid base (--_{c}-bg — unchanged, always a <color>)
background-image = the finish layer (--_{c}-fill-finish, generator-emitted)
```
**Why.** Keeping the base as a *color* in `background-color` and the finish
as an *image layer* dissolves four problems that plagued the color-value
design, by construction instead of by patch:
1. **`forced-colors`**: the UA strips every non-`url()` `background-image`
and force-paints `background-color` — a finished button degrades to
exactly today's solid button. Zero fallback tokens, zero HCM blocks.
(The initial analysis had this backwards — assuming the gradient would
survive; MDN-verified during the reference research and corrected.)
2. **The state layer (§38)** composes veils in `background-image`; base color
and finish never fight it for the same longhand value slot.
3. **Hover transition**: `transition: background …` interpolates the COLOR
layer (9→10) exactly as today; gradients don't interpolate and don't need
to.
4. **The mesh caveat** (a mesh token's trailing base color is invalid as
`background-image`) stops mattering for components — and mesh never enters
component fills anyway (§10).
**Alpha / transparency (audited 2026-07-15, by computed values).** The
layered fill is what makes transparency correct across the whole system:
- **Opaque identities** (every `solid` slot in the palette — all 9 roles and
33 scales) → the ramp/spread stops are opaque; a finished button is fully
opaque, matching today's solid. *(Measured: opaque teal → `oklch(…)` with
no alpha channel.)*
- **Alpha finishes are the point, not a problem.** The `aurora` named finish
is deliberately translucent — role blobs `color-mix(…, transparent)` fading
to `transparent` — layered over the identity's opaque solid base, so the
base shows through the gaps. *(Measured: blobs at α 0.62 over the solid.)*
- **Translucent tint slots** (`surface`/`track` = `a2`/`a3`) and the
`soft`/`surface` variants are **untouched** — the finish is inert on
non-solid, so their translucency is preserved by not being read.
- **`data-on` inks** are intentionally translucent (`color-mix(ink,
transparent)` for the secondary/muted/border hierarchy).
The ONE edge, and why it is a non-issue: feeding a *translucent* value into
the `solid` slot makes the anchor `color-mix` (toward opaque `#000`/`#fff`)
drift the alpha UP and non-uniformly (α 0.5 → 0.545 weak / 0.631 deep for the
ramp; 0.545 uniform for spread). But `solid` is the **opaque** variant by
definition — translucency lives in the tint slots the finish never touches —
and a translucent solid is not reachable through any component prop (only by
manually overriding `--{c}-palette-solid` inline). The base `background-color`
keeps its alpha regardless, so the element stays translucent overall. The
alpha drift is a consequence of an out-of-contract input, not a defect in the
intended path — documented here rather than left tacit.
---
## 4. D3 — The ramp derives from the instance's own palette slots
**Decision.** There is **no per-color gradient catalog**. The ramp is
composed in CSS from the slots the identity already resolves —
`--{c}-palette-solid` / `--{c}-palette-solid-hover` — via `color-mix(in
oklch, …)`, emitted once per opted-in recipe by `renderRecipeGradientFinish`
(`render-css.ts`).
**Why.** The THM-2 shared palette layer already routes **every** identity —
9 roles, 33 donor scales, `data-color-custom` raw brand colors — into those
per-instance slots. Deriving from them means the finish re-tints with roles,
scales, custom colors, themes and light/dark **automatically**, with zero new
rows in the shared layer and zero maintenance per color. The ink is the same
inherited `--{c}-palette-contrast` the flat fill uses. Non-interactive
recipes (Badge) declare no `solid-hover`; the deep stop falls back to `solid`
— the ramp still deepens via the anchor mix.
Interpolation is `in oklch` (perceptual) — consistent with the named-gradient
builder and with Tailwind v4's default; it keeps midpoint luminance smooth and
predictable, which the guard's sampling relies on.
---
## 5. D4 — Why the ramp endpoints are NOT other scale steps (9→7, 9→11)
**Decision.** The ramp never reaches for other steps of the 12-step scale.
Its endpoints live in the solid's own neighborhood (solid / solid-hover,
mixed toward the anchor).
**Why.** The obvious idea — "want a wider ramp? span two steps" — fails on
the scale's own semantics, verified against the generated theme values:
- The 12 steps are **functional, mode-relative slots**, not a luminance
ruler: 7 = border, 9/10 = solid, 11 = text. In light mode purple-11 is
`#8145b5` (ΔL ≈ 0.04 from solid — an even FLATTER ramp than 9→10); in dark
mode purple-11 is `#d19dff` — a **light** text tone: the 9→11 ramp inverts
direction and kills white ink. 9→7 dies symmetrically (7-light `#d1afec` is
a pastel: white ink ~1.9:1; 7-dark is darker than 9 — inverted again).
- The luminance cliff of the scale sits at 8→9 and 11→12, not inside the
solid region. In slot language the proposal reads "fill from *solid* to
*border*" or "to *text*" — the names themselves flag the category error.
- The **only mode-stable anchor** in the entire scale is the solid itself
(purple-9 is `#8e4ec6` in BOTH modes). Anything anchored elsewhere imports
per-mode direction flips and per-scale polarity exceptions.
This analysis is demonstrated live in the lab's «¿y por qué no saltar 2
pasos?» panel, with the real theme values.
---
## 6. D5 — ONE theme dial: `--gradient-finish-lift`
**Decision.** The finish intensity is **config data**:
`primitives.gradientFinish.lift` (default `26%`) → emitted as the
`--gradient-finish-lift` token. One dial scales every ramp in the catalog;
`0%` degenerates the ramp to ≈flat (the dial is also the global off-switch).
Per-instance override is free via the custom-property cascade; a **theme**
overrides it by declaring `gradientFinish.lift` in its `ThemeDefinition` —
the theme block re-emits the token at the same specificity, later in the
cascade, so light and dark can run different intensities (or `0%` to turn
the finish off per theme). The compound `gradient-finish-*` prefix reserves
the namespace against the named-gradient open cage exactly like
`--gradient-angle-*` does.
**Why.** This is the state-layer precedent (§38/§40) applied again: visual
magnitudes are **config data emitted by the generator**, never hand-written
CSS constants. A theme retunes one number; the whole catalog follows. The
alternative — per-kind or per-component knobs — multiplies surface without a
consumer, and the pure 9→10 ramp (no knob at all) was measured imperceptible
at button size in the interactive mockup that preceded implementation.
---
## 7. D6 — The ANCHORED ramp: «la rampa huye de la tinta»
This is the load-bearing decision, and the one that was **rectified under
measurement** mid-implementation. Both halves — the mistake and the fix —
are part of the record.
**The mistake (documented deliberately).** The first ratified form was a
global *lift toward white*: light end = `solid + lift`, deep end =
`solid-hover − lift/3`. It looked right on the mockup — which was calibrated
on **purple**, one of the highest-headroom colors in the palette (sampling
bias), and it shipped with its validation queued instead of gating. The probe
(84 combos = 9 roles + 33 scales × light/dark; floors mirroring
`on-solid.ts`: APCA |Lc| ≥ 60 ∧ WCAG ≥ 3) then measured:
- **52/84 combos broke the inherited ink at 26 %** at the ramp extremes
(37/84 even sampling only the text band);
- per-color safe ceilings: teal **1 %**, gray 15 %, dark-mode blues/greens
**0 %** — the scale's step 9 simply has **no headroom toward white** on
half the palette, because the on-solid floors are calibrated exactly there;
- the **global safe ceiling was 0 %** — no perceptible global lift exists.
Process lesson, recorded as doctrine: **measure before fixing a default; a
default calibrated visually on one color is a sampling artifact.** The guard
must gate the number, not trail it.
**The fix — the doctrine.** The ramp **anchors to the ink's shadow side**:
- **White-ink colors** (most roles/scales): anchor `#000`, strong stop DOWN —
the fill deepens from the solid into shade. The classic shaded CTA.
- **Dark-ink colors** (light solids: amber, yellow, lime, mint, sky…):
anchor `#fff`, strong stop UP — the fill lifts into highlight. Glossy.
Formally: `weak = mix(solid, anchor, lift/3)` under the label,
`strong = mix(solid-hover, anchor, lift)` falling away, with the gradient
angle flipping with the polarity (`180deg` / `0deg` via the existing
`--gradient-angle-*` tokens). Because **both stops move toward the ink's
safe side**, the label's contrast can only **improve** over the base solid
pairing that the component already ships. That is *constructive safety*:
- the dial is **unbounded** (probe: 0 regressions up to 40 %, the dial's UI
range);
- intensity is **uniform** across the whole palette — no per-color caps, no
colors where the feature silently vanishes;
- the physical reading is coherent: the light source sits on the label's
side; the material falls away toward shade.
**How the polarity resolves.** By the SAME flip the `contrast` slot already
rides — no second source of truth:
- per role, at theme time: `--color-{role}-finish-anchor` / `-finish-angle`
are emitted next to the role's contrast decision (`onSolidClearsFloors`);
- per donor scale, at build time: the THM-2 shared rows carry
`--palette-finish-anchor` / `-finish-angle` decided by the same
`lightSolidScales` membership that decides `palette-contrast`;
- per recipe, with the presence guard mirroring the palette forward: the
host block pins **primary's** anchor so a colorless nested instance never
inherits an ancestor's polarity over its own primary fill (the nesting
gap, same defense as THM-2).
**The guard.** [`gradient-finish-guard.test.ts`](../../src/uix/eidos/gradient-finish-guard.test.ts)
pins the invariant executable:
1. coverage — the full color axis (≥ 84 combos);
2. **no-regression** — every combo whose base solid clears the floors with
its own ink still clears them at any dial value ≤ 40 % (worst-sample of
the anchored ramp);
3. the pre-existing **flat-fail set is pinned** — `{role:risk(orange),
scale:cyan, scale:orange}` are mid-tone solids where NO single ink clears
APCA 60 even flat (an industry-wide on-solid reality, out of the finish's
scope); the set must never grow silently.
**Rejected alternatives, structurally.** (a) global lift + lower default —
ceiling is 0 %, no default exists; (b) per-color caps (`min(dial, cap)`) —
uniformity dies, the feature vanishes on teal/grays and to 0 on
orange/cyan; (c) computing a separate ramp ink per color — flat and finished
siblings would carry different inks for the same identity, and it reopens
the very ink-over-gradient problem the ecosystem cannot solve.
---
## 8. D7 — The generator emits the VAR; the recipe paints
**Decision.** `renderRecipeGradientFinish` emits only the ramp variable
(`--_{c}-fill-finish`, plus the anchor/angle pins) under
`[data-{c}][data-gradient]`. The **recipe** paints it on its saturated fill
(`[data-variant='solid']`) and re-asserts on `:hover`, next to its own hover
rule with its own guards.
**Why.** Button's hover is `[data-button]:hover:not([data-disabled]):not([data-loading])`
— specificity (0,4,0) — and uses the `background:` **shorthand**, which
resets the `background-image` longhand. A generic generator re-assert at
(0,3,0) silently loses; matching each recipe's guard shape from the generator
would couple it to every recipe's selector conventions. The split follows the
system's grain: *the generator fabricates from data; the recipe decides where
paint lands* — exactly how variants consume palette slots. The finish applies
to the saturated fill and is **inert** on soft/outline/ghost: no fill to
finish, and no broken contract, because `gradient` never claimed to be the
identity (contrast with the rejected color-value design, where the same
pixels violated the axis).
---
## 9. D8 — `data-gradient` is an eidos-only WRAPPER attr (not morfo-declared)
**Decision.** The attr is stamped by the eidos wrapper (Button: via the soma
provider's passthrough props; Badge: directly on its element) and does NOT
appear in any morfo declaration — the `data-variant` / `data-size` family.
**Why (found live, the bug is instructive).** Declaring it in the morfo (on
the `data-color-custom` precedent) made the attr silently vanish: a
morfo-declared attr resolves in the **soma provider's prop space**, where the
pure-visual `gradient` prop does not exist → the runtime emits `undefined`
for it → soma's `mergeProps(restProps, state.props)` gives the runtime's
`undefined` precedence and **clobbers the wrapper's stamp**. The corrected
rule of thumb, now doctrine: **the morfo declares an attr only when its
driving prop crosses the soma boundary** (`data-color-custom` qualifies —
Card's `colorCustom` is a soma prop; `gradient` does not). eidos-lint
classifies these as "wrapper attrs" (eidos-only) — that category *is* the
convention.
---
## 10. Boundaries — what the finish is NOT
- **Not the named gradients.** `--gradient-{name}` (config `gradients`, the
documented "open cage", `buildGradient` factories) stays a decorative /
scenography axis: `Image`'s shimmer today; the `Surface` primitive with
`gradient="aurora" on="light|dark"` in v2. Mesh materials live there —
never in component fills.
- **Not gradient text.** `TextGradient` clips a single `<image>` through
glyphs; disjoint mechanism, unchanged.
- **The subtree ink context is MINIMAL by design (D12).** `data-on` re-binds
the global content/border roles for plain content only; nested components
with their own tokens and portaled content are NOT re-inked — by
construction, and documented. The FULL surface-inversion system stays an
independent initiative (Card's solid variant needs it today, without
gradients).
- **Roadmap — COMPLETE (2026-07-15).** v1 ramp · v1.x (guard, Badge, theme
dial override) · v1.5 `spread` (D10) · v2.1 **`Surface`** · v2.2 **named
finishes** (D11) + **`on`/`data-on` minimal** (D12). Beyond the plan: the
full subtree-inversion initiative, per-component demo pages, and
model-based validation of named inks (see D11's honesty note).
### D11 — Named finishes: the open cage as a finish, with AUTHORED ink
`gradient="aurora"` (any name opted in via `gradientFinish.named`) paints an
open-cage gradient as the finish layer. Three structural points:
- **The base stays the identity's solid** (`background-color`, the layered
fill D2) — which is why the shipped `aurora` is pure alpha blobs with NO
trailing base color: it composes over ANY identity's solid and stays
`background-image`-valid (the mesh caveat dissolved by architecture, not
by exception).
- **The ink is AUTHORED, required, and themeable**: a multi-role image
cannot inherit a single derived ink — the config declares it
(`named: { aurora: { ink: 'var(--color-content-on-solid)' } }` → emitted
as `--gradient-{name}-ink`). It travels as the **`--_{c}-finish-ink`
var** and each recipe's SOLID slice consumes it with its contrast as the
fallback — the D7 split again (generator fabricates, recipe paints).
*Audited 2026-07-15*: the first shape (a direct `--_{c}-fg` override)
TIED Surface's own slice at (0,3,0) and left the winner to stylesheet
order — not a contract; the var indirection wins by existing, never by
specificity, and non-solid variants stay inert by never reading it. This
is the rejected design A salvaged as an EXPLICIT opt-in: the derived
kinds keep the constructive guarantee; the named kind trades it for
expressiveness and says so.
- **An UNDECLARED name degrades to the ramp** (measured): `gradient="foo"`
matches the presence block (which sets the ramp) and no named block
overrides it — graceful fallback, never a broken render.
- **Honesty note — validation**: the guard pins config sanity (named ⊆
gradients, ink present). Numeric worst-stop validation of an arbitrary CSS
gradient string is not implementable (stops unknown); it becomes possible
only for model-built gradients (`buildGradient`) — deferred, documented.
### D12 — `on`: the minimal subtree ink context
`<Surface on="dark">` stamps `data-on='dark'`; the foundation re-binds the
GLOBAL content roles for the subtree (`--color-content-primary` → the
on-solid ink; secondary/muted/border → oklch mixes of it; `'light'` uses the
dark on-solid-contrast ink). Verified live: a `muted` paragraph inside an
aurora Surface computes the context ink at 64 % — the Radix
`appearance`/MD3 `on-surface` move as custom-property re-binding. Hard
limits, by construction and documented at every consumer: nested components
resolve their OWN tokens; portaled content escapes ancestor inheritance.
Under `forced-colors` the context resolves to `CanvasText`/`GrayText`
(appended to the foundation's forced-colors block). No `color-scheme` on
purpose: it flips UA chrome only, and would mismatch inside an opposite
theme.
### D10 — The `spread` kind (v1.5): anchored constant-L hue rotation
`gradient="spread"` rotates the fill's hue ±`--gradient-finish-spread`
(default `30`, a **unitless** number: the relative-color `h` channel is a
`<number>` — `calc(h ± 30deg)` computes to `none`, measured in Chromium)
around the instance's solid, at constant L/C, horizontally. **Measured, not
assumed**: OKLCH L is *not* relative luminance — a pure constant-L rotation
broke grass at ±4° and gold at ±27°. The rescue is the doctrine itself
applied lightly: **both rotated stops take the ramp's weak anchor mix**
(`lift/3` toward the ink's shadow side), which buys back the hue-induced
luminance drift — 0 regressions up to ±45° across the 78 base-passing
combos, pinned in the guard. On achromatic identities (the gray family,
C ≈ 0) the rotation is imperceptible by nature: spread renders ≈flat there —
documented behavior, not a special case. Ink, border, hover: inherited,
unchanged. One doctrine, two kinds: *the ramp flees the ink strongly; the
spread flees it lightly while it plays with hue.*
---
## 11. Decision register
| # | Decision | Why (short) | Status |
| --- | --- | --- | --- |
| D1 | Gradient = **finish** (material), not a color value, variant, or bg prop | `<image>` cannot honor the color axis' derive-all-slots contract; taxonomy: identity × wearing × material × feedback; Mantine's variant = its ink trap | 2026-07-15 · standing |
| D2 | **Layered fill**: base `background-color` + finish `background-image` | forced-colors degrades free; state layer composes; hover transition intact; mesh caveat moot | standing |
| D3 | Ramp **derives from the instance's palette slots** (no catalog) | THM-2 rails: roles + 33 scales + custom re-tint automatically; ink inherited | standing |
| D4 | Endpoints never reach steps 7/11 | steps are functional & mode-relative (11-dark is light TEXT `#d19dff`); only the solid is mode-stable | standing |
| D5 | ONE dial `--gradient-finish-lift` = config data (`26%`, `0%` ≈ off) | state-layer §40 precedent; theme retunes one number; pure 9→10 measured imperceptible | standing |
| D6 | **Anchored ramp** — «la rampa huye de la tinta» (white ink → deepen ↓, dark ink → lift ↑) | measured: global lift-toward-white breaks 52/84 at 26 %, global ceiling 0 %; anchored = contrast can only improve, dial unbounded, 0 regressions ≤ 40 % | 2026-07-15 · standing (rectified same day it was measured) |
| D6b | Process rule: **measure before fixing a default**; guards gate, not trail | the 26 % default was calibrated visually on purple (top-headroom color) — sampling bias caught by the probe | doctrine |
| D7 | Generator emits the **var**; the **recipe paints** (+ hover re-assert) | recipe hover shorthand at (0,4,0) resets the longhand; generator can't know per-recipe guards; matches variant-consumption grain | standing |
| D8 | `data-gradient` = eidos-only **wrapper attr**; morfo declares an attr only if its prop crosses soma | morfo-declared attr resolved in soma's prop space → `undefined` → `mergeProps` clobbers the stamp (found live) | doctrine |
| D9 | Pre-existing flat-fail set (cyan/orange mid-tones) is **pinned**, not fixed here | no ink clears APCA 60 on those solids even flat — on-solid scope, industry-wide; the guard forbids silent growth | standing |
| D10 | `spread` kind = constant-L hue rotation **with the weak anchor mix** on both stops; the spread token is **unitless** | pure constant-L broke grass ±4°/gold ±27° (L ≠ luminance, measured); anchored: 0 fails ≤ ±45°; RCS `h` is a `<number>` — `deg` computes `none` (measured in Chromium); ≈flat on grays (C≈0), documented | 2026-07-15 · standing |
| D11 | Named finishes = open-cage gradients opted in via `gradientFinish.named`, each with **required authored ink**; base stays the identity's solid | a multi-role image can't inherit a derived ink; alpha-blob materials need no trailing base (layered fill) → `background-image`-valid by architecture; numeric validation only possible for model-built gradients (deferred, documented) | 2026-07-15 · standing |
| D12 | `on` prop → `data-on` re-binds global content roles for the subtree; MINIMAL by design | plain content re-inks (verified live: muted = context ink @64 %); nested components + portals excluded by construction; forced-colors → `CanvasText`; no `color-scheme` (UA chrome only) | 2026-07-15 · standing |
---
## 12. Artifacts
| What | Where |
| --- | --- |
| Doctrine (standing summary) | [`theming/reference.md §39`](./reference.md) |
| Chronicle (dated, incl. the rectification) | [`theming/changelog.md §42`](./changelog.md) |
| Operational plan + phase queue | [`docs/process/gradient-finish-plan-2026-07.md`](../process/gradient-finish-plan-2026-07.md) |
| Generator | `src/uix/eidos/lib/render-css.ts` — `renderRecipeGradientFinish`, `GRADIENT_FINISH_COMPONENTS` gate; anchor/angle emission in the role loop + THM-2 shared rows |
| Config + default | `src/uix/eidos/lib/config-types.ts` (`gradientFinish`) · `lib/primitives/static.ts` (`STATIC_GRADIENT_FINISH`) |
| Consumers (finish gate) | `components/button/*` · `components/badge/*` · `components/surface/*` (prop `gradient`, paint rules) |
| The themeable canvas | `components/surface/*` — Box + treatment (v2.1); ficha with its own Decisiones/Gaps |
| **Executable invariant** | `src/uix/eidos/gradient-finish-guard.test.ts` — coverage / no-regression ≤ 40 % / pinned flat-fail set |
| Live lab (9 cases, real components, real token) | `web/routes/temas/gradientes` |

Powered by TurnKey Linux.