uix(chart): temable — 9 % → 49 %, y la ficha pedía 71 claves de las que 20 llevaban el mismo valor

22 claves nuevas fusionadas en el bloque existente (17 → 39). Censo chart
9 % → **49 %**; global **56 % → 57 %** (2.638 → 2.675 públicos; <20 % 19 → 18).

LO QUE DECIDIÓ LA MEDICIÓN, no la ficha:

Las 84 filas fuera de alcance colapsaban a **26 valores distintos** — 9×
`content-secondary`, 9× `content-primary`, 9× `100%`, 6× `font-size-sm`, 6×
`font-size-xs`, 6× `font-weight-medium`, 5× `surface-default`, 4× `radius-sm`.
Nombrar por parte habría dado cinco claves para el pelo que separa marcas y
nueve para la tinta de anotación: la inflación de `spinner` con otra cara.

**El texto no recibe tokens de chart.** Toma su ROL DE TAMAÑO de los estilos con
nombre del canon —`caption` (xs) para las anotaciones, `label` (sm) para el
texto de cromo— y su tinta y su peso de las ranuras del sistema. Acuñar
`--chart-label-font-size` sería tipografía por componente, prohibido el mismo
día para text-effects. Precedente de consumir la capa: `code-block`, `kbd`,
`announce`. Doce declaraciones cambian de `var(--font-size-xs|sm)` a
`var(--style-caption|label-font-size)`, mismo valor.

**La receta sólo posee la tinta de lo que NO es texto**, cinco claves y cada una
un concepto que el CSS repite: `axis-fg` (eje + rejilla smith + conector del
funnel), `grid-fg` (rejilla + radar), `separator-fg` (el pelo de superficie
entre marcas contiguas: porción, punto activo, punto smith, pétalo, segmento),
`smith-boundary-fg` y `track-bg` (raíl del bar-list y del gauge). Más 17 de
geometría de cromo (gap, los pares row/column-gap, swatch, track, bar-segment).

**Incumplimiento heredado, corregido**: el CSS leía la coordenada CRUDA
`var(--control-height-sm, var(--space-7))` —lo que `recipe-css-contract`
prohíbe— con un respaldo muerto. El contrato guarda el bundle
`var(--size-sm-control-height)`, 1:1 con él.

**Identidad fuera de contrato**: los nueve `100%` («ocupa a tu padre») y el
`min-inline-size: max-content` del tooltip, misma clase ya adjudicada en
`date-picker`. Por eso el techo es 49 %, y está escrito en el veredicto §5.

EL INSTRUMENTO ESTABA CIEGO DE TRES MANERAS, arregladas ANTES de medir:

1. **La superficie de chart vive en DIECISÉIS rutas de demo**, una por tipo:
   `/chart` monta 58 nodos de los 302 de la página y el resto vive en rutas
   hermanas. El guard gana `urls`: un token está VIVO si ALGUNA superficie del
   componente lo sigue, y cada URL sólo re-prueba lo pendiente (el caso de una
   sola ruta cuesta exactamente lo que costaba). De paso `picker-shell` recibe
   la suya: su ruta propia es un 404 y se mide dentro del date-picker — pasa de
   0/31 con seis sin adjudicar a 6/31 sin tener que saberlo.
2. **El guard no leía NINGUNA propiedad SVG.** Sin `fill` / `stroke` /
   `strokeWidth` / `fillOpacity` / `r` / `rx`, doce de las 39 claves de un
   componente que dibuja en SVG leían muertas estando vivas. Misma clase que el
   hueco de `filter` / `backdrop-filter` de ayer.
3. **El valor centinela se elegía por SUBCADENA**: `slice-stroke-width` recibía
   `rgb(1, 2, 3)` por contener «stroke» —inválido para una longitud— y leía
   muerta. La dimensión se decide ahora por el FINAL del nombre.

Los 28 componentes con ledger re-verificados tras los tres cambios: cero
regresiones, cero STALE. (Ojo: doce a la vez contra el mismo dev server dan
falsos exit=1 por timeout; repetir a solas antes de creerse una regresión.)

Gates: sonda **0 diffs en 161.850 valores computados** (16 rutas × 7 estados,
892 nodos en reposo) · centinela **37/39**, con `transition-duration` y `-ease`
adjudicadas midiéndolas sin la congelación (0,12s → 11,5s · cubic-bezier →
steps(4)) · censo --only 49 % · --names 39/39 conformes · component-audit PASS ·
eidos-lint 0 invalid · suite eidos con el único rojo conocido
(`skin-media-player`) · rtl:check 0 · docs:check 0 · `check` con 72 errores
globales y 0 en los ficheros tocados.

Abre (→ next-features §13): el censo **penaliza consumir la capa tipográfica**
si no eres un primitivo —las doce declaraciones correctas siguen contando
`global`—, que es la nota de `listbox` con una segunda ocurrencia; y el número
del gauge no tiene estilo con nombre que lo represente (un readout de dato no es
un `h2`), así que sigue leyendo `--font-size-xl`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 5e7462a3ff
commit 15eb9c56c9

@ -10,9 +10,9 @@
- **Medido**: 2026-08-22 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/`
- **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **56%** — 2638 de 4679 knobs pasan por un token público del componente
- **Reparto**: público 2638 · privado 338 · global 1145 · literal 558 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 19 · **alcance 100 %**: 17 · **con eje `size`**: 58
- **Alcance global**: **57%** — 2675 de 4682 knobs pasan por un token público del componente
- **Reparto**: público 2675 · privado 338 · global 1115 · literal 554 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 18 · **alcance 100 %**: 17 · **con eje `size`**: 58
## Cómo se lee
@ -72,11 +72,11 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma
| ---: | --- | ---: | ---: | ---: | ---: |
| 1 | [palabras](./palabras.md) | 363 | 22% | 471 | 0 |
| 2 | [chronos](./chronos.md) | 235 | 1% | 245 | 9 |
| 3 | [chart](./chart.md) | 80 | 9% | 92 | 17 |
| 4 | [proof-of-human](./proof-of-human.md) | 77 | 15% | 95 | 20 |
| 5 | [prose](./prose.md) | 73 | 1% | 74 | 2 |
| 6 | [time-range-picker](./time-range-picker.md) | 67 | 20% | 88 | 22 |
| 7 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 |
| 3 | [proof-of-human](./proof-of-human.md) | 77 | 15% | 95 | 20 |
| 4 | [prose](./prose.md) | 73 | 1% | 74 | 2 |
| 5 | [time-range-picker](./time-range-picker.md) | 67 | 20% | 88 | 22 |
| 6 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 |
| 7 | [chart](./chart.md) | 46 | 49% | 95 | 39 |
| 8 | [color-picker](./color-picker.md) | 39 | 51% | 82 | 54 |
| 9 | [field-langs](./field-langs.md) | 39 | 0% | 41 | 0 |
| 10 | [date-range-picker](./date-range-picker.md) | 30 | 40% | 53 | 22 |
@ -118,7 +118,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [chronos](./chronos.md) | 1% | 245 | 3 | 21 | 202 | 12 | 7 | 9 | y |
| [prose](./prose.md) | 1% | 74 | 1 | 0 | 43 | 30 | 0 | 2 | y |
| [nav-tree](./nav-tree.md) | 4% | 25 | 1 | 0 | 17 | 6 | 1 | 1 | – |
| [chart](./chart.md) | 9% | 92 | 8 | 0 | 66 | 14 | 4 | 17 | – |
| [result](./result.md) | 14% | 22 | 3 | 0 | 15 | 4 | 0 | 8 | – |
| [proof-of-human](./proof-of-human.md) | 15% | 95 | 14 | 17 | 29 | 31 | 4 | 20 | – |
| [menubar](./menubar.md) | 15% | 27 | 4 | 6 | 16 | 0 | 1 | 5 | y |
@ -148,6 +147,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [navigation-menu](./navigation-menu.md) | 44% | 38 | 15 | 1 | 13 | 5 | 4 | 26 | y |
| [skeleton](./skeleton.md) | 45% | 11 | 5 | 5 | 0 | 1 | 0 | 9 | – |
| [popover](./popover.md) | 46% | 51 | 22 | 4 | 22 | 0 | 3 | 36 | y |
| [chart](./chart.md) | 49% | 95 | 45 | 0 | 36 | 10 | 4 | 39 | – |
| [text-scramble](./text-scramble.md) | 50% | 4 | 2 | 0 | 0 | 2 | 0 | 0 | – |
| [split-button](./split-button.md) | 50% | 2 | 1 | 0 | 1 | 0 | 0 | 1 | y |
| [color-picker](./color-picker.md) | 51% | 82 | 40 | 6 | 22 | 11 | 3 | 54 | y |

@ -5,106 +5,72 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-20 · **Alcance**: **9%** — 8 de 88 knobs por token público
- **Knobs de apariencia**: 92 — público 8 · privado 0 · global 66 · literal 14 · sistema 4 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 17 pública(s) — `line-width`, `axis-width`, `grid-width`, `bar-radius`, `point-radius`, `area-opacity`, `slice-stroke-width`, `transition-duration`, `transition-ease`, `tooltip-bg`, `tooltip-border`, `tooltip-radius`, `tooltip-shadow`, `tooltip-padding`, `tooltip-gap`, `crosshair-fg`, `crosshair-width`
- **Medido**: 2026-08-22 · **Alcance**: **49%** — 45 de 91 knobs por token público
- **Knobs de apariencia**: 95 — público 45 · privado 0 · global 36 · literal 10 · sistema 4 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 39 pública(s) — `line-width`, `axis-width`, `grid-width`, `bar-radius`, `point-radius`, `area-opacity`, `slice-stroke-width`, `transition-duration`, `transition-ease`, `tooltip-bg`, `tooltip-border`, `tooltip-radius`, `tooltip-shadow`, `tooltip-padding`, `tooltip-gap`, `crosshair-fg`, `crosshair-width`, `axis-fg`, `grid-fg`, `separator-fg`, `smith-boundary-fg`, `track-bg`, `track-height`, `track-radius`, `gap`, `legend-row-gap`, `legend-column-gap`, `legend-item-gap`, `swatch-size`, `swatch-radius`, `tooltip-swatch-size`, `bar-list-row-gap`, `bar-list-column-gap`, `bar-segment-gap`, `bar-segment-track-gap`, `bar-segment-track-radius`, `bar-segment-fill-min-inline-size`, `bar-segment-legend-row-gap`, `bar-segment-legend-column-gap`
- **Eje `size`**: no · **ficheros**: `chart.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (66)
### 1.1 Directo a primitivo global (36)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `chart.css:14` | `[data-chart-frame]` | `gap` | `var(--space-2)` |
| 2 | `chart.css:36` | `[data-chart-empty]` | `color` | `var(--color-content-secondary)` |
| 3 | `chart.css:37` | `[data-chart-empty]` | `font-size` | `var(--font-size-sm)` |
| 4 | `chart.css:43` | `[data-chart-axis-line], [data-chart-axis-tick]` | `stroke` | `var(--color-border-default)` |
| 5 | `chart.css:48` | `[data-chart-axis-label]` | `fill` | `var(--color-content-secondary)` |
| 6 | `chart.css:49` | `[data-chart-axis-label]` | `font-size` | `var(--font-size-xs)` |
| 7 | `chart.css:54` | `[data-chart-grid-line]` | `stroke` | `var(--color-border-subtle)` |
| 8 | `chart.css:82` | `[data-chart-slice]` | `stroke` | `var(--color-surface-default)` |
| 9 | `chart.css:98` | `[data-chart-active-dot]` | `stroke` | `var(--color-surface-default)` |
| 10 | `chart.css:123` | `[data-chart-tooltip]` | `font-size` | `var(--font-size-xs)` |
| 11 | `chart.css:124` | `[data-chart-tooltip]` | `color` | `var(--color-content-primary)` |
| 12 | `chart.css:131` | `[data-chart-tooltip-x]` | `font-weight` | `var(--font-weight-medium)` |
| 13 | `chart.css:132` | `[data-chart-tooltip-x]` | `color` | `var(--color-content-primary)` |
| 14 | `chart.css:138` | `[data-chart-tooltip-row]` | `color` | `var(--color-content-secondary)` |
| 15 | `chart.css:143` | `[data-chart-tooltip-swatch]` | `border-radius` | `var(--radius-sm)` |
| 16 | `chart.css:149` | `[data-chart-tooltip-value]` | `color` | `var(--color-content-primary)` |
| 17 | `chart.css:160` | `[data-chart-figure]` | `gap` | `var(--space-2)` |
| 18 | `chart.css:166` | `[data-chart-title]` | `font-size` | `var(--font-size-sm)` |
| 19 | `chart.css:167` | `[data-chart-title]` | `font-weight` | `var(--font-weight-medium)` |
| 20 | `chart.css:168` | `[data-chart-title]` | `color` | `var(--color-content-primary)` |
| 21 | `chart.css:174` | `[data-chart-legend]` | `gap` | `var(--space-1-5) var(--space-4)` |
| 22 | `chart.css:181` | `[data-chart-legend-item]` | `gap` | `var(--space-1-5)` |
| 23 | `chart.css:182` | `[data-chart-legend-item]` | `font-size` | `var(--font-size-xs)` |
| 24 | `chart.css:183` | `[data-chart-legend-item]` | `color` | `var(--color-content-secondary)` |
| 25 | `chart.css:189` | `[data-chart-swatch]` | `border-radius` | `var(--radius-sm)` |
| 26 | `chart.css:202` | `[data-chart-bar-list]` | `gap` | `var(--space-2) var(--space-3)` |
| 27 | `chart.css:212` | `[data-chart-bar-list-label]` | `font-size` | `var(--font-size-sm)` |
| 28 | `chart.css:213` | `[data-chart-bar-list-label]` | `color` | `var(--color-content-primary)` |
| 29 | `chart.css:219` | `[data-chart-bar-list-track]` | `block-size` | `var(--control-height-sm, var(--space-7))` |
| 30 | `chart.css:220` | `[data-chart-bar-list-track]` | `background` | `var(--color-surface-muted)` |
| 31 | `chart.css:221` | `[data-chart-bar-list-track]` | `border-radius` | `var(--radius-sm)` |
| 32 | `chart.css:226` | `[data-chart-bar-list-bar]` | `border-radius` | `var(--radius-sm)` |
| 33 | `chart.css:230` | `[data-chart-bar-list-value]` | `font-size` | `var(--font-size-sm)` |
| 34 | `chart.css:231` | `[data-chart-bar-list-value]` | `color` | `var(--color-content-secondary)` |
| 35 | `chart.css:241` | `[data-chart-bar-segment]` | `gap` | `var(--space-3)` |
| 36 | `chart.css:246` | `[data-chart-bar-segment-track]` | `gap` | `var(--border-width-medium)` |
| 37 | `chart.css:247` | `[data-chart-bar-segment-track]` | `block-size` | `var(--control-height-sm, var(--space-7))` |
| 38 | `chart.css:248` | `[data-chart-bar-segment-track]` | `border-radius` | `var(--radius-md)` |
| 39 | `chart.css:253` | `[data-chart-bar-segment-fill]` | `min-inline-size` | `var(--border-width-thick)` |
| 40 | `chart.css:264` | `[data-chart-bar-segment-legend]` | `gap` | `var(--space-2) var(--space-4)` |
| 41 | `chart.css:269` | `[data-chart-bar-segment-legend-item]` | `gap` | `var(--space-1-5)` |
| 42 | `chart.css:270` | `[data-chart-bar-segment-legend-item]` | `font-size` | `var(--font-size-xs)` |
| 43 | `chart.css:271` | `[data-chart-bar-segment-legend-item]` | `color` | `var(--color-content-secondary)` |
| 44 | `chart.css:274` | `[data-chart-bar-segment-legend-value]` | `color` | `var(--color-content-primary)` |
| 45 | `chart.css:282` | `[data-chart-radar-ring], [data-chart-radar-spoke]` | `stroke` | `var(--color-border-subtle)` |
| 46 | `chart.css:313` | `[data-chart-smith-boundary]` | `stroke` | `var(--color-content-secondary)` |
| 47 | `chart.css:318` | `[data-chart-smith-grid]` | `stroke` | `var(--color-border-default)` |
| 48 | `chart.css:322` | `[data-chart-smith-label]` | `fill` | `var(--color-content-secondary)` |
| 49 | `chart.css:323` | `[data-chart-smith-label]` | `font-size` | `var(--font-size-xs)` |
| 50 | `chart.css:328` | `[data-chart-smith-point]` | `stroke` | `var(--color-surface-default)` |
| 51 | `chart.css:332` | `[data-chart-smith-point-label]` | `fill` | `var(--color-content-primary)` |
| 52 | `chart.css:333` | `[data-chart-smith-point-label]` | `font-size` | `var(--font-size-xs)` |
| 53 | `chart.css:334` | `[data-chart-smith-point-label]` | `font-weight` | `var(--font-weight-medium)` |
| 54 | `chart.css:340` | `[data-chart-polar-petal]` | `stroke` | `var(--color-surface-default)` |
| 55 | `chart.css:353` | `[data-chart-funnel-segment]` | `stroke` | `var(--color-surface-default)` |
| 56 | `chart.css:357` | `[data-chart-funnel-label]` | `font-size` | `var(--font-size-sm)` |
| 57 | `chart.css:358` | `[data-chart-funnel-label]` | `font-weight` | `var(--font-weight-medium)` |
| 58 | `chart.css:363` | `[data-chart-funnel-connector]` | `stroke` | `var(--color-border-default)` |
| 59 | `chart.css:367` | `[data-chart-funnel-side]` | `font-size` | `var(--font-size-sm)` |
| 60 | `chart.css:370` | `[data-chart-funnel-name]` | `fill` | `var(--color-content-primary)` |
| 61 | `chart.css:371` | `[data-chart-funnel-name]` | `font-weight` | `var(--font-weight-medium)` |
| 62 | `chart.css:374` | `[data-chart-funnel-meta]` | `fill` | `var(--color-content-secondary)` |
| 63 | `chart.css:380` | `[data-chart-gauge-track]` | `fill` | `var(--color-surface-muted)` |
| 64 | `chart.css:386` | `[data-chart-gauge-label]` | `fill` | `var(--color-content-primary)` |
| 65 | `chart.css:387` | `[data-chart-gauge-label]` | `font-size` | `var(--font-size-xl)` |
| 66 | `chart.css:388` | `[data-chart-gauge-label]` | `font-weight` | `var(--font-weight-medium)` |
| 1 | `chart.css:46` | `[data-chart-empty]` | `color` | `var(--color-content-secondary)` |
| 2 | `chart.css:47` | `[data-chart-empty]` | `font-size` | `var(--style-label-font-size)` |
| 3 | `chart.css:58` | `[data-chart-axis-label]` | `fill` | `var(--color-content-secondary)` |
| 4 | `chart.css:59` | `[data-chart-axis-label]` | `font-size` | `var(--style-caption-font-size)` |
| 5 | `chart.css:133` | `[data-chart-tooltip]` | `font-size` | `var(--style-caption-font-size)` |
| 6 | `chart.css:134` | `[data-chart-tooltip]` | `color` | `var(--color-content-primary)` |
| 7 | `chart.css:141` | `[data-chart-tooltip-x]` | `font-weight` | `var(--font-weight-medium)` |
| 8 | `chart.css:142` | `[data-chart-tooltip-x]` | `color` | `var(--color-content-primary)` |
| 9 | `chart.css:148` | `[data-chart-tooltip-row]` | `color` | `var(--color-content-secondary)` |
| 10 | `chart.css:159` | `[data-chart-tooltip-value]` | `color` | `var(--color-content-primary)` |
| 11 | `chart.css:176` | `[data-chart-title]` | `font-size` | `var(--style-label-font-size)` |
| 12 | `chart.css:177` | `[data-chart-title]` | `font-weight` | `var(--font-weight-medium)` |
| 13 | `chart.css:178` | `[data-chart-title]` | `color` | `var(--color-content-primary)` |
| 14 | `chart.css:193` | `[data-chart-legend-item]` | `font-size` | `var(--style-caption-font-size)` |
| 15 | `chart.css:194` | `[data-chart-legend-item]` | `color` | `var(--color-content-secondary)` |
| 16 | `chart.css:224` | `[data-chart-bar-list-label]` | `font-size` | `var(--style-label-font-size)` |
| 17 | `chart.css:225` | `[data-chart-bar-list-label]` | `color` | `var(--color-content-primary)` |
| 18 | `chart.css:242` | `[data-chart-bar-list-value]` | `font-size` | `var(--style-label-font-size)` |
| 19 | `chart.css:243` | `[data-chart-bar-list-value]` | `color` | `var(--color-content-secondary)` |
| 20 | `chart.css:283` | `[data-chart-bar-segment-legend-item]` | `font-size` | `var(--style-caption-font-size)` |
| 21 | `chart.css:284` | `[data-chart-bar-segment-legend-item]` | `color` | `var(--color-content-secondary)` |
| 22 | `chart.css:287` | `[data-chart-bar-segment-legend-value]` | `color` | `var(--color-content-primary)` |
| 23 | `chart.css:335` | `[data-chart-smith-label]` | `fill` | `var(--color-content-secondary)` |
| 24 | `chart.css:336` | `[data-chart-smith-label]` | `font-size` | `var(--style-caption-font-size)` |
| 25 | `chart.css:345` | `[data-chart-smith-point-label]` | `fill` | `var(--color-content-primary)` |
| 26 | `chart.css:346` | `[data-chart-smith-point-label]` | `font-size` | `var(--style-caption-font-size)` |
| 27 | `chart.css:347` | `[data-chart-smith-point-label]` | `font-weight` | `var(--font-weight-medium)` |
| 28 | `chart.css:370` | `[data-chart-funnel-label]` | `font-size` | `var(--style-label-font-size)` |
| 29 | `chart.css:371` | `[data-chart-funnel-label]` | `font-weight` | `var(--font-weight-medium)` |
| 30 | `chart.css:380` | `[data-chart-funnel-side]` | `font-size` | `var(--style-label-font-size)` |
| 31 | `chart.css:383` | `[data-chart-funnel-name]` | `fill` | `var(--color-content-primary)` |
| 32 | `chart.css:384` | `[data-chart-funnel-name]` | `font-weight` | `var(--font-weight-medium)` |
| 33 | `chart.css:387` | `[data-chart-funnel-meta]` | `fill` | `var(--color-content-secondary)` |
| 34 | `chart.css:399` | `[data-chart-gauge-label]` | `fill` | `var(--color-content-primary)` |
| 35 | `chart.css:400` | `[data-chart-gauge-label]` | `font-size` | `var(--font-size-xl)` |
| 36 | `chart.css:401` | `[data-chart-gauge-label]` | `font-weight` | `var(--font-weight-medium)` |
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (14)
### 1.3 Literales (10)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `chart.css:15` | `[data-chart-frame]` | `inline-size` | `100%` |
| 2 | `chart.css:19` | `[data-chart-plot]` | `inline-size` | `100%` |
| 3 | `chart.css:25` | `[data-chart]` | `inline-size` | `100%` |
| 4 | `chart.css:35` | `[data-chart-empty]` | `block-size` | `100%` |
| 5 | `chart.css:125` | `[data-chart-tooltip]` | `min-inline-size` | `max-content` |
| 6 | `chart.css:141` | `[data-chart-tooltip-swatch]` | `inline-size` | `0.6em` |
| 7 | `chart.css:142` | `[data-chart-tooltip-swatch]` | `block-size` | `0.6em` |
| 8 | `chart.css:161` | `[data-chart-figure]` | `inline-size` | `100%` |
| 9 | `chart.css:187` | `[data-chart-swatch]` | `inline-size` | `0.75em` |
| 10 | `chart.css:188` | `[data-chart-swatch]` | `block-size` | `0.75em` |
| 11 | `chart.css:203` | `[data-chart-bar-list]` | `inline-size` | `100%` |
| 12 | `chart.css:225` | `[data-chart-bar-list-bar]` | `block-size` | `100%` |
| 13 | `chart.css:242` | `[data-chart-bar-segment]` | `inline-size` | `100%` |
| 14 | `chart.css:252` | `[data-chart-bar-segment-fill]` | `block-size` | `100%` |
| 1 | `chart.css:25` | `[data-chart-frame]` | `inline-size` | `100%` |
| 2 | `chart.css:29` | `[data-chart-plot]` | `inline-size` | `100%` |
| 3 | `chart.css:35` | `[data-chart]` | `inline-size` | `100%` |
| 4 | `chart.css:45` | `[data-chart-empty]` | `block-size` | `100%` |
| 5 | `chart.css:135` | `[data-chart-tooltip]` | `min-inline-size` | `max-content` |
| 6 | `chart.css:171` | `[data-chart-figure]` | `inline-size` | `100%` |
| 7 | `chart.css:215` | `[data-chart-bar-list]` | `inline-size` | `100%` |
| 8 | `chart.css:237` | `[data-chart-bar-list-bar]` | `block-size` | `100%` |
| 9 | `chart.css:254` | `[data-chart-bar-segment]` | `inline-size` | `100%` |
| 10 | `chart.css:264` | `[data-chart-bar-segment-fill]` | `block-size` | `100%` |
## 2. Sistema transversal (4) — informativo, fuera del ratio
@ -112,10 +78,10 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `chart.css:259` | `[data-chart-bar-segment-fill][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
| 2 | `chart.css:300` | `[data-chart-heat-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
| 3 | `chart.css:308` | `[data-chart-cal-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
| 4 | `chart.css:348` | `[data-chart-polar-petal][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
| 1 | `chart.css:271` | `[data-chart-bar-segment-fill][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
| 2 | `chart.css:313` | `[data-chart-heat-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
| 3 | `chart.css:321` | `[data-chart-cal-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
| 4 | `chart.css:361` | `[data-chart-polar-petal][data-dim='true']` | `opacity` | `var(--opacity-scrim)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
@ -123,7 +89,7 @@ _La receta no declara privados propios en su CSS._
## 4. Propuesta de corrección
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (71)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (37)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
@ -133,74 +99,40 @@ no distingue lo que debería — se marca `⚠`.
| token (`--chart-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `frame-gap` | `root` | `var(--space-2)` | 1 |
| `empty-fg` | `root` | `var(--color-content-secondary)` | 1 |
| `empty-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `axis-line-stroke` | `root` | `var(--color-border-default)` | 1 |
| `empty-font-size` | `root` | `var(--style-label-font-size)` | 1 |
| `axis-label-fill` | `root` | `var(--color-content-secondary)` | 1 |
| `axis-label-font-size` | `root` | `var(--font-size-xs)` | 1 |
| `grid-line-stroke` | `root` | `var(--color-border-subtle)` | 1 |
| `slice-stroke` | `root` | `var(--color-surface-default)` | 1 |
| `active-dot-stroke` | `root` | `var(--color-surface-default)` | 1 |
| `tooltip-font-size` | `root` | `var(--font-size-xs)` | 1 |
| `axis-label-font-size` | `root` | `var(--style-caption-font-size)` | 1 |
| `tooltip-font-size` | `root` | `var(--style-caption-font-size)` | 1 |
| `tooltip-fg` | `root` | `var(--color-content-primary)` | 1 |
| `tooltip-width` | `root` | `max-content` | 1 |
| `tooltip-x-font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `tooltip-x-fg` | `root` | `var(--color-content-primary)` | 1 |
| `tooltip-row-fg` | `root` | `var(--color-content-secondary)` | 1 |
| `tooltip-swatch-width` | `root` | `0.6em` | 1 |
| `tooltip-swatch-height` | `root` | `0.6em` | 1 |
| `tooltip-swatch-radius` | `root` | `var(--radius-sm)` | 1 |
| `tooltip-value-fg` | `root` | `var(--color-content-primary)` | 1 |
| `figure-gap` | `root` | `var(--space-2)` | 1 |
| `title-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `title-font-size` | `root` | `var(--style-label-font-size)` | 1 |
| `title-font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `title-fg` | `root` | `var(--color-content-primary)` | 1 |
| `legend-gap` | `root` | `var(--space-1-5) var(--space-4)` | 1 |
| `legend-item-gap` | `root` | `var(--space-1-5)` | 1 |
| `legend-item-font-size` | `root` | `var(--font-size-xs)` | 1 |
| `legend-item-font-size` | `root` | `var(--style-caption-font-size)` | 1 |
| `legend-item-fg` | `root` | `var(--color-content-secondary)` | 1 |
| `swatch-width` | `root` | `0.75em` | 1 |
| `swatch-height` | `root` | `0.75em` | 1 |
| `swatch-radius` | `root` | `var(--radius-sm)` | 1 |
| `bar-list-gap` | `root` | `var(--space-2) var(--space-3)` | 1 |
| `bar-list-label-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `bar-list-label-font-size` | `root` | `var(--style-label-font-size)` | 1 |
| `bar-list-label-fg` | `root` | `var(--color-content-primary)` | 1 |
| `bar-list-track-height` | `root` | `var(--control-height-sm, var(--space-7))` | 1 |
| `bar-list-track-bg` | `root` | `var(--color-surface-muted)` | 1 |
| `bar-list-track-radius` | `root` | `var(--radius-sm)` | 1 |
| `bar-list-bar-radius` | `root` | `var(--radius-sm)` | 1 |
| `bar-list-value-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `bar-list-value-font-size` | `root` | `var(--style-label-font-size)` | 1 |
| `bar-list-value-fg` | `root` | `var(--color-content-secondary)` | 1 |
| `bar-segment-gap` | `root` | `var(--space-3)` | 1 |
| `bar-segment-track-gap` | `root` | `var(--border-width-medium)` | 1 |
| `bar-segment-track-height` | `root` | `var(--control-height-sm, var(--space-7))` | 1 |
| `bar-segment-track-radius` | `root` | `var(--radius-md)` | 1 |
| `bar-segment-fill-width` | `root` | `var(--border-width-thick)` | 1 |
| `bar-segment-legend-gap` | `root` | `var(--space-2) var(--space-4)` | 1 |
| `bar-segment-legend-item-gap` | `root` | `var(--space-1-5)` | 1 |
| `bar-segment-legend-item-font-size` | `root` | `var(--font-size-xs)` | 1 |
| `bar-segment-legend-item-font-size` | `root` | `var(--style-caption-font-size)` | 1 |
| `bar-segment-legend-item-fg` | `root` | `var(--color-content-secondary)` | 1 |
| `bar-segment-legend-value-fg` | `root` | `var(--color-content-primary)` | 1 |
| `radar-ring-stroke` | `root` | `var(--color-border-subtle)` | 1 |
| `smith-boundary-stroke` | `root` | `var(--color-content-secondary)` | 1 |
| `smith-grid-stroke` | `root` | `var(--color-border-default)` | 1 |
| `smith-label-fill` | `root` | `var(--color-content-secondary)` | 1 |
| `smith-label-font-size` | `root` | `var(--font-size-xs)` | 1 |
| `smith-point-stroke` | `root` | `var(--color-surface-default)` | 1 |
| `smith-label-font-size` | `root` | `var(--style-caption-font-size)` | 1 |
| `smith-point-label-fill` | `root` | `var(--color-content-primary)` | 1 |
| `smith-point-label-font-size` | `root` | `var(--font-size-xs)` | 1 |
| `smith-point-label-font-size` | `root` | `var(--style-caption-font-size)` | 1 |
| `smith-point-label-font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `polar-petal-stroke` | `root` | `var(--color-surface-default)` | 1 |
| `funnel-segment-stroke` | `root` | `var(--color-surface-default)` | 1 |
| `funnel-label-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `funnel-label-font-size` | `root` | `var(--style-label-font-size)` | 1 |
| `funnel-label-font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `funnel-connector-stroke` | `root` | `var(--color-border-default)` | 1 |
| `funnel-side-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `funnel-side-font-size` | `root` | `var(--style-label-font-size)` | 1 |
| `funnel-name-fill` | `root` | `var(--color-content-primary)` | 1 |
| `funnel-name-font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `funnel-meta-fill` | `root` | `var(--color-content-secondary)` | 1 |
| `gauge-track-fill` | `root` | `var(--color-surface-muted)` | 1 |
| `gauge-label-fill` | `root` | `var(--color-content-primary)` | 1 |
| `gauge-label-font-size` | `root` | `var(--font-size-xl)` | 1 |
| `gauge-label-font-weight` | `root` | `var(--font-weight-medium)` | 1 |
@ -221,6 +153,71 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-22 — 9 % → 49 %**, 22 claves nuevas fusionadas en el bloque
existente (17 → 39). El 49 % es el techo de este diseño, no un trabajo a medias:
lo que queda fuera está medido y cada clase tiene su razón escrita abajo.
**La propuesta §4 pedía 71 claves y habría escrito ~20 con el MISMO valor.** La
medición que lo decidió: las 84 filas fuera de alcance colapsaban a **26 valores
distintos** — 9× `content-secondary`, 9× `content-primary`, 9× `100%`, 6×
`font-size-sm`, 6× `font-size-xs`, 6× `font-weight-medium`, 5×
`surface-default`, 4× `radius-sm`, 3× `border-default`. Nombrar por parte
habría dado cinco claves para el pelo que separa marcas y nueve para la tinta
de anotación. Es la inflación que F2-B ya cazó en `spinner`, con otra forma.
**El texto NO recibe tokens de chart, por doctrina.** Toma su ROL DE TAMAÑO de
los estilos con nombre —`caption` (xs) para las anotaciones y `label` (sm) para
el texto de cromo— y su TINTA y PESO de las ranuras del sistema
(`--color-content-*`, `--font-weight-*`). Acuñar `--chart-label-font-size`
sería inventar tipografía por componente, que es exactamente lo que el autor
prohibió el mismo día para la familia text-effects. Precedente de consumir la
capa: `code-block`, `kbd`, `announce`. Doce declaraciones cambian de
`var(--font-size-xs|sm)` a `var(--style-caption|label-font-size)`, mismo valor.
**Consecuencia medida en la métrica**: esas doce y las veinte de tinta/peso
siguen contando como `global`, porque el censo sólo exime `--style-*` a los
PRIMITIVOS tipográficos y chart no lo es. El censo penaliza aquí hacer lo
correcto — la misma nota que dejó `listbox` en F2-A (§13).
**Lo que la receta sí posee: la tinta de lo que no es texto** — cinco claves,
cada una un concepto que el CSS repite: `axis-fg` (eje + rejilla smith +
conector del funnel), `grid-fg` (rejilla + radar), `separator-fg` (el pelo de
superficie entre marcas contiguas: porción, punto activo, punto smith, pétalo,
segmento), `smith-boundary-fg` (el borde del disco, más fuerte a propósito) y
`track-bg` (raíl del bar-list y del gauge). Más 17 de geometría de cromo.
**Un incumplimiento heredado, corregido**: el CSS leía
`var(--control-height-sm, var(--space-7))` — coordenada CRUDA, que
`recipe-css-contract` prohíbe en un token de receta, con un respaldo muerto (el
primitivo existe). El contrato guarda `var(--size-sm-control-height)`, el
bundle, que es 1:1 con él.
**Fuera de contrato por identidad** (los 10 literales): nueve `100%` que
significan «ocupa a tu padre» y el `min-inline-size: max-content` del tooltip,
que es la corrección de composición de la misma clase ya adjudicada en
`date-picker`. Moverlos no es tematizar, es romper el layout.
**Una escotilla sin cerrar**: el `--font-size-xl` del número del gauge. Ningún
estilo con nombre juega ese papel (un readout de dato no es un `h2`) y acuñar
`gauge-label-font-size` sería tipografía por componente. Anotado en §13.
**El instrumento estaba ciego de tres maneras, y las tres se arreglaron ANTES
de medir** (ninguna era culpa de los tokens):
1. **La superficie de chart vive en DIECISÉIS rutas de demo**, una por tipo de
gráfico: `/chart` monta una línea con área y nada más (58 nodos de 302 en la
página). El guard ganó `urls` — un token está VIVO si alguna superficie del
componente lo sigue— y la sonda se corrió por las dieciséis.
2. **El guard no leía NINGUNA propiedad SVG.** Sin `fill` / `stroke` /
`strokeWidth` / `fillOpacity` / `r` / `rx`, doce de las 39 claves de un
componente que dibuja en SVG leían muertas estando vivas. Misma clase que el
hueco de `filter` / `backdrop-filter` del día anterior.
3. **El valor centinela se elegía por SUBCADENA**: `slice-stroke-width` recibía
`rgb(1, 2, 3)` por contener «stroke», valor inválido para una longitud, y
leía muerta. Ahora la dimensión se decide por el final del nombre.
Con eso: sonda **0 diffs en 161.850 valores computados** (16 rutas × 7 estados)
y centinela **37/39**, con `transition-duration` y `-ease` adjudicadas y
medidas sin la congelación (0.12s → 11.5s · cubic-bezier → steps(4)).
<!-- veredicto:end -->

@ -958,5 +958,27 @@ para resolver después** (autor, 2026-08-22):
ser un guard**: la lógica es la misma que la del test de fantasmas sin
fallback que ya existe, con el filtro de vocabulario de sistema encima.
Lo que `chart` destapó (2026-08-22):
- **⚠ El censo penaliza consumir la capa tipográfica cuando no eres un
primitivo.** `chart` pasa doce declaraciones de `var(--font-size-xs|sm)` a
`var(--style-caption|label-font-size)` —lo que la doctrina manda: el rol de
tamaño lo poseen los estilos con nombre— y las doce siguen contando como
`global`, porque `TYPOGRAPHIC_PRIMITIVES` sólo exime a los seis primitivos
y `LAYER_VOCABULARY` no cubre `--style-*` para el resto. Es la misma nota que
dejó `listbox` en F2-A, ahora con una segunda ocurrencia y una forma clara:
**leer la capa que posee el eje debería contar como alcance en CUALQUIER
componente, no sólo en los primitivos.** Cambiarlo mueve la cifra de varios
componentes de golpe, así que es decisión, no arreglo.
- **⚠ El número del gauge no tiene estilo con nombre que lo represente.**
`[data-chart-gauge-label]` es un READOUT de dato a `--font-size-xl`; el canon
tiene `h2` en ese tamaño, pero un dato no es un encabezado, y acuñar
`--chart-gauge-label-font-size` sería tipografía por componente (prohibido,
2026-08-22). Queda leyendo el primitivo. Si aparecen más readouts —el valor
del `Gauge`, el total de un `Funnel`, el número de un `Stat`— la respuesta
es un estilo con nombre nuevo en el módulo de tipografía, que es firma del
autor.
**Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando
estorben.

@ -4,14 +4,14 @@
con su revisión adversarial pasada (2026-08-22); la cola sigue abierta y
`text-gradient` ya está dentro (2026-08-22).** Alcance global **56 %** (33 % al abrir el eje, 45 % tras F2-A, 50 % tras F2-B,
y **+6 puntos al arreglar la MEDICIÓN**, no el código: ver abajo). Sin contrato
**24**; al 100 % **17**; alcance <20 % **19**.
**24**; al 100 % **17**; alcance <20 % **18**. Último: `chart` 9 % → 49 %.
## LO PRIMERO AL ENTRAR (cinco minutos, en este orden)
1. **Comprueba que el suelo no se ha movido**:
```bash
node --import tsx/esm scripts/theming-census.ts # 162 · 5.188 · 2.638 (56 %) · 24 sin contrato
node --import tsx/esm scripts/theming-census.ts # 162 · 5.191 · 2.675 (57 %) · 24 sin contrato
node --import tsx/esm scripts/theming-census.ts --names # DESVIADAS 0 — la gramática se mantiene
```
@ -134,6 +134,28 @@ declarados). Tabla en next-features §13. La más grave no es de esta familia:
`--style-*-font-variation-settings` de `text.css` significan que **el eje de
fuente variable no llega nunca al primitivo `Text`**. Ninguna se arregla aquí.
### 3.quater `chart` 9 % → 49 % (2026-08-22) — y el instrumento, tres veces ciego
Lo que hay que llevarse de aquí:
- **La ficha pedía 71 claves; ~20 habrían llevado el mismo valor.** Antes de
aceptar una propuesta larga, **agrupa las filas por VALOR**: las 84 de chart
colapsaban a 26. Nombrar por parte lo que es un concepto es la inflación de
`spinner` con otra cara.
- **El texto de un componente no recibe tokens propios**: rol de tamaño de los
estilos con nombre (`caption` / `label`), tinta y peso de las ranuras del
sistema. Sólo se acuña la tinta de lo que NO es texto.
- **El guard ya sabe medir en VARIAS rutas** (`urls` en `COMPONENT_OVERRIDES`):
un token vive si alguna superficie del componente lo sigue. Chart tiene 16;
`picker-shell` ya lleva la del date-picker (su ruta propia es 404).
- **El guard ya lee SVG** (`fill`, `stroke`, `strokeWidth`, `fillOpacity`, `r`,
`rx`…) y **elige el valor centinela por el FINAL del nombre**, no por
subcadena — `slice-stroke-width` recibía un color por contener «stroke».
- Tras cualquier cambio del guard: **re-verificar los 28 componentes con
ledger** (se hizo: cero regresiones, cero STALE). Y ojo, correr doce a la vez
contra el mismo dev server da falsos exit=1 por timeout: repite el que falle
a solas antes de creerte una regresión.
### 4. La cola que queda, por alcance
| componente | knobs | nota |
@ -142,6 +164,8 @@ fuente variable no llega nunca al primitivo `Text`**. Ninguna se arregla aquí.
| `field-langs` | 41 | espera el **mandato Field** (firma 2), que NO es tokenización: manda que los x-field incorporen el wrapper Field — cambia composición y mueve píxel |
| `mockup` | 29 (38 %) | ⚠ **sin ruta de demo (404)**, como `picker-shell`: sin sonda ni guard. Y sale NEEDS-WORK en el audit, preexistente |
| `chronos` | 245 (1 %) | el segundo mayor; sin abrir |
| `proof-of-human` | 95 (15 %) | escena de tono fijo firmada; lo demás son hooks de clase (eje aparte) |
| `prose` | 74 (1 %) | su superficie es la capa tipográfica: mismo caso que chart, un piso más arriba |
| `result` · `banner` · … | varios | ver el censo, `reach < 20 %` = 19 |
`avatar` ya está al **75 %** y `knob` al 75 % (ambos hechos). `heading` /

@ -455,6 +455,61 @@ de commit. Sin artefacto, el paso no se ha hecho.
## 8. Registro
- 2026-08-22 — **`chart` 9 % → 49 %** (22 claves nuevas fusionadas en su bloque,
17 → 39). El mayor de los de alcance <20 % que no estaba bloqueado.
**La ficha pedía 71 claves y ~20 habrían llevado el mismo valor.** Lo decidió
la medición: las 84 filas fuera de alcance colapsan a **26 valores distintos**
(9× `content-secondary`, 9× `content-primary`, 9× `100%`, 6× `font-size-sm`,
6× `font-size-xs`, 6× `font-weight-medium`, 5× `surface-default`). La
inflación de `spinner` con otra forma: nombrar por parte lo que es un
concepto.
**El texto no recibe tokens de chart**: toma su ROL DE TAMAÑO de los estilos
con nombre (`caption` para anotaciones, `label` para el cromo) y su tinta y
peso de las ranuras del sistema. Acuñarlos sería tipografía por componente,
lo que el autor prohibió el mismo día. Precedente de consumir la capa:
`code-block`, `kbd`, `announce`. **La receta sólo posee la tinta de lo que no
es texto** — `axis-fg`, `grid-fg`, `separator-fg` (el pelo de superficie que
separa marcas contiguas: cinco partes, una idea), `smith-boundary-fg`,
`track-bg` — más 17 de geometría de cromo. Identidad fuera de contrato: los
nueve `100%` y el `max-content` del tooltip.
**Incumplimiento heredado corregido**: el CSS leía la coordenada CRUDA
`var(--control-height-sm, var(--space-7))` (respaldo muerto); el contrato
guarda el bundle `var(--size-sm-control-height)`, 1:1.
**El instrumento estaba ciego de TRES maneras, arregladas antes de medir:**
1. **La superficie de chart vive en 16 rutas de demo**, una por tipo:
`/chart` monta 58 nodos de los 302 de la página y el resto vive en rutas
hermanas. El guard gana `urls`: un token está VIVO si ALGUNA superficie del
componente lo sigue, y cada URL sólo re-prueba lo que sigue pendiente.
De paso `picker-shell` recibe la suya (su ruta propia es un 404, se mide
dentro del date-picker: 0/31 → 6/31 sin pasar la URL a mano).
2. **El guard no leía NINGUNA propiedad SVG** — sin `fill`/`stroke`/
`strokeWidth`/`fillOpacity`/`r`/`rx`, doce claves de un componente que
dibuja en SVG leían muertas estando vivas. Misma clase que el hueco de
`filter` del día anterior.
3. **El valor centinela se elegía por SUBCADENA**: `slice-stroke-width`
recibía `rgb(1, 2, 3)` por contener «stroke» — inválido para una longitud,
así que leía muerta. Ahora la dimensión se decide por el final del nombre.
Gates: sonda **0 diffs en 161.850 valores** (16 rutas × 7 estados, 892 nodos
en reposo) · centinela **37/39** con dos adjudicaciones medidas sin la
congelación (`transition-duration` 0.12s → 11.5s, `-ease` → steps(4)) ·
censo `--only` 49 % y global **56 % → 57 %** (2.638 → 2.675 públicos,
<20 % 19 → 18) · `--names` 39/39 conformes · audit PASS · eidos-lint 0
invalid · suite eidos con el único rojo conocido · rtl 0 · docs 0 · `check`
sin errores en los ficheros tocados. **Los 28 componentes con ledger
re-verificados tras los tres cambios de instrumento: cero regresiones, cero
STALE.**
Abre (→ §13): el censo **penaliza consumir la capa tipográfica** si no eres
un primitivo (las doce declaraciones correctas siguen contando `global`) — la
nota de `listbox` con una segunda ocurrencia; y el número del gauge no tiene
estilo con nombre que lo represente (un readout de dato no es un `h2`).
- 2026-08-22 — **`text-scramble` ADJUDICADO: sin contrato que escribir** (y el
barrido de fantasmas, hecho). Sin cambios de código: su veredicto §5.

@ -314,6 +314,12 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'disabled-row-fg':
'no disabled row in the demo (0 mounted, counted); forced data-disabled → reaches (oklch(0.61 0 0) → rgb(1,2,3))'
},
chart: {
'transition-duration':
"the guard FREEZES transitions on purpose (a transitioned property returns its starting value), which is exactly what hides a token whose whole job IS the transition — same class as text-circular.char-transition. Measured without the freeze 2026-08-22 on [data-chart-line]: 0.12s -> 11.5s",
'transition-ease':
"same freeze, same measurement 2026-08-22 on [data-chart-line]: cubic-bezier(0, 0, 0.2, 1) -> steps(4)"
},
'text-circular': {
'char-transition':
"the guard FREEZES transitions on purpose (a transitioned property returns its starting value), which is exactly what hides a token whose whole job is the transition. Measured without the freeze 2026-08-22 -> reaches (0.5s cubic-bezier -> 9.5s linear)"

@ -82,6 +82,21 @@ const PROPS = [
'outlineWidth',
'outlineOffset',
'zIndex',
// SVG paint + geometry. Without these EVERY token of an SVG recipe reads
// dead: chart's axis / grid / separator ink, its stroke widths, the point
// radius and the area's fill-opacity are all painted through presentation
// attributes the box properties above never see (measured 2026-08-22 —
// 12 of chart's 39 tokens). Same class as the `filter` / `backdrop-filter`
// gap fixed the same day.
'fill',
'fillOpacity',
'stroke',
'strokeWidth',
'strokeOpacity',
'strokeDasharray',
'r',
'rx',
'ry',
'left',
'right',
'top',
@ -101,11 +116,16 @@ const PROPS = [
*/
const COMPONENT_OVERRIDES: Record<
string,
{ attrPrefix?: string; openWith?: string[]; openBy?: 'click' | 'hover' }
{ attrPrefix?: string; openWith?: string[]; openBy?: 'click' | 'hover'; urls?: string[] }
> = {
'picker-shell': {
attrPrefix: 'data-picker',
openWith: ['[data-uix-stage-area] [data-popover-trigger]']
openWith: ['[data-uix-stage-area] [data-popover-trigger]'],
// It has no route of its own — `/uix/components/picker-shell` is a 404 —
// so the default URL measured NOTHING and the guard reported 0/31 with six
// unadjudicated. Named here so nobody has to know: it is measured inside a
// host picker (6/31, the figure its own commit `67b4c810c` recorded).
urls: ['/uix/components/date-picker']
},
// A hover card opens on POINTER-OVER, not on click: clicking its trigger
// (an `<a>`) navigates instead of revealing the panel, so the portaled
@ -115,6 +135,38 @@ const COMPONENT_OVERRIDES: Record<
// `showBorder=false`, and the frame owns three of the seven tokens. The
// chip is outside the component, so clicking it cannot poison a hover
// state (the pointer is parked right after, as for every other opener).
// Chart is ONE recipe whose surface is spread over SIXTEEN demo routes, one
// per chart type: `/chart` mounts a line+area chart and NOTHING else, so
// two thirds of the contract (bar list, stacked bar, radar, smith, gauge,
// funnel, polar, heat, pie…) reads dead on it. Measured 2026-08-22: 58
// chart nodes on `/chart` against 302 in the whole page and the rest in
// sibling routes.
chart: {
// The tooltip and the crosshair only exist WHILE the pointer is over the
// plot — there is no trigger to click and no state to latch. Hovering the
// plot is what mounts them, and `openBy: 'hover'` also keeps the pointer
// parked there instead of moving it away before each token.
openBy: 'hover',
openWith: ['[data-chart-plot]'],
urls: [
'/uix/components/chart',
'/uix/components/line-chart',
'/uix/components/area-chart',
'/uix/components/bar-chart',
'/uix/components/scatter-chart',
'/uix/components/bubble-chart',
'/uix/components/pie-chart',
'/uix/components/sparkline',
'/uix/components/bar-list',
'/uix/components/bar-segment',
'/uix/components/radar-chart',
'/uix/components/smith-chart',
'/uix/components/polar-area',
'/uix/components/funnel',
'/uix/components/gauge',
'/uix/components/heatmap'
]
},
'text-gradient': {
openWith: [
'[data-uix-control]:has([data-uix-control-label]:text-is("showBorder")) [data-uix-chip]:text-is("true")'
@ -130,6 +182,11 @@ function sentinelFor(key: string): string {
if (/opacity|scale/.test(key)) return '0.123';
if (/duration/.test(key)) return '11.5s';
if (/letter-spacing/.test(key)) return '4.5px';
// BEFORE the colour test on purpose: a DIMENSION whose name merely contains a
// colour word was getting `rgb(1, 2, 3)` and, being invalid for a length,
// moved nothing and read dead — measured on `chart.slice-stroke-width`
// (2026-08-22), which the substring `stroke` was capturing.
if (/(width|size|radius|gap|height|padding|offset|thickness|inset)$/.test(key)) return '1234px';
if (
/color|bg$|fg$|border$|ring$|separator|-bg-|fill|stroke|outline$|glass|scrim$|track$/.test(key)
)
@ -159,205 +216,221 @@ async function main() {
.map((m) => m[1])
.filter((k) => !k.startsWith('_'));
// A component whose surface is spread over SEVERAL demo routes cannot be
// judged on one page: what that page does not mount reads dead. A token is
// LIVE when ANY of the component's surfaces follows it; only what nothing
// moves anywhere is dead. Each URL only re-tests what is still pending, so
// the common case (one route) costs exactly what it did before.
const origin = new URL(url).origin;
const urls = override.urls ? override.urls.map((path) => origin + path) : [url];
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 1200 } });
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForTimeout(600);
// A [data-busy] demo is still mutating — measuring it is measuring an instant.
await page
.waitForFunction(() => !document.querySelector('[data-busy]'), null, { timeout: 15000 })
.catch(() => {});
const live: string[] = [];
let pending = keys;
// Freeze transitions: a transitioned property reads its STARTING value right
// after the write, so a live token looked dead (measured on combobox's
// `box-shadow`, which transitions on `--duration-fast`).
await page.addStyleTag({
content: '*, *::before, *::after { transition: none !important; }'
});
for (const target of urls) {
if (!pending.length) break;
const page = await browser.newPage({ viewport: { width: 1440, height: 1200 } });
await page.goto(target, { waitUntil: 'networkidle' });
await page.waitForTimeout(600);
// A [data-busy] demo is still mutating — measuring it is measuring an instant.
await page
.waitForFunction(() => !document.querySelector('[data-busy]'), null, { timeout: 15000 })
.catch(() => {});
// Open whatever can be opened, so portaled parts are in the document — then
// BLUR: a mouse click on a text input matches `:focus-visible`, and the
// focus rule repaints the rest-state chrome (the input-border false
// negative). Popovers dismiss on outside pointerdown, not on blur, so the
// open state survives.
for (const sel of [
...(override.openWith ?? []),
`[data-${component}-trigger]`,
`[data-${component}-input]`,
`[data-${component}-stop]`
]) {
const el = page.locator(sel).first();
if (await el.count()) {
try {
if (override.openBy === 'hover') await el.hover({ timeout: 1500 });
else await el.click({ timeout: 1500 });
await page.waitForTimeout(400);
break;
} catch {
/* not clickable */
}
}
}
if (override.openBy !== 'hover')
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.());
// ...and PARK THE POINTER (never for a hover-opened panel: moving the cursor
// away is exactly what dismisses it). blur() drops the focus but Playwright leaves the
// cursor where it clicked, so `:hover` keeps matching — and a hover rule
// usually outweighs the rest / focus / invalid ones it shares a node with
// (measured on textarea 2026-08-21: hover (0,4,0) beats focus (0,3,0) beats
// invalid (0,2,0) beats rest (0,1,0), so THREE rest-state tokens read dead).
// Same class as the click-focus false negative above, and the half that fix
// left behind. The per-token hover pass re-hovers on purpose further down.
if (override.openBy !== 'hover') await page.mouse.move(0, 0);
// Freeze transitions: a transitioned property reads its STARTING value right
// after the write, so a live token looked dead (measured on combobox's
// `box-shadow`, which transitions on `--duration-fast`).
await page.addStyleTag({
content: '*, *::before, *::after { transition: none !important; }'
});
const dead: string[] = [];
const live: string[] = [];
// Runs before EVERY token. A component with no `content` part (textarea,
// any flat control) falls through to the click branch on every single key,
// so the pointer parking below is not belt-and-braces — without it the
// cursor sits on the input for the whole run.
const reopen = async () => {
const open = await page.locator(
override.attrPrefix ? `[data-${component}]` : `[data-${component}-content]`
).count();
if (open) return;
// Open whatever can be opened, so portaled parts are in the document — then
// BLUR: a mouse click on a text input matches `:focus-visible`, and the
// focus rule repaints the rest-state chrome (the input-border false
// negative). Popovers dismiss on outside pointerdown, not on blur, so the
// open state survives.
for (const sel of [
...(override.openWith ?? []),
`[data-${component}-trigger]`,
`[data-${component}-input]`
`[data-${component}-input]`,
`[data-${component}-stop]`
]) {
const el = page.locator(sel).first();
if (await el.count()) {
try {
if (override.openBy === 'hover') await el.hover({ timeout: 1000 });
else await el.click({ timeout: 1000 });
await page.waitForTimeout(250);
if (override.openBy !== 'hover') {
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.());
await page.mouse.move(0, 0);
}
return;
if (override.openBy === 'hover') await el.hover({ timeout: 1500 });
else await el.click({ timeout: 1500 });
await page.waitForTimeout(400);
break;
} catch {
/* keep trying */
/* not clickable */
}
}
}
};
if (override.openBy !== 'hover')
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.());
// ...and PARK THE POINTER (never for a hover-opened panel: moving the cursor
// away is exactly what dismisses it). blur() drops the focus but Playwright leaves the
// cursor where it clicked, so `:hover` keeps matching — and a hover rule
// usually outweighs the rest / focus / invalid ones it shares a node with
// (measured on textarea 2026-08-21: hover (0,4,0) beats focus (0,3,0) beats
// invalid (0,2,0) beats rest (0,1,0), so THREE rest-state tokens read dead).
// Same class as the click-focus false negative above, and the half that fix
// left behind. The per-token hover pass re-hovers on purpose further down.
if (override.openBy !== 'hover') await page.mouse.move(0, 0);
const staticPass = (key: string, value: string) =>
page.evaluate(
([kebab, token, val, props, prefix]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
);
// Write on :root AND on every component node: resolved names are
// declared per part (root/host), and a portaled panel never sees the
// component root. Writing the var everywhere cannot fake a win at the
// PROPERTY level — a declaration another rule (or an inline style)
// beats stays beaten.
const hosts = () => [document.documentElement, ...all()];
const sized = all().filter((n) => n.hasAttribute('data-size'));
const original = sized.map((n) => n.getAttribute('data-size'));
const snap = () =>
all()
.map((n) => {
const cs = getComputedStyle(n);
const own = (props as string[]).map((p) => cs[p as never]).join('|');
const b = getComputedStyle(n, '::before');
const a = getComputedStyle(n, '::after');
// ::placeholder IS readable through getComputedStyle - measured
// 2026-08-21 on textarea (the sentinel colour came straight back).
// The next-features §13 note saying it is not was wrong, and it had
// already cost command.input-placeholder-fg a hand-checked entry.
const ph = getComputedStyle(n, '::placeholder');
const pseudo = (props as string[])
.map((p) => `${b[p as never]}~${a[p as never]}~${ph[p as never]}`)
.join('|');
return own + '#' + pseudo;
})
.join('@');
let moved = false;
for (const size of ['md', 'xs', 'sm', 'lg', 'xl']) {
for (const n of sized) n.setAttribute('data-size', size);
const before = snap();
for (const h of hosts()) h.style.setProperty(`--${kebab}-${token}`, val as string);
const after = snap();
for (const h of hosts()) h.style.removeProperty(`--${kebab}-${token}`);
if (before !== after) {
moved = true;
break;
// Runs before EVERY token. A component with no `content` part (textarea,
// any flat control) falls through to the click branch on every single key,
// so the pointer parking below is not belt-and-braces — without it the
// cursor sits on the input for the whole run.
const reopen = async () => {
const open = await page.locator(
override.attrPrefix ? `[data-${component}]` : `[data-${component}-content]`
).count();
if (open) return;
for (const sel of [
...(override.openWith ?? []),
`[data-${component}-trigger]`,
`[data-${component}-input]`
]) {
const el = page.locator(sel).first();
if (await el.count()) {
try {
if (override.openBy === 'hover') await el.hover({ timeout: 1000 });
else await el.click({ timeout: 1000 });
await page.waitForTimeout(250);
if (override.openBy !== 'hover') {
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.());
await page.mouse.move(0, 0);
}
return;
} catch {
/* keep trying */
}
}
sized.forEach((n, i) => (original[i] ? n.setAttribute('data-size', original[i]!) : null));
return moved;
},
[component, key, value, PROPS, attrPrefix] as const
);
// Hover pass — a `hover-*` token can only move a computed value while some
// node is really hovered (tree-grid's hover-row-bg, gradient-picker's
// hover-preset-border were false negatives without it).
const hoverPass = async (key: string, value: string) => {
const count = await page.evaluate(
(prefix) =>
[...document.querySelectorAll('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
).length,
attrPrefix
);
for (let i = 0; i < Math.min(count, 30); i++) {
const handle = await page.evaluateHandle(
([prefix, idx]) =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
)[idx as number] ?? null,
[attrPrefix, i] as const
);
const el = handle.asElement();
if (!el) continue;
try {
await el.hover({ timeout: 800 });
} catch {
continue;
}
const moved = await page.evaluate(
([prefix, token, val, props, idx, kebab]) => {
};
const staticPass = (key: string, value: string) =>
page.evaluate(
([kebab, token, val, props, prefix]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
);
const node = all()[idx as number];
if (!node) return false;
const snap = () => {
const cs = getComputedStyle(node);
return (props as string[]).map((p) => cs[p as never]).join('|');
};
const before = snap();
for (const h of [document.documentElement, ...all()])
h.style.setProperty(`--${kebab}-${token}`, val as string);
const after = snap();
for (const h of [document.documentElement, ...all()])
h.style.removeProperty(`--${kebab}-${token}`);
return before !== after;
// Write on :root AND on every component node: resolved names are
// declared per part (root/host), and a portaled panel never sees the
// component root. Writing the var everywhere cannot fake a win at the
// PROPERTY level — a declaration another rule (or an inline style)
// beats stays beaten.
const hosts = () => [document.documentElement, ...all()];
const sized = all().filter((n) => n.hasAttribute('data-size'));
const original = sized.map((n) => n.getAttribute('data-size'));
const snap = () =>
all()
.map((n) => {
const cs = getComputedStyle(n);
const own = (props as string[]).map((p) => cs[p as never]).join('|');
const b = getComputedStyle(n, '::before');
const a = getComputedStyle(n, '::after');
// ::placeholder IS readable through getComputedStyle - measured
// 2026-08-21 on textarea (the sentinel colour came straight back).
// The next-features §13 note saying it is not was wrong, and it had
// already cost command.input-placeholder-fg a hand-checked entry.
const ph = getComputedStyle(n, '::placeholder');
const pseudo = (props as string[])
.map((p) => `${b[p as never]}~${a[p as never]}~${ph[p as never]}`)
.join('|');
return own + '#' + pseudo;
})
.join('@');
let moved = false;
for (const size of ['md', 'xs', 'sm', 'lg', 'xl']) {
for (const n of sized) n.setAttribute('data-size', size);
const before = snap();
for (const h of hosts()) h.style.setProperty(`--${kebab}-${token}`, val as string);
const after = snap();
for (const h of hosts()) h.style.removeProperty(`--${kebab}-${token}`);
if (before !== after) {
moved = true;
break;
}
}
sized.forEach((n, i) => (original[i] ? n.setAttribute('data-size', original[i]!) : null));
return moved;
},
[attrPrefix, key, value, PROPS, i, component] as const
[component, key, value, PROPS, attrPrefix] as const
);
if (moved) return true;
}
return false;
};
for (const key of keys) {
await reopen();
const value = sentinelFor(key);
let moved = await staticPass(key, value);
if (!moved && /hover/.test(key)) moved = await hoverPass(key, value);
if (moved) live.push(key);
else dead.push(key);
// Hover pass — a `hover-*` token can only move a computed value while some
// node is really hovered (tree-grid's hover-row-bg, gradient-picker's
// hover-preset-border were false negatives without it).
const hoverPass = async (key: string, value: string) => {
const count = await page.evaluate(
(prefix) =>
[...document.querySelectorAll('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
).length,
attrPrefix
);
for (let i = 0; i < Math.min(count, 30); i++) {
const handle = await page.evaluateHandle(
([prefix, idx]) =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
)[idx as number] ?? null,
[attrPrefix, i] as const
);
const el = handle.asElement();
if (!el) continue;
try {
await el.hover({ timeout: 800 });
} catch {
continue;
}
const moved = await page.evaluate(
([prefix, token, val, props, idx, kebab]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
);
const node = all()[idx as number];
if (!node) return false;
const snap = () => {
const cs = getComputedStyle(node);
return (props as string[]).map((p) => cs[p as never]).join('|');
};
const before = snap();
for (const h of [document.documentElement, ...all()])
h.style.setProperty(`--${kebab}-${token}`, val as string);
const after = snap();
for (const h of [document.documentElement, ...all()])
h.style.removeProperty(`--${kebab}-${token}`);
return before !== after;
},
[attrPrefix, key, value, PROPS, i, component] as const
);
if (moved) return true;
}
return false;
};
const stillDead: string[] = [];
for (const key of pending) {
await reopen();
const value = sentinelFor(key);
let moved = await staticPass(key, value);
if (!moved && /hover/.test(key)) moved = await hoverPass(key, value);
if (moved) live.push(key);
else stillDead.push(key);
}
pending = stillDead;
await page.close();
}
await browser.close();
const dead = pending;
const ledger = SENTINEL_EXCEPTIONS[component] ?? {};
const adjudicated = dead.filter((k) => k in ledger);

@ -159,8 +159,46 @@ tick measures as "no overlap" while reading as contact.
## Theming
Series: `var(--color-{role}-solid)`. Axis lines: `--color-border-default`;
grid: `--color-border-subtle`; labels: `--color-content-secondary`; title:
`--color-content-primary`. Pie slice separators: `--color-surface-default`.
Geometry + tooltip surface: the `--chart-*` recipe tokens (`lib/recipes/base.ts`).
`chart.css` is code-split (imported from `chart.svelte`).
Contrato en `lib/recipes/base.ts > chart` — **39 claves** (17 + 22 el
2026-08-22). Alcance del censo: **49 %** (era 9 %). **Guard R-5.4**
(`npm run theming:sentinel -- chart <url>`): **37/39** en vivo; las dos que
callan son `transition-duration` / `-ease`, adjudicadas con su medición (el
guard congela transiciones a propósito).
**Series**: `var(--color-{role}-solid)`, inline por serie.
**El texto no tiene tokens de chart, y es doctrina**: toma su ROL DE TAMAÑO de
los estilos con nombre del canon —`caption` para las anotaciones (etiquetas de
eje, leyenda, tooltip, etiquetas smith) y `label` para el texto de cromo
(título, filas del bar-list, funnel)— y su TINTA y su PESO de las ranuras del
sistema (`--color-content-*`, `--font-weight-*`), que es para lo que existen.
La receta no acuña tipografía: mismo precedente que `code-block`, `kbd` y
`announce`. Un `Chart` dentro de un tema que retoca `caption` le sigue.
**Lo que la receta SÍ posee es la tinta de lo que no es texto**, porque nada
más puede nombrarlo, y cada clave es UN concepto que el CSS repite:
```
--chart-axis-fg eje + rejilla del smith + conector del funnel
--chart-grid-fg rejilla + anillos y radios del radar
--chart-separator-fg el pelo de superficie que separa marcas contiguas:
porción, punto activo, punto smith, pétalo, segmento
--chart-smith-boundary-fg el borde del disco RF, más fuerte que su rejilla
--chart-track-bg el raíl del bar-list y el del gauge
```
Más la geometría del cromo (`gap`, los pares `row-gap`/`column-gap` de leyenda
y listas, `swatch-size`, `track-height`, radios y separaciones del bar-segment)
y la geometría de marca que ya existía (grosores, radios, `area-opacity`,
timing, superficie del tooltip, crosshair).
`track-height` lee **el bundle de talla** (`--size-sm-control-height`), no el
primitivo crudo `--control-height-sm` que el CSS traía: el crudo es lo que
`recipe-css-contract` prohíbe, y su `var(--space-7)` de respaldo estaba muerto.
**Fuera de contrato por identidad**: los nueve `100%` («ocupa a tu padre») y el
`min-inline-size: max-content` del tooltip. Y una escotilla sin cerrar: el
`--font-size-xl` del número del gauge, que no tiene estilo con nombre que juegue
ese papel (un readout de dato no es un `h2`) — anotado en next-features §13.
`chart.css` es code-split (se importa desde `chart.svelte`).

@ -4,14 +4,24 @@
* geometric constant (stroke widths, radii, opacities, transition timing,
* tooltip surface) comes from a `--chart-*` recipe token sourced from canonical
* primitives — see `lib/recipes/base.ts`. Series fills/strokes are themed per
* series inline (`var(--color-{role}-solid)`); colours for chrome come from the
* global content/border/surface tokens.
* series inline (`var(--color-{role}-solid)`).
*
* TEXT takes its SIZE ROLE from the canon's named styles — `caption` for the
* annotations (axis labels, legend, tooltip, smith labels) and `label` for the
* chrome text (title, bar-list rows, funnel) — and its INK and WEIGHT from the
* system's content / weight slots, which is what those slots are for. The
* recipe mints NO typography of its own (theming §6, D-TH.2 precedent:
* code-block / kbd / announce read the same layer).
*
* What the recipe DOES own, because nothing else can name it: the ink of the
* lines and fills that are not text — axis, grid, the hairline that separates
* adjacent marks, the rails — plus its geometry.
*/
[data-chart-frame] {
display: flex;
flex-direction: column;
gap: var(--space-2);
gap: var(--chart-gap);
inline-size: 100%;
}
[data-chart-plot] {
@ -34,24 +44,24 @@
place-items: center;
block-size: 100%;
color: var(--color-content-secondary);
font-size: var(--font-size-sm);
font-size: var(--style-label-font-size);
}
/* ── Axes ─────────────────────────────────────────────────────────────── */
[data-chart-axis-line],
[data-chart-axis-tick] {
stroke: var(--color-border-default);
stroke: var(--chart-axis-fg);
stroke-width: var(--chart-axis-width);
}
[data-chart-axis-label] {
fill: var(--color-content-secondary);
font-size: var(--font-size-xs);
font-size: var(--style-caption-font-size);
}
/* ── Grid ─────────────────────────────────────────────────────────────── */
[data-chart-grid-line] {
stroke: var(--color-border-subtle);
stroke: var(--chart-grid-fg);
stroke-width: var(--chart-grid-width);
}
@ -79,7 +89,7 @@
/* Pie / donut slices, separated by a hairline of the page surface. */
[data-chart-slice] {
stroke: var(--color-surface-default);
stroke: var(--chart-separator-fg);
stroke-width: var(--chart-slice-stroke-width);
/* So the `bloom` enter scales each slice from the chart (viewport) centre — a radial grow. */
transform-box: view-box;
@ -95,7 +105,7 @@
}
[data-chart-active-dot] {
r: var(--chart-point-radius);
stroke: var(--color-surface-default);
stroke: var(--chart-separator-fg);
stroke-width: var(--chart-axis-width);
pointer-events: none;
}
@ -120,7 +130,7 @@
border: var(--chart-axis-width) solid var(--chart-tooltip-border);
border-radius: var(--chart-tooltip-radius);
box-shadow: var(--chart-tooltip-shadow);
font-size: var(--font-size-xs);
font-size: var(--style-caption-font-size);
color: var(--color-content-primary);
min-inline-size: max-content;
}
@ -138,9 +148,9 @@
color: var(--color-content-secondary);
}
[data-chart-tooltip-swatch] {
inline-size: 0.6em;
block-size: 0.6em;
border-radius: var(--radius-sm);
inline-size: var(--chart-tooltip-swatch-size);
block-size: var(--chart-tooltip-swatch-size);
border-radius: var(--chart-swatch-radius);
flex: none;
}
[data-chart-tooltip-value] {
@ -157,13 +167,13 @@
position: relative; /* anchor for the pie's hover tooltip */
display: flex;
flex-direction: column;
gap: var(--space-2);
gap: var(--chart-gap);
inline-size: 100%;
}
[data-chart-title] {
margin: 0;
font-size: var(--font-size-sm);
font-size: var(--style-label-font-size);
font-weight: var(--font-weight-medium);
color: var(--color-content-primary);
}
@ -171,22 +181,23 @@
[data-chart-legend] {
display: flex;
flex-wrap: wrap;
gap: var(--space-1-5) var(--space-4);
row-gap: var(--chart-legend-row-gap);
column-gap: var(--chart-legend-column-gap);
align-items: center;
}
[data-chart-legend-item] {
display: inline-flex;
align-items: center;
gap: var(--space-1-5);
font-size: var(--font-size-xs);
gap: var(--chart-legend-item-gap);
font-size: var(--style-caption-font-size);
color: var(--color-content-secondary);
}
[data-chart-swatch] {
inline-size: 0.75em;
block-size: 0.75em;
border-radius: var(--radius-sm);
inline-size: var(--chart-swatch-size);
block-size: var(--chart-swatch-size);
border-radius: var(--chart-swatch-radius);
flex: none;
}
@ -199,7 +210,8 @@
display: grid;
grid-template-columns: minmax(0, max-content) 1fr auto;
align-items: center;
gap: var(--space-2) var(--space-3);
row-gap: var(--chart-bar-list-row-gap);
column-gap: var(--chart-bar-list-column-gap);
inline-size: 100%;
}
[data-chart-bar-list-item] {
@ -209,25 +221,25 @@
align-items: center;
}
[data-chart-bar-list-label] {
font-size: var(--font-size-sm);
font-size: var(--style-label-font-size);
color: var(--color-content-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-chart-bar-list-track] {
block-size: var(--control-height-sm, var(--space-7));
background: var(--color-surface-muted);
border-radius: var(--radius-sm);
block-size: var(--chart-track-height);
background: var(--chart-track-bg);
border-radius: var(--chart-track-radius);
overflow: hidden;
}
[data-chart-bar-list-bar] {
block-size: 100%;
border-radius: var(--radius-sm);
border-radius: var(--chart-bar-radius);
transition: inline-size var(--chart-transition-duration) var(--chart-transition-ease);
}
[data-chart-bar-list-value] {
font-size: var(--font-size-sm);
font-size: var(--style-label-font-size);
color: var(--color-content-secondary);
font-variant-numeric: tabular-nums;
text-align: end;
@ -238,19 +250,19 @@
position: relative; /* anchor for the hover tooltip */
display: flex;
flex-direction: column;
gap: var(--space-3);
gap: var(--chart-bar-segment-gap);
inline-size: 100%;
}
[data-chart-bar-segment-track] {
display: flex;
gap: var(--border-width-medium);
block-size: var(--control-height-sm, var(--space-7));
border-radius: var(--radius-md);
gap: var(--chart-bar-segment-track-gap);
block-size: var(--chart-track-height);
border-radius: var(--chart-bar-segment-track-radius);
overflow: hidden;
}
[data-chart-bar-segment-fill] {
block-size: 100%;
min-inline-size: var(--border-width-thick);
min-inline-size: var(--chart-bar-segment-fill-min-inline-size);
transition:
inline-size var(--chart-transition-duration) var(--chart-transition-ease),
opacity var(--chart-transition-duration) var(--chart-transition-ease);
@ -261,13 +273,14 @@
[data-chart-bar-segment-legend] {
display: flex;
flex-wrap: wrap;
gap: var(--space-2) var(--space-4);
row-gap: var(--chart-bar-segment-legend-row-gap);
column-gap: var(--chart-bar-segment-legend-column-gap);
}
[data-chart-bar-segment-legend-item] {
display: inline-flex;
align-items: center;
gap: var(--space-1-5);
font-size: var(--font-size-xs);
gap: var(--chart-legend-item-gap);
font-size: var(--style-caption-font-size);
color: var(--color-content-secondary);
}
[data-chart-bar-segment-legend-value] {
@ -279,7 +292,7 @@
[data-chart-radar-ring],
[data-chart-radar-spoke] {
fill: none;
stroke: var(--color-border-subtle);
stroke: var(--chart-grid-fg);
stroke-width: var(--chart-grid-width);
}
[data-chart-radar-area] {
@ -310,34 +323,34 @@
/* Smith chart (RF reflection-coefficient disk). */
[data-chart-smith-boundary] {
fill: none;
stroke: var(--color-content-secondary);
stroke: var(--chart-smith-boundary-fg);
stroke-width: var(--border-width-medium);
}
[data-chart-smith-grid] {
fill: none;
stroke: var(--color-border-default);
stroke: var(--chart-axis-fg);
stroke-width: var(--border-width-thin);
}
[data-chart-smith-label] {
fill: var(--color-content-secondary);
font-size: var(--font-size-xs);
font-size: var(--style-caption-font-size);
font-variant-numeric: tabular-nums;
pointer-events: none;
}
[data-chart-smith-point] {
stroke: var(--color-surface-default);
stroke: var(--chart-separator-fg);
stroke-width: var(--border-width-medium);
}
[data-chart-smith-point-label] {
fill: var(--color-content-primary);
font-size: var(--font-size-xs);
font-size: var(--style-caption-font-size);
font-weight: var(--font-weight-medium);
pointer-events: none;
}
/* ── Polar area (Nightingale / coxcomb petals) ────────────────────────────── */
[data-chart-polar-petal] {
stroke: var(--color-surface-default);
stroke: var(--chart-separator-fg);
stroke-width: var(--chart-slice-stroke-width);
fill-opacity: 0.9;
/* So the `bloom` enter scales each petal from the chart centre (radial grow). */
@ -350,21 +363,21 @@
/* ── Funnel (tapering stages) ─────────────────────────────────────────────── */
[data-chart-funnel-segment] {
stroke: var(--color-surface-default);
stroke: var(--chart-separator-fg);
stroke-width: var(--chart-slice-stroke-width);
}
[data-chart-funnel-label] {
font-size: var(--font-size-sm);
font-size: var(--style-label-font-size);
font-weight: var(--font-weight-medium);
pointer-events: none;
}
/* Right-side labels (always legible — theme text colour, never on a coloured fill). */
[data-chart-funnel-connector] {
stroke: var(--color-border-default);
stroke: var(--chart-axis-fg);
stroke-width: var(--chart-grid-width);
}
[data-chart-funnel-side] {
font-size: var(--font-size-sm);
font-size: var(--style-label-font-size);
}
[data-chart-funnel-name] {
fill: var(--color-content-primary);
@ -377,7 +390,7 @@
/* ── Gauge (radial single value) ──────────────────────────────────────────── */
[data-chart-gauge-track] {
fill: var(--color-surface-muted);
fill: var(--chart-track-bg);
}
[data-chart-gauge-value] {
transition: opacity var(--chart-transition-duration) var(--chart-transition-ease);

@ -2486,6 +2486,28 @@
--chart-tooltip-gap: var(--space-1-5);
--chart-crosshair-fg: var(--color-border-strong);
--chart-crosshair-width: var(--border-width-thin);
--chart-axis-fg: var(--color-border-default);
--chart-grid-fg: var(--color-border-subtle);
--chart-separator-fg: var(--color-surface-default);
--chart-smith-boundary-fg: var(--color-content-secondary);
--chart-track-bg: var(--color-surface-muted);
--chart-track-height: var(--size-sm-control-height);
--chart-track-radius: var(--radius-sm);
--chart-gap: var(--space-2);
--chart-legend-row-gap: var(--space-1-5);
--chart-legend-column-gap: var(--space-4);
--chart-legend-item-gap: var(--space-1-5);
--chart-swatch-size: 0.75em;
--chart-swatch-radius: var(--radius-sm);
--chart-tooltip-swatch-size: 0.6em;
--chart-bar-list-row-gap: var(--space-2);
--chart-bar-list-column-gap: var(--space-3);
--chart-bar-segment-gap: var(--space-3);
--chart-bar-segment-track-gap: var(--border-width-medium);
--chart-bar-segment-track-radius: var(--radius-md);
--chart-bar-segment-fill-min-inline-size: var(--border-width-thick);
--chart-bar-segment-legend-row-gap: var(--space-2);
--chart-bar-segment-legend-column-gap: var(--space-4);
--qr-code-fg: #18181b;
--qr-code-bg: #ffffff;
--qr-code-size-xs: 96px;

@ -3746,7 +3746,45 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'tooltip-padding': 'var(--space-2)',
'tooltip-gap': 'var(--space-1-5)',
'crosshair-fg': 'var(--color-border-strong)',
'crosshair-width': 'var(--border-width-thin)'
'crosshair-width': 'var(--border-width-thin)',
// ── The ink of everything that is NOT text (2026-08-22) ──────────────
// Text takes its size role from the named styles (`caption` / `label`)
// and its ink from the content slots — the recipe mints no typography.
// These four are lines and fills nothing else can name, and each is ONE
// concept the CSS repeats: the axis ink also draws the smith grid and the
// funnel connector; the grid ink also draws the radar rings; and
// `separator-fg` is the hairline of page surface that keeps adjacent
// marks apart — pie slice, active dot, smith point, polar petal, funnel
// segment: five parts, one idea. Naming them per part would be five keys
// carrying one value.
'axis-fg': 'var(--color-border-default)',
'grid-fg': 'var(--color-border-subtle)',
'separator-fg': 'var(--color-surface-default)',
// The RF disk outline is deliberately stronger than the grid it encloses.
'smith-boundary-fg': 'var(--color-content-secondary)',
// ── Rails (bar list, stacked bar, gauge) ─────────────────────────────
// `track-height` reads the size BUNDLE, not `--control-height-sm`: the raw
// coordinate is what `recipe-css-contract` forbids, and the CSS carried it
// (behind a dead `--space-7` fallback — the primitive does exist).
'track-bg': 'var(--color-surface-muted)',
'track-height': 'var(--size-sm-control-height)',
'track-radius': 'var(--radius-sm)',
// ── Chrome geometry ──────────────────────────────────────────────────
gap: 'var(--space-2)',
'legend-row-gap': 'var(--space-1-5)',
'legend-column-gap': 'var(--space-4)',
'legend-item-gap': 'var(--space-1-5)',
'swatch-size': '0.75em',
'swatch-radius': 'var(--radius-sm)',
'tooltip-swatch-size': '0.6em',
'bar-list-row-gap': 'var(--space-2)',
'bar-list-column-gap': 'var(--space-3)',
'bar-segment-gap': 'var(--space-3)',
'bar-segment-track-gap': 'var(--border-width-medium)',
'bar-segment-track-radius': 'var(--radius-md)',
'bar-segment-fill-min-inline-size': 'var(--border-width-thick)',
'bar-segment-legend-row-gap': 'var(--space-2)',
'bar-segment-legend-column-gap': 'var(--space-4)'
},
'qr-code': {
fg: '#18181b',

Loading…
Cancel
Save

Powered by TurnKey Linux.