@ -562,6 +562,10 @@ bump if the authored `scales` shape changes; old 12-hex scales read the same.
## 11. Phase plan (each verifiable and mergeable alone)
### Phase 0 — Types + generator (build-time lib), no behavior change
> **Status (2026-07-20):** the generator + math shipped as `$color` (an
> isomorphic art). The config-authoring types (`ColorScaleSeed` /
> `ColorScaleSource` ) stay **deferred** — decision D5 (§15): runtime seed paths
> were the validation banks; the types land when a config authors seeds.
- Add `Oklch` , `ColorScaleSeed` , `ColorScaleSource` , widen `ThemeColorSet.scales` .
- `lib/themes/generate-scale.ts` (template morph) + OKLCH↔sRGB↔P3 conversion +
gamut-mapping. Not consumed yet — the existing scales stay verbatim.
@ -579,6 +583,9 @@ bump if the authored `scales` shape changes; old 12-hex scales read the same.
the color saturates. Test that the fallback always exists.
### Phase 3 — Migrate base + grafito to seeds
> **Status (2026-07-20): not done, not needed** — decision D2 (§15) keeps the
> base **verbatim ground-truth** ; the contrast audit guards it. The §9 cleanup
> rows it bundled (primary≡loss etc.) were already fixed in code separately.
- Re-author `themes/base.ts` and `_lib/grafito.ts` with `seed` . Fix
`primary≡loss` .
- **Verify** : per-step ΔE2000 vs the current hex within tolerance; visual
@ -678,9 +685,68 @@ sacrifices.
The RFC puts eidos **on par with Radix/M3 in color fidelity**
(OKLCH+P3+APCA+generator) while keeping what it already had **exclusively**
(TSC + the sema layer). That is the "next level".
(TSC + the sema layer). That is the "next level". One addition since: the
tonal-ramp text contrast is now **by construction + CI-guarded** (§15) — a
guarantee Radix (curation) and Tailwind/Chakra (no contrast logic) don't make,
though the mechanism is inheritance, not a solver.
---
**Last revision**: 2026-06-04. If anything here contradicts the code after
implementation, the code wins — open an issue to sync.
## 15. Stage 2 — tonal-ramp text contrast: inherited, not solved (2026-07-20)
The "contrast parity" initiative ([`next-features.md §1`](../next-features.md))
had two stages. Stage 1 (2026-07-19) measured the slot-pair promises and ratified
the doctrine ([`theming/reference.md §40`](../theming/reference.md) ·
[`changelog.md §44` ](../theming/changelog.md )). Stage 2 was planned as a
**by-construction generator that SOLVES each step's luminance** to satisfy the
pair table for any seed ([execution plan](../process/contrast-stage2-plan-2026-07.md),
verified by a 7-agent workflow; 6 user decisions locked 2026-07-20 — D1 `text·11`
soft band ≈APCA 60 · D2 measured-pass · D3 flip-only on-solid · D4 opt-in post-pass
· D5 runtime-first · D6 pair-table-as-data).
**Execution disproved the premise — no solver was needed.** The template morph
(§6) does not *compute* text contrast; it **inherits** it. Steps 11/12 (the text
inks) copy the donor's L-curve **verbatim** , and text contrast is dominated by
lightness — the sRGB gamut-map reduces chroma at **fixed L** , so contrast is
~**chroma-invariant**. The exact-anchor (§6 step 5) only moves step 9. Therefore
**every scale generated from a §40-compliant donor library inherits the ratified
text floors by construction.** The base itself is §40-compliant (D2 keeps it
verbatim ground-truth), so `applyColorScheme` / `generatePalette` — which morph
from the active theme's scales — inherit compliance too.
Measured across three banks (`scripts/contrast-audit.ts` + the CI guard):
| Bank | Hard gate `text-strong·12` ≥ 4.5 WCAG |
|---|---|
| Authored base (33× 2) | ✅ 198/198 · min 9.82:1 |
| Morph-generated, leave-one-out (33× 2) | ✅ 198/198 · min 9.87:1 |
| Out-of-distribution seeds (L 0.30– 0.88, C ≤ 0.24) | ✅ 0 failures · min 9.7:1 |
The luminance solver would have had nothing to resolve for realistic inputs, so
it was **dropped as speculative** . What shipped instead:
- **The ratified pair table as shared data** — `$color` → `CONTRAST_PAIRS`
(`arts/color/contrast-contract.ts`): the hard `text-strong·12` floor, the
`text·11` soft band (cap relational to `text-strong` , no magic number), and the
`border·7` exemption. One source consumed by the audit and any future checker.
- **The audit consumes it + a morph-generated regression bank**
(`scripts/contrast-audit.ts`; its pre-verdict `PAIRS` — which gated `text·11` at
a hard 4.5, contradicting the §40 verdict — is gone).
- **A CI guard that locks the inheritance** —
`eidos/lib/contrast-invariant.test.ts` (the `palette-invariant.test.ts` pattern):
fails if a future authored family or a generator change breaks the property. The
by-construction guarantee is now a test, not a claim.
**Consequences for this RFC's phase plan (§11):** Phase 3 (migrate base→seeds) is
**not done and not needed** — D2 keeps the base verbatim (the audit guards it).
Phase 0's config-authoring types (`ColorScaleSeed` / `ColorScaleSource` ) stay
**deferred** (D5) — the runtime seed paths that already exist were the validation
banks; the types land when a config authors seeds. The only scenario where a
luminance solver would earn its keep is a **custom, NON-§40-compliant donor
library** — which the framework does not ship, and which the CI guard would flag.
---
**Last revision**: 2026-07-20 (§15 Stage 2 resolution — contrast inherited, not
solved). If anything here contradicts the code after implementation, the code
wins — open an issue to sync.