# PLAN — Theming: que TODO componente sea personalizable por un tema (eje `theme-reach`) > **Estado (act. 2026-08-23): EN EJECUCIÓN — alcance global 65 %, 25 > componentes al 100 %, 24 sin contrato, 12 por debajo del 20 %. Vocabulario > CERRADO (codemod + R-5.3 + muro de tipos), bloques F2-A y F2-B CERRADOS y la > cola avanzando: dieciséis componentes el 2026-08-23. Las D-TH.1…8 de §4 > FIRMADAS TODAS el 2026-08-20** (más las > tres decisiones hermanas del mismo día: familia calendar = capa compartida, > mandato Field ejecutable, hover→capa de estado — ver «Firmas del 2026-08-20» > del CONTINUE). El registro con cifras y commits está en §8, y el punto > de entrada diario es [`CONTINUE-theming.md`](./CONTINUE-theming.md). Toda > decisión NUEVA que surja se sigue presentando una por mensaje, con las > cinco preguntas. > > **Origen (conversación 2026-08-19)**: al arreglar `navigation-menu` salió que > el radio de su trigger vivía en un privado clavado a `--radius-default` y que > el contrato del componente sólo publicaba tres knobs: ningún tema podía decir > «píldora» sin mover el radio del sistema entero. El autor, textual: **«esto > ha abierto una duda importante, y es si los componentes creados son > themables; si no lo son, es un error como framework. Necesitaríamos > desarrollar un plan para auditar todos los componentes, verificar qué grado > de personalización soportan y desarrollar una corrección a cada uno de ellos > para que sean totalmente personalizables.»** Y sobre la ejecución: **«lo > ejecutará Opus, y Opus falla mucho, por lo que tienes que establecer procesos > de verificación en todo el plan.»** Este documento obedece a las dos frases: > §1 es la auditoría MEDIDA (reproducible por script, no por memoria), §5 son > las fases, y §7 es el protocolo de verificación que cada sesión ejecuta sin > excepción. > > **Kickoff para sesión nueva**: _«Lee `docs/process/PLAN-theming.md` ENTERO y > después `CONTINUE-theming.md`; si las D-TH de §4 están firmadas, ejecuta el > siguiente bloque de §5 con el protocolo de §7, un componente por commit; si > no, PARA y preséntalas una a una.»_ > > Fuentes de doctrina que este plan cita y NO copia: > [`docs/theming/reference.md`](../theming/reference.md) §1bis, §3, §5, §6, > §7, §12, §16 · [`docs/canon/tsc.md`](../canon/tsc.md) · > [`docs/canon/recipe-contract.md`](../canon/recipe-contract.md) §1–§5 · > [`docs/theming/motion.md`](../theming/motion.md) §"When both write > `animation` on ONE node" · [`docs/guides/component-guide.md`](../guides/component-guide.md) > §Build contract · [`src/uix/eidos/components/README.md`](../../src/uix/eidos/components/README.md) > · el precedente ejecutado y firmado: [`PLAN-sidebar.md`](./PLAN-sidebar.md) > §3 + F3 (el eje `size` de la fila, 2026-08-19) y `navigation-menu` > (`ae9e277fa` · `a82794845`, el cromo del trigger en tokens públicos). --- ## 0. Veredicto corto 1. **La doctrina ya lo exige; los guards no lo comprueban.** recipe-contract §1: «Every recipe declares its knobs in `lib/recipes/base.ts`»; theming §6: el token público de componente es `--{c}-{slot}`; la TSC decide dónde se emite. Pero R-1…R-4 comprueban que **no haya literales** (y que lo que haya sea un `var(`), no que cada knob sea **alcanzable por un tema**. Una receta con todo en `var(--radius-md)` y en privados `--_{c}-*` pasa `component:audit` en PASS y la dimensión 10 «Adopción theming» de la ficha en ✓ — y sólo se puede temar moviendo el planeta. Es un error de framework porque rompe la promesa que el framework hace por escrito. 2. **Medido, no opinado** (§1, `scripts/theming-census.ts`): de 5.205 knobs de apariencia en 162 recetas, **1.621 (33 %) pasan por un token público del componente**; 1.880 atan directo a un primitivo global; 856 a privados; 614 son literales. **62 componentes no tienen una sola entrada en el contrato**; 59 tienen alcance < 20 %; 6 están al 100 %. `navigation-menu`, tras el trabajo de hoy, está en 44 %: el eje no se cierra arreglando un componente. 3. **La corrección es mecánica y ya tiene forma firmada**: la de la fila del `Sidebar` (D-SB.5/D-SB.6, `PLAN-sidebar.md` §3) y la del trigger del nav (`ae9e277fa`): tokens públicos por talla que apuntan al bundle `--size-{k}-*`, nombre RESUELTO por `data-size`, acentos de estado en `background-color`, y **el default queda idéntico** — sólo cambia quién puede moverlo. Lo que NO hay que hacer: rediseñar nada por el camino. 4. **Sin guard no hay cierre**: el censo de hoy se repetirá en seis meses si R-5 no existe. F0 crea el guard en `warn` con la cifra de hoy como suelo; F3 lo gradúa a `error` cuando el backfill termina. Igual que hizo R-4.x el 2026-07-02. --- ## 1. Inventario medido (2026-08-19) **Instrumento**: `node --import tsx/esm scripts/theming-census.ts` (añadido con este plan; `--json` para máquina; `--only {c}` por componente). Cuenta, por receta CSS, cada declaración de APARIENCIA (lista `KNOB_PROPS` en el script: fondo, tinta, borde, radio, sombra, padding, gap, tipografía, tamaño, opacidad, fill/stroke, filtros) y la clasifica por el ORIGEN de su valor: | Clase | Qué es | ¿La alcanza un tema de componente? | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `public` | `var(--{c}-…)` — token del contrato del componente | **Sí** | | `private` | `var(--_{c}-…)` — nombre interno | No (salvo que el privado DERIVE de un público — el script no lo distingue, cuenta como no) | | `global` | cualquier otro `var(--…)`: `--space-*`, `--radius-*`, `--color-*`, `--font-size-*`, `--size-*`… | Sólo moviendo el sistema | | `literal` | sin `var(`: `8px`, `1.25`, `#fff` | No, y además viola R-4/R-2 | | `system` | sistemas transversales que una receta CONSUME: capa de estado, anillo de foco, planos de depth, motion, bandas z, opacidad, shape, `floating-gap` | Sí, **a nivel de sistema**, por diseño (recipe-contract §2) — fuera del ratio | **Alcance** = `public / (public + private + global + literal)`. ### 1.1 Totales | Medida | Valor | | -------------------------------------- | ---------------- | | Componentes con receta CSS | 162 | | Knobs de apariencia | 5.205 | | Por token público del componente | **1.621 (33 %)** | | Por privado | 856 | | Directo a primitivo global | 1.880 | | Literales | 614 | | Sistema transversal (fuera del ratio) | 234 | | **Sin ninguna entrada en el contrato** | **62** | | Alcance < 20 % | 59 | | Alcance = 100 % | 6 | | Con eje `data-size` en la receta | 82 | ### 1.2 Por familia (propuesta de familias en D-TH.4 — el autor la firma o la cambia) | Familia | Comp. | Knobs | Alcance | Sin contrato | Fuera del contrato | | ----------------------------------- | ----: | ----: | ------: | -----------: | -----------------: | | Tiempo (chronos · fecha/hora) | 15 | 879 | 9 % | 9 | 774 | | Campos | 24 | 905 | 38 % | 9 | 526 | | Controles · marcas | 33 | 867 | 39 % | 9 | 502 | | Menús · listas · navegación | 24 | 735 | 37 % | 10 | 432 | | Superficies · overlays · estado | 34 | 807 | 45 % | 10 | 429 | | WIP (`palabras`) | 1 | 471 | 22 % | 1 | 363 | | Texto · tipografía | 12 | 238 | 9 % | 10 | 217 | | Media · chat | 9 | 283 | 61 % | 1 | 104 | | Layout (primitivos) | 9 | 14 | 93 % | 3 | 1 | | (`separator`, sin familia asignada) | 1 | 6 | 67 % | 0 | 2 | ### 1.3 Los tres defectos que el censo NO ve y la ejecución sí tiene que mirar El script es un regex sobre texto: sobre-reporta, nunca infra-reporta. Tres clases de defecto de theming que un alcance del 100 % no garantiza: - **Privado que no deriva de un público** (`--_c-radius: var(--radius-default)`): cuenta como `private`; la corrección es que el privado lea el público o desaparezca. - **Velo o acento en el nodo equivocado**: `archetype: 'item'` en un `
  • ` envoltorio (`navigation-menu`, `822b78cf2`), o el shorthand `background` matando la capa de estado (`Sidebar` F3, `navigation-menu` `ae9e277fa`). No es un knob: es un nodo. Se detecta midiendo desde el píxel hacia arriba (§7.3). - **Doble animación al mover un sello** a una superficie con animación propia (`navigation-menu` `822b78cf2`: `present-rise` + la entrada de receta). Se detecta con un registro de `animationstart/end`, no con `getComputedStyle`. ### 1.4 Tabla completa por componente Al final del documento (§9), ordenada por alcance. Se REGENERA con el script en cada bloque; no se edita a mano. --- ## 2. Contrato objetivo — qué significa «totalmente personalizable» (los cuatro ejes) Un componente es personalizable cuando un tema puede moverlo **sin tocar el sistema ni la receta**, en estos ejes: | Eje | Mecanismo canónico | Cómo se comprueba | | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | **A · Tokens** (forma, color, espacio, tipografía) | todo knob de apariencia lee `--{c}-{slot}` del contrato (`recipes/base.ts`), nombre RESUELTO por talla/estado; los privados sólo derivan de públicos | censo al 100 % **+** prueba de centinela (§7.4) | | **B · Talla** | `size` → `data-size` en el wrapper; tokens `{part}-{eje}-{k}` apuntando al bundle `--size-{k}-*` (theming §5; recipe-contract §1) | computed por talla = bundle; `md` idéntico al default anterior | | **C · Color / variante** | `data-color` / `data-variant` vía paleta (`--{c}-{role}-{slot}`, `--{c}-palette-*`), los 9 roles (theming §4) | cada variante lee tokens, ninguna un `--scale-*` (R-4.6) | | **D · Estados** | hover/press/selected neutros = la capa del sistema en el nodo CON forma; acentos de estado (`on-bg`…) en `background-color` para que la capa componga | `background-image` presente en hover, `
  • `/envoltorios sin velo, abierto + hover = acento + capa | **Regla de oro del eje**: **el default no cambia.** Cada componente se mide antes y después en sus estados (reposo · hover · activo/abierto · disabled · foco) y tallas, y el diff de computed styles es **cero** salvo donde una decisión firmada diga lo contrario. Este eje mueve QUIÉN puede cambiar cada cosa, no QUÉ se ve. --- ## 3. Contrato objetivo — la regla R-5 «alcance de tema» Nueva regla de `component-audit` (familia R-5), misma mecánica que R-4.x: - **R-5.1** — Toda declaración de apariencia (`KNOB_PROPS`) en una receta lee (a) un público `var(--{c}-…)`, (b) un privado `--_{c}-*` cuya definición en la misma receta lee un público, o (c) un token de sistema transversal (la lista `SYSTEM` del censo). Otra cosa es fallo. - **R-5.2** — Todo componente con receta tiene entrada en `recipes/base.ts`. - **R-5.3** — Los nombres de los tokens siguen recipe-contract §1 (ejes lógicos, `{part}-{eje}-{k}`) y theming §6 regla 7 (slots de color: `bg, fg, border, on-bg, on-fg, on-border, hover-bg, on-hover-bg`). **Hoy no se cumple** ni en recetas viejas (`tabs`: `trigger-color-*`) ni en las de hoy (`navigation-menu`: `trigger-bg-active`, `trigger-color-active`) — ver D-TH.6. - **Válvula** (recipe-contract §3): `R-5.1 exception: ` en el README del componente, greppable, UNA por knob o grupo de knobs con la razón (p. ej. «glifo ∝ diámetro, excepción canónica avatar/marker»). Sin razón escrita no hay excepción. - **Severidad**: `warn` desde F0 con el suelo del censo; `error` en F3 (D-TH.7 — firmada con enmienda: **R-5.3 gradúa a `error` directo tras el codemod**, su deuda muere en el mismo pass; R-5.1/5.2 esperan F3). --- ## 4. Decisiones que firma el autor (una por mensaje, con sus cinco preguntas) | Id | Decisión | Propuesta | Estado | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **D-TH.1** | ¿R-5 es regla DURA? («todo knob público salvo sistema transversal o excepción firmada») | Sí. Es lo que recipe-contract §1 ya dice sin guard. | **FIRMADA 2026-08-20** tal cual. Ancla: la medición 2026-07-01 (canon sin guard deriva 30-85 %) y el mandato origen del eje. | | **D-TH.2** | Perímetro de «knob» | La lista `KNOB_PROPS` del script (apariencia; layout fuera). Cambios a la lista = cambio del guard, firmados aquí. | **FIRMADA 2026-08-20** con la opción (b) de la ficha `heading`: la familia tipográfica B5 mide su superficie por `--style-*` como sistema transversal (verdad estructural — la vertebración tipográfica RECHAZA acoplar componentes a la capa semántica; acuñar `--heading-*` = 36 alias de una capa ya pública, la clase que §39 mató). No se acuñan `--{c}-*` para los primitivos tipográficos. | | **D-TH.3** | Tracks WIP (`palabras`, `words`, `chronos` y su familia de fecha/hora) | En el CENSO siempre; en R-5 `error` sólo cuando salgan de su carril (coherente con R-4.x, que los excluye). | **FIRMADA 2026-08-20** tal cual. | | **D-TH.4** | Familias y ORDEN del backfill | El de §5 F2: menús/navegación → campos → controles → superficies → texto → media/chat → tiempo (el último por ser el más grande y estar en carril propio). Dentro de cada familia, sin contrato primero, luego por knobs fuera del contrato. | **FIRMADA 2026-08-20** con enmienda: la cola REVISADA del CONTINUE (fichas con veredicto §5 verificado) tiene prioridad sobre el orden de familias — trabajo revisado no se deja enfriar. | | **D-TH.5** | El default NO cambia en este eje | Sí. Toda mejora visual que aparezca se anota como deuda y se firma aparte (como hoy con `navigation-menu`). | **FIRMADA 2026-08-20** sin matices — es lo que hace verificable el eje (diff de computed = 0 como gate). Única excepción viva: la migración hover→capa de estado, firmada aparte el mismo día (ver «Firmas del 2026-08-20» del CONTINUE), que mueve píxel con captura y diff explicado. | | **D-TH.6** | Vocabulario de slots (R-5.3): ¿se NORMALIZAN los nombres que ya se desvían (`trigger-color-*` de `tabs`, `*-bg-active`/`*-color-active` del nav de hoy, …) con un codemod value-preserving como el de px/py del 2026-07-06? | Sí, en F1, ANTES del backfill — si no, el backfill escribirá más nombres fuera de vocabulario. Inventario exacto de desviaciones sale en F0 (el censo añade `--names`). | **FIRMADA COMPLETA 2026-08-20**, en dos actos. (1) El slot de tinta es **`fg`**; `color` como slot MUERE (196 claves cola). Razón: el principio de plataforma del 2026-07-06 gobierna los ejes DIMENSIONALES, no la pareja `bg`/`fg` (si gobernara, `bg` sería `background-color`); `color` colisiona con `--color-*`, `data-color` y los 9 roles; `on-fg` compone. (2) Modificador recipe-level DELANTE — regla: **delante lo interactivo, detrás lo dimensional y contextual** (`hover-bg` pero `control-height-md`, `scrim-fg-over-dark`); rol/paleta conserva su gramática propia (detrás, `COLOR_ROLE_SLOTS` — las 47 `{rol}-{slot-de-rol}` son canónicas). Modificador válido ∈ vocabulario universal (+`current`, aria) ∪ valores `data-state`/ejes del MORFO del componente (así `partial` de rating-group y `read/failed` de chat-message validan contra el compilado, igual que la parte). `waveform.played/buffered` = pseudo-partes eidos-only (`played-fg`). Los hovers neutros se EXCLUYEN del codemod: migran a la capa de estado (firma aparte, mismo día). Detalle operativo: CONTINUE §«Mañana». | | **D-TH.7** | Cuándo gradúa R-5 a `error` | Cuando el censo global dé 100 % o excepción documentada (F3), nunca antes de terminar F2 — un `error` a medio backfill rompe el `component:audit` de todo el parque. | **FIRMADA 2026-08-20** con enmienda: **R-5.3 (gramática de nombres) gradúa a `error` DIRECTO tras el codemod** — su deuda muere en el mismo pass (precedente R-4.4, «sin allowlist»). R-5.1/R-5.2 esperan F3 como propone la fila. | | **D-TH.8** | ¿Entra la exposición en la demo de temas (`/temas`: un panel que liste los tokens públicos por componente y permita moverlos)? | Sí pero DESPUÉS (F4, opcional): es la prueba viva de A; sin el backfill no hay nada que exponer. | **FIRMADA 2026-08-20** tal cual — el instrumental existe (`listRecipes()` / `getRecipeTokens()`; la ruta `/temas` existe). | --- ## 5. Fases y gates Cada fase: **qué leer ANTES** (entero), **qué producir**, **guard**. Ninguna arranca sin la anterior en verde. Todo bloque de F2 ejecuta el protocolo de §7 **por componente**, sin atajos: es la parte del plan que existe porque «Opus falla mucho». ### F0 — Instrumento + guard en `warn` (una sesión) - Leer: theming §1bis, §3, §5, §6, §7, §12, §16 · tsc.md entero · recipe-contract §1–§5 · `scripts/component-audit.ts` (cómo se declara una regla R-x, `readmeException`, severidades) · `src/uix/eidos/recipe-css-contract.test.ts` (qué ya guarda) · `PLAN-sidebar.md` §3 y F3 · este plan entero. - Producir: (a) `scripts/theming-census.ts` endurecido: `--names` (inventario de nombres de token fuera del vocabulario §1/§6.7), detección «privado que deriva de público» (leer `--_c-x: var(--c-y)` en la misma receta y reclasificar), salida `--json` estable; (b) **R-5.1/5.2/5.3 en `component-audit`** con severidad `warn` y la válvula `R-5.x exception:`; (c) `docs/theming/reference.md` §12 (tabla de tooling) gana la fila del censo; (d) **HECHO 2026-08-22** — `src/uix/eidos/theming-reach-floor.test.ts`, que **fija el suelo**: alcance global ≥ el de hoy (33 %) y literales ≤ 614 — el número SUBE o el test falla (muta-prueba: meter un literal en una receta y ver el rojo antes de dar el test por bueno — memoria `a-guard-that-inspects-nothing-passes`. Ya pasó una vez HOY: la primera versión del censo no contaba una regla de una sola línea, y la mutación `padding-inline: 8px` no movió la cifra; se endureció el regex y la mutación pasó a 6→7→8). - Guard: `node --import tsx/esm scripts/theming-census.ts` reproduce §1.1 a la cifra · `npm run component:audit` NO añade ningún `error` (sólo `warn`) · `npx vitest run src/uix/eidos` sin nuevos rojos (el rojo conocido es `skin-media-player`, ajeno) · `npm run check` con cero errores en los ficheros tocados (§7.6). ### F1 — Doctrina + vocabulario (una sesión) - Leer: F0 + theming §6 (reglas estrictas) + el codemod del 2026-07-06 (recipe-contract §1, bloque «Normalización ejecutada») como molde. - Producir: recipe-contract §1 gana la regla R-5 (y §2 una fila «Alcance de tema»), §4 la tabla de enforcement, §5 el checklist (punto 12: «censo del componente al 100 % o excepción»); theming §6 referencia cruzada; el checklist de cierre (`docs/guides/completion-checklist.md`) y el `COMPONENT_AUDIT_GUIDE` endurecen las dimensiones 5 y 10 («usa tokens» ≠ «es temable»: la ficha exige el % del censo). **D-TH.6**: codemod value-preserving de los nombres fuera de vocabulario (lista de F0), con `generated/base.css` regenerado y diff = renombres 1:1, computed idéntico en navegador (mismo triple muro que px/py). - Guard: `docs:check` 0 · el diff de `generated/base.css` son SOLO renombres · censo idéntico antes/después del codemod (un rename no cambia el alcance) · sonda de computed en 3 componentes renombrados = idéntico. - **Repaso de lo ya ejecutado (2026-08-20)**: firmar una gramática obliga a releer lo corregido ANTES de ella. Auditados los 7 de §8 contra `fg` + familias (`scripts/__names-audit7.ts`): 6 limpios, y una sola corrección — `gradient-builder.checker-color` → `checker-fg` (slot, no sustantivo; 4 consumos en `gradient-builder.css`). Las 4 claves `stop-color-*` del mismo componente son falso amigo verificado (`stop-color` es la PARTE) y NO se tocan. La corrección entra DENTRO del codemod. Detalle y tabla: [`CONTINUE-theming.md`](./CONTINUE-theming.md) §«Primera posición». ### F2 — Backfill por familias (6–8 bloques de sesión; **un componente = un commit**) Orden propuesto (D-TH.4). Dentro de cada bloque: sin contrato primero, luego por knobs fuera del contrato. Cada componente sigue §7 entero. | Bloque | Familia | Comp. | Knobs fuera | Entran primero (sin contrato) | | ------ | ----------------------------------------------------- | ----: | ----------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | B1 | Menús · listas · navegación | 24 | 432 | `command`, `listbox`, `table`, `tree-grid`, `tree-view`, `grid-list`, `virtual-list`, `virtual-grid`, `menu-dial`, `feed` — y `navigation-menu` (44 %) hasta el 100 % | | B2 | Campos | 24 | 526 | `combobox` (1), `textarea`, `css-field`, `mask-field`, `number-field`, `field-langs`, `gradient-builder`, `gradient-picker`, `label` | | B3 | Controles · marcas | 33 | 502 | `knob`, `avatar`, `kbd`, `link`, `mark`, `highlight`, `clipboard`, `proof-of-human`, `aspect-ratio` | | B4 | Superficies · overlays · estado | 34 | 429 | `alert-dialog`, `announce`, `carousel`, `link-preview`, `skeleton`, `spinner`, `scroll-frames`, `motion`, `mockup`, `cascade`, `card-group` | | B5 | Texto · tipografía | 12 | 217 | `heading`, `text`, `s-text`, `display`, `code`, `code-block`, `text-*` (los que no tienen contrato) | | B6 | Media · chat | 9 | 104 | `skin-media-player` | | B7 | Tiempo (chronos · fecha/hora) — **si D-TH.3 lo mete** | 15 | 774 | `range-calendar`, `month-grid`, `year-grid`, `date-picker`, `date-range-picker`, `time-picker`, `time-range-picker`, `natural-time-picker`, `picker-shell` | | B8 | `palabras` — **si D-TH.3 lo mete** | 1 | 363 | — | Por componente se produce: entrada (o ampliación) en `recipes/base.ts` con los nombres de §1/§6.7 — dimensionales por talla si el componente tiene `size` (patrón `Sidebar`: `{part}-{eje}-{k}` + nombre resuelto con `declarations` por `scope: 'size:{k}'`), colores por rol/slot, acentos de estado; la receta consume SOLO nombres resueltos (nunca un primitivo crudo, nunca un literal); privados sólo si derivan de públicos; `generated/base.css` regenerado por el script (**jamás a mano**, theming §16 F); README de eidos con la tabla de tokens («Talla y tema», molde: `navigation-menu`); registro en §8. - Guard por componente: §7 entero, y el censo `--only {c}` al **100 %** (o `R-5.x exception:` con razón) — si no llega, el commit NO se hace. - Guard por bloque: **revisión adversarial** (§7.7) antes de dar el bloque por cerrado: un pase escéptico por componente que intente REFUTAR «el default es idéntico» y «cada token alcanza» con medición propia, no con la del autor del commit. ### F3 — R-5 a `error` + cierre (una sesión) - Producir: severidad `error` en R-5.1/5.2/5.3 (D-TH.7); el test del suelo pasa a exigir 100 % global (o excepciones listadas); theming §12 y recipe-contract §4 lo reflejan; `CONTINUE-theming.md` cerrado. - Guard: `component:audit` PASS en los 162 · censo 100 % · `docs:check` 0. ### F4 — Exposición en la demo de temas (opcional, D-TH.8) - Producir: en `/temas` (o en cada demo) un panel que lista los públicos del componente desde `recipes/base.ts` y los mueve en vivo — la prueba viva del eje A, y el detector de tokens que no alcanzan (mover y que nada cambie). - Guard: mover cada token del componente en la demo y ver cambiar el computed (automatizable con el centinela de §7.4). --- ## 6. Deuda transversal que este plan destapa — NO se ejecuta aquí | Id | Dónde | Qué | | ---------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **T-TH.1** | `component-audit` dims 5 y 10 | Dan ✓ a «usa tokens»; el criterio se endurece en F1. Las fichas ya escritas (`docs/audit/components/*.md`) quedan con el ✓ viejo: se corrigen al pasar cada componente por F2, no en masa. | | **T-TH.2** | Vocabulario de slots (theming §6.7) | Existe y no se guarda: `tabs` (`trigger-color-*`), `navigation-menu` (`-bg-active`, `-color-active`) y más lo incumplen. D-TH.6. | | **T-TH.3** | `menubar` | Misma clase que el nav: cromo de barra en privados («the bar itself styles via foundation privates», `recipes/base.ts`) + `commit-select` en el trigger al DESPLEGAR (otra sesión, sema). | | **T-TH.4** | `archetype: 'item'` en envoltorios | Hay que censar qué otras partes `
  • `/wrapper lo declaran (memoria `archetype-item-pulls-interactive-styling`: Timeline, Breadcrumb, NavigationMenu ya corregidos). Un guard de morfo («un `item` no envuelve a un `trigger`») es candidato. | | **T-TH.5** | `--control-height-*` en `:root` (T-6 del sidebar) | La densidad/zoom por SUBÁRBOL no re-deriva; afecta a la promesa de theming «por ámbito». Eje de theming aparte. | | **T-TH.6** | 25 knobs visuales fuera del morfo (`component-visual-attrs`, eje Affix) | Filas sin firmar `image/skeleton/qr-code/s-text`. El eje C (variantes) depende de ellas. | --- ## 7. PROTOCOLO DE VERIFICACIÓN — por componente, sin excepciones Existe porque el que ejecuta falla, y porque en un solo día (2026-08-19, `navigation-menu`) cuatro de estos pasos ausentes costaron cuatro commits de corrección: se dio por arreglado un hover leyendo el nodo equivocado; se resolvió un doble de animación en la dirección contraria a la doctrina; se inventó un hover por componente; y se declaró «idéntico» sin captura. Cada paso tiene un **artefacto** (número, captura, salida de comando) que va al mensaje de commit. Sin artefacto, el paso no se ha hecho. ### 7.1 Antes de tocar el componente (las cinco preguntas + lectura) 1. Las cinco preguntas del autor, UN componente por mensaje cuando haya decisión (nombres fuera de vocabulario, una excepción, un default que parece defecto): qué soluciona · leído todo o presupuesto · qué afecta · qué doctrina · qué modificaciones. Si no hay decisión nueva (el componente sólo necesita tokens con nombres canónicos), se ejecuta sin preguntar y se reporta. 2. Leer ENTEROS: la receta del componente, su entrada en `recipes/base.ts`, su README de eidos y de soma, su morfo (¿qué partes llevan arquetipo?), su ficha en `docs/audit/components/`. Y la doctrina de §0 (fuentes) si la sesión no la ha leído hoy. 3. `git status` + `git log -1`: ¿hay otra sesión con el árbol sucio? **Nunca `git stash`** para medir una base (le arranca el trabajo a otro). ### 7.2 Medir ANTES (el suelo del «idéntico») 4. `node --import tsx/esm scripts/theming-census.ts --only {c}` → guardar la línea. 5. Sonda Playwright **headless desde la raíz del repo** (la pantalla oculta congela rAF; `playwright` no resuelve desde el scratchpad) contra la demo del componente: para cada parte visible y cada talla, computed de los knobs (`backgroundColor`, `backgroundImage`, `color`, `borderRadius`, `padding*`, `fontSize`, `lineHeight`, `blockSize`/`inlineSize`, `boxShadow`, `borderColor`) en reposo · hover · activo/abierto · disabled · `:focus-visible`. Muestrear FUERA de callbacks de `MutationObserver` (miden el instante anterior al flush). Guardar JSON. 6. Captura 2× de la demo en reposo y en hover (sirve de «antes» para la comparación a ojo). **6.bis — CUENTA LOS NODOS ANTES DE CREER NADA.** La sonda imprime cuántos mide; el guard, cuántos tokens mueven. Un recuento de 0–2 nodos, o un 0/N del guard, NO es un resultado: es el instrumento ciego. Las siete causas medidas hasta el 2026-08-23, con el interruptor que las arregla (todos en `COMPONENT_OVERRIDES` del guard y en `EXTRA_NODES`/`DEMO_VARIANTS` de la sonda): | síntoma | causa | interruptor | | --- | --- | --- | | 0 nodos, ruta 404 | el componente **no tiene demo** (picker-shell, mockup, surface, audio-player) | `urls: ['/ruta/donde/sí/vive']` | | 0–2 nodos con la página llena | sus partes **no llevan `data-{c}-*`**: hooks de clase (onion-menu, prose) o partes de OTRO chasis (audio-player) | `extraNodes` / `EXTRA_NODES` | | la superficie no está en el DOM | abre por **hover**, no por clic (tooltip, link-preview) | `openBy: 'hover'` | | la superficie se CIERRA sola | nace abierta y el guard la pulsa (onion-menu) | `openMarker` | | media docena de tokens muertos por variante | el panel/skin sólo existe tras un **chip de la demo** | `openWith` + `DEMO_VARIANTS` | | todos los tonos muertos | la paleta resuelve por `data-color` **o `data-intent`**, y por VARIANTE | barrido de tono (automático) + `sweepAttr` | | `TimeoutError` antes de medir | la página **nunca alcanza `networkidle`** (image reintenta un `src` roto) | caída a `load` (automática) | **Y tres lecturas que mienten aunque el nodo esté bien**: una propiedad EN TRANSICIÓN devuelve el valor viejo (congela antes de leer, salvo que el token SEA la transición); un nodo ANIMADO devuelve el valor de la animación (congela también `animation`, a mano); y `::placeholder` sobre un input sin `placeholder` devuelve el estilo del ELEMENTO. ### 7.3 Editar (la forma firmada — nada más) 7. Tokens con los nombres de recipe-contract §1 / theming §6.7. Por talla si hay `size`. Acentos de estado en `background-color`. Hover/press/selected neutros = la capa del sistema (nunca `background:` a mano, nunca `background-image: none` para «limpiar», nunca una escalera de color propia). Privados sólo derivando de públicos. Literales: cero. 8. `npm run generate:eidos-css` y `git diff --stat src/uix/eidos/generated/`: el diff son SOLO los tokens del componente. Si toca otra cosa, parar. 9. Si un envoltorio lleva `archetype: 'item'`/`'option'`/`'trigger'` sin ser el interactivo, es una DECISIÓN (morfo): se presenta, no se toca de oficio. ### 7.4 Medir DESPUÉS (los tres artefactos) 10. **Idéntico**: repetir la sonda de 7.2 → diff de computed por parte/estado/talla = **vacío**. Un solo valor distinto = parar y explicar (o es defecto previo que se firma, o es error del commit). 11. **Alcanza**: la **prueba de centinela**, que desde 2026-08-21 es el guard **R-5.4** (`npm run theming:sentinel -- {c} `, doctrina en recipe-contract §4) — por cada token público, un valor imposible de confundir (`1234px`, `rgb(1, 2, 3)`, `0.123`) y algún computed tiene que seguirlo. Si un token no mueve nada, es un token que miente (fantasma, nombre equivocado, o una cascada que jamás gana) — o su silencio tiene una razón MEDIDA escrita en el ledger (`scripts/theming-sentinel-exceptions.ts`): el guard falla con un muerto sin adjudicar y avisa de excepciones STALE. 12. **Desde el píxel hacia arriba** en los estados de hover: `elementsFromPoint` en el centro del control y la pila de ancestros con `backgroundImage` / `backgroundColor` — el velo cae en el nodo CON forma y en ningún envoltorio (memoria `measure-the-node-the-user-points-at`). Si hay animación propia y un sello de sema en el mismo nodo, registro de `animationstart/end` (no `getAnimations` en un solo instante): UNA entrada por ocurrencia (memoria motion.md §"when both write `animation`"). 13. Captura 2× después, al lado de la de antes. Mirarla. **Nota 2026-08-23 (revisión `REVIEW-theming-2026-08-23.md`, D3)**: los pasos 6 y 13 —las capturas— **no se hicieron en ninguno de los 33 componentes** de los bloques del 22 y el 23. No se recuperan retroactivamente: para esos, el artefacto es el diff de computed en vacío más el centinela. **Se exigen desde el siguiente componente**, y el paso 12 (píxel hacia arriba) es OBLIGATORIO en cuanto el commit toque una regla `:hover` — que es donde el 2026-08-19 costó cuatro commits de corrección. ### 7.5 Guards mecánicos (todos, cada componente) 14. `node --import tsx/esm scripts/theming-census.ts --only {c}` → **100 %** o excepción escrita. Y el global NO baja. 15. `node --import tsx/esm scripts/component-audit.ts --only {c}` → PASS, sin R-5 nuevos (en `warn` durante F2, pero se listan y se resuelven). 16. `node --import tsx/esm scripts/eidos-lint.ts {c}` → 0 invalid. 17. `npx vitest run src/uix/eidos` → sin rojos nuevos (comparar **por fichero de test**, no por cifra: el rojo conocido es `skin-media-player`). 18. `npm run rtl:check` 0 · `npm run docs:check` 0 · `npx prettier --check` en los ficheros tocados (ojo: `autocrlf=true` hace que prettier se queje del CRLF del checkout — comprobar el contenido normalizado a LF, no el fichero; git guarda LF). 19. `npm run check`: atribuir errores **por fichero** (`grep ERROR | grep {c}`), nunca por cifra global; la rama tiene sesiones concurrentes y el total se mueve solo. Si el log no llega a `COMPLETED`, es `tmp/lexical/`: relanzar. ### 7.6 Commit (uno por componente) 20. `git add` con rutas EXPLÍCITAS (sólo lo del componente + `recipes/base.ts` - `generated/base.css` + su README + este plan §8); `git diff --cached --stat` y `git show :ruta` para verificar el ÁRBOL INDEXADO, no el de trabajo; `git status` para ver que no entra nada de otra sesión. Mensaje con `git commit -F fichero` (backticks en `-m` se ejecutan en bash), en castellano, con: qué knobs entran, la cifra del censo antes → después, el diff de computed (vacío), el centinela (n tokens probados), los guards. **Nunca `--amend`** en la rama compartida. 21. Registro en §8 de este plan (componente · commit · censo antes→después · excepciones) y, si el bloque cierra, en `CONTINUE-theming.md`. ### 7.7 Revisión adversarial por bloque 22. Al cerrar un bloque, una pasada escéptica (otra sesión o un pase separado) que por componente intente REFUTAR con medición propia: «el default es idéntico» (sonda 7.2 vs 7.4 rehecha, no leída del commit), «cada token alcanza» (centinela rehecho), «el velo cae donde debe» (píxel arriba), «no hay doble animación». Hallazgos reales se arreglan en el bloque; los refutados se anotan. El molde es la revisión del `Sidebar` (21 hallazgos · 17 refutados · 4 reales, `bae03e55c`). ### 7.8 Lo que NO se hace en este eje (y se anota como deuda si aparece) - Rediseñar un default (radio, color, altura…): deuda firmable, no cambio. - Tocar sema/morfo salvo un arquetipo en un envoltorio (decisión aparte). - Cascadear a otro componente «ya que estamos» (memoria `no-cascade-changes`). - Inventar nombres de token fuera de §1/§6.7, o un hover/estado por componente. - Editar `generated/base.css` a mano. - Dar un paso por hecho sin su artefacto. --- ## 8. Registro - 2026-08-23 — **`card` 78 % → 78 %** (`f3189a05a`), contrato 97 → **98** claves, centinela **49/98** con 49 adjudicadas. **Cerrado en su número, y ése es el resultado**: sus ocho privados son TODOS, transitivamente, el puente THM-2 — cuatro leen `--_card-palette-*` directo y cuatro leen el conmutador por VARIANTE (`--_card-bg` / `--_card-fg`), cuyas ramas son ese mismo puente. Un público encima mataría el `color=` por instancia. Lo único que faltaba era **la costura**: la rama `outline` del conmutador leía `var(--color-surface-default)` a pelo → `--card-outline-bg`. **Lo que enseñó**: (1) el trío `emerge-*` no pinta en la demo porque el montaje CEDE al sistema de motion (`:not([data-animation-style])`) y el escenario arranca con un preset — y `emerge-distance` tiene además una segunda causa, que alimenta `--motion-distance-md` y la animación acaba en `translate 0` con `fill-mode: both`, así que `transform` en reposo no puede enseñarlo nunca; (2) **una receta que COMPONE gana a la compuesta**: dentro de un `CardGroup` el eje `radius` de la card está muerto (`[data-card-group] [data-card]` es (0,2,0) contra (0,1,0)) — medido, deliberado (anidación concéntrica §30) y registrado. Diff de computed vacío (1.216 valores × 7 estados + 2.484 × 18 celdas), capturas 2× antes/después. - 2026-08-23 — **`toggle` 81 % → 100 %** (`edc019515` + `007c2af52`), contrato 119 → **126** claves, centinela **70/126** con 56 adjudicadas. Tres knobs y una firma: el **radio** (`radius` + los cinco pasos de `rounded`) leía `--radius-default` y la escala global A PELO —el agujero de `navigation-menu`, y el único de los CINCO componentes con escala `rounded` que no tenía la suya (`button`, `badge`, `image`, `color-swatch` la declaran)—; el **tinte inválido** iba a `--color-threat-element` a pelo (la costura §2-A); y los tres literales quedan FIRMADOS (identidad del `block`, técnica `sr-only`). El desacople de `size` se conserva: son pasos de `rounded`, no de talla. **Lo que enseñó**: el centinela empezó en 36/126 y llegó a 70 **sin adjudicar nada** — las claves `{variante}-*` y `{variante}-on-*` son las mismas tres variantes en dos ESTADOS, y forzar el estado con `prepareWith` cambiaba una mitad por la otra (36 → 46). El guard aprende a barrer el **producto cartesiano** de varios ejes, a QUITAR un atributo con `null` (sellar `data-invalid` toda la corrida hacía que el borde inválido ganase a los otros seis) y a aplicar el barrido también en el **pase de hover**, que no barría nada — que es literalmente la razón escrita en el ledger de `radio-group.hover-segmented-segment-fg`. Los 12 componentes con `sweepAttr` re-verificados uno a uno: **cero regresiones, cero STALE** (y tres de ellos dieron exit=1 falso corriendo doce seguidos: a solas, verdes). Diff de computed vacío (416 valores × 7 estados + 1.150 × 25 celdas de barrido dirigido), capturas 2× antes/después. **Anotado y FUERA por D-TH.5**: el `:hover` tapa el borde inválido (mismo peso, declarado antes) — mueve píxel en los cuatro controles de formulario. - 2026-08-23 — **`rating-group` 80 % → 100 %** (`f6a618328`), contrato 25 → **29** claves, centinela **23/29** con 6 adjudicadas. Tres RESUELTAS por talla (`gap`, `item-size`, `font-size`) jubilan sus tres privados y los cuatro bloques `[data-size]` que los conmutaban: eran «privado con fuentes por TALLA», o sea una ESCALA, que sube al contrato. Más `invalid-item-fg` (la costura: leía `--color-threat-element` a pelo) y los dos `100%` del glifo FIRMADOS como identidad, que salen del ratio. Diff **0** sobre 5.624 valores. Lo que enseñó: **el centinela no fotografía `transform`**, así que `item-hover-offset` —el único knob de la receta que mueve uno— leía muerto estando vivo (§13). - 2026-08-23 — **`radio-group` 77 % → 81 %, y su TECHO es ése** (`f5d252443`), contrato 61 → **69** claves, centinela **46/69** con 23 adjudicadas. Ocho claves: tres RESUELTAS por talla (`item-size`, `dot-size`, `font-size`) que retiran **diecisiete bloques `[data-size]`** y jubilan `--_radio-item-size` (privado con el nombre ABREVIADO, clase `--_mp-*`/`--gp-*`), cuatro de validación que iban a `--color-threat-*` a pelo (el trío del anillo con el vocabulario del gemelo `checkbox`) y `segmented-segment-padding-block`. Diff **0** sobre 25.263 valores en 47 estados. Los diez knobs que quedan son el puente THM-2 de paleta: **el techo no es el 100 %**. **`row-min-height-touch` se acuñó y hubo que RETIRARLO**: `--touch-target` no lo emite ningún generador (constante ergonómica escrita a mano en `archetypes.css`) y `recipe-css-contract` lo cazó como referencia FANTASMA en el acto — recipe-contract §2 ya lo lista entre los sistemas transversales que una receta consume y nunca acuña. **Y salieron TRES declaraciones que no pintan** (§13, las tres mueven píxel ⇒ firma): la escala del punto no ha pintado nunca (`` escribe `width:1em` INLINE — 13,3281px en las cinco tallas), `[data-radio-group-label][data-disabled]` no casa jamás (soma no estampa el atributo en la parte `Label`), y el fondo de un radio marcado Y deshabilitado pierde (0,3,0) contra la regla de variante (0,4,0). - 2026-08-23 — **`chat-composer` 79 % → 100 %** (`71a097ddc`), contrato 31 → **33** claves, centinela **29/33** con 4 adjudicadas. **La mitad de su contrato no la montaba la demo**: 14 muertas de 31, y once eran la barra de contexto (opt-in tras un chip) más la bandeja de adjuntos (opt-in tras un botón); con las dos encendidas por `prepareWith`, 29/33 antes de tocar el código. Lo acuñado son **DOS** claves donde el generador pedía cuatro: `context-close-size` —el chip de descarte es un control CUADRADO, una clave para los dos ejes como `send-size` en la misma receta; el ⚠ del generador fundía la caja (26 px) con su glifo (`1em`)— y `context-close-radius`. **CUATRO declaraciones retiradas por muertas**: los dos `> svg` (`1em` del cierre, `1.1em` del envío) no pueden ganar al `style="width: var(--icon-size-{k})"` que el `Icon` compuesto escribe INLINE — medido 14 px contra 13,3 y 16 px contra 14,7; retirarlas da 0 diffs. Misma lección que `chat-log` la misma tarde. **Y una colisión ENTRE RECETAS**: `--chat-composer-disabled-opacity` no alcanza porque `file-upload` barre DESCENDIENTES (`[data-file-upload] [data-disabled]`, (0,2,0), el mismo peso que `[data-chat-composer-send][data-disabled]`) y su receta se emite MÁS TARDE — y el composer vive dentro de un FileUpload en su composición DOCUMENTADA. Fuera del envoltorio alcanza (0.4 → 0.123); los dos defaults son `--opacity-disabled`, así que es invisible hasta que un tema mueva uno. No se arregla aquí (sería cascadear): adjudicada. Las otras tres son hábitos del guard (`focus-border` bajo `:focus-within`, y el par que ES la transición que congela). **El diff no salió limpio del todo y la causa está medida**: 2-4 valores de `[data-chat-composer-attach]` `blockSize` flotando 30 ↔ 36 px, que flota IGUAL corriendo dos veces el mismo código — una recarga de cada ocho, el `Button size="sm" variant="ghost"` que la demo compone ahí computa 36 px y fondo sólido primario con sus `data-size`/`data-variant` puestos: las reglas de talla y variante de `button` llegan tarde en el dev server. Los 2.204 valores restantes, idénticos. - 2026-08-24 — **`calendar` 84 % → 89 %** (`509d1144f`), contrato **74 claves sin tocar**, centinela **45/74 → 68/74** con 6 adjudicadas. **CERO claves acuñadas: el censo no tenía nada que coser** (`global` ya era 0 desde la capa `calendar-surface`), así que lo único fuera de alcance eran cuatro literales de IDENTIDAD (`max-content` ×3 + `100%`), ahora firmados con su razón. **Todo el trabajo fue MEDIR**, y enseñó la ley del bloque: **una entrada de receta de FAMILIA no se juzga en la ruta de UN componente** — 23 de sus 29 «muertos» eran el cromo que `month-grid` y `range-calendar` pintan con sus propios atributos, invisible al filtro `data-calendar`; y la propia demo pone SELECTORES de mes/año donde iría el `heading`, así que tres claves más no tenían nodo aquí. Con `urls` a las tres rutas de la familia + `extraNodes` + barrido de `variant`, sin tocar a los tres consumidores (siguen a 0 % honesto). **Hallazgo de píxel**: el tamaño de letra de los selectores de mes / año es NO DETERMINISTA — `[data-calendar-month-select][data-button]` y `[data-popover-trigger]:not([data-archetype='field-trigger'])` empatan a (0,2,0) y gana el módulo que Vite inyecte el último (cuatro cargas: 14/16/14/14 px). Lo destapó la sonda dando **24 diffs corriendo dos veces el MISMO código**; es la clase de `tree-grid` y va a §13. Sonda 22.656 valores × 7 estados: **0 diffs fuera de esos dos nodos**; capturas 2× idénticas byte a byte. Guards: censo `--only` 89 % (global 68 %, no baja) · audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0. - 2026-08-24 — **`select` 83 % → 92 %** (`cc010f79a`), contrato 101 → **103** claves, centinela **69/103** con 34 adjudicadas. Dos claves de COSTURA: **`invalid-border`** (las TRES declaraciones del estado inválido leían `--color-risk-border` a pelo — y son UNA clave, no las dos que proponía la §4: las filas se agrupan por VALOR) y **`group-gap`** (nombre contado en el catálogo: `toolbar.group-gap`). Tres literales de identidad firmados. Lo que queda son los 5 forwards de paleta THM-2: **CERRADO en 92 %**. Lo que enseñó, todo del PORTAL: (1) el guard tiene que mantener el panel ABIERTO para medir 30 tokens y **el estado abierto tapa los de reposo** —`trigger-border` y `trigger-indicator-fg` miden bien con el panel cerrado—; (2) tres knobs viven en nodos SIN `data-select-*` (la `z` en el `[data-floating-wrapper]`, el ritmo del viewport en el `ScrollArea` compuesto): con `extraNodes`, 65/101 → 69/103; (3) la cascada de paleta anula sus 24 tonos en su forma MULTI-PARTE (trigger + content, porque el panel se porta fuera); (4) **⚠ `separator-size` alcanza y aun así no pinta**: `flex-shrink` 1 en un panel que desborda aplasta su 1px a **0px** (con `flex-shrink: 0`, 1px → 9px) — defecto de píxel a §13. §12.9 respetado: nada de `font-family` / `line-height` para la superficie `overlay`. Sonda 2.176 valores × 8 estados = **0 diffs**; capturas 2× (cerrado y abierto) idénticas byte a byte. Guards: censo 92 % · audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0. - 2026-08-23 — **`chat-log` 77 % → 100 %** (`fd8f01bac`), contrato 31 → **35** claves, centinela **34/35** con 1 adjudicada. **El instrumento daba 15/31 antes de tocar el código, y las dieciséis «muertas» estaban VIVAS las dieciséis.** Tres cegueras, ninguna del componente: (1) el viewport que scrollea es del VirtualList compuesto (`data-virtual-list-viewport`, cero `data-chat-log-*`), donde el log pinta sus dos paddings — la cuarta forma del punto ciego de prose / navigation-menu / waveform; (2) la demo no montaba TRES de las seis partes (separador de día y divisor de no-leídos no existían; la fila de evento vivía en el índice 0 de una lista virtualizada anclada al final, o sea fuera de la ventana) — once claves sin nodo; (3) la pastilla del separador es un `> span` sin atributo, seis claves más. Con las tres arregladas: **31/31 antes de acuñar nada**. Lo acuñado es la COSTURA de los cuatro globales: `divider-padding-block`, `event-radius`, `pill-gap`, `to-latest-radius` — con los nombres del CATÁLOGO (`divider-*` / `pill-*`), no los del generador (`divider-unread-*` / `pill-new-*`), que nombra por el atributo de la parte. **Y una regla RETIRADA por muerta**: `[data-chat-log-to-latest] svg { block-size: 1em }` nunca ha pintado porque el `Icon` compuesto escribe `style="width: var(--icon-size-sm)"` EN LÍNEA — el glifo mide 16 px, no los 14 px de `1em`; retirarla da 0 diffs, la prueba. Es la clase `carousel.item-gap`, pero el inline lo pone un COMPONENTE COMPUESTO, no soma. Tres identidades firmadas (`block-size: 100%` ×2, `inline-size: fit-content`). Adjudicada: `event-radius`, que sólo pinta bajo `:focus-visible` (medido con teclado sobre la fila real: 0 → 4 → 1234 px). Diff de computed VACÍO (1.920 valores · 7 estados · 9 nodos). - 2026-08-23 — **`editable` 75 % → 82 %** (`2117d7422`), contrato 56 claves, centinela **42/56** con 14 adjudicadas. **Su apertura es frágil AL REVÉS**: salir del modo edición es lo que hace un `blur` —da igual lo que diga `submitMode`, esa opción sólo decide si el valor se compromete— y el guard blurea justo después de abrir, así que abrir con clic medía una superficie ya cerrada (`submit-fg` leía muerto y mueve perfectamente abierta). Se abre por FOCO, con la demo en `activationMode=focus`. Además tiene **DOS superficies excluyentes** (preview/input comparten celda; Edit contra Save+Cancel), así que todo se midió dos veces: diff 0 en modo preview (1.696 valores) y en modo edición (2.528). Entra **`disabled-fg`** —la tinta apagada de TODO el componente, una sola regla; era el único knob a primitivo crudo y traía un respaldo `, var(--color-content-muted)` MUERTO— y sale **`control-border`**, clave pública que no consumía ninguna regla (deuda inversa). Dos literales de identidad firmados. Los 7 privados son el puente de paleta THM-2: **82 % es su techo**. ⚠ **`risk-border` NO cae en el patrón de paleta**: las tres reglas de inválido la leen directamente, así que su silencio tiene otra causa y su propia adjudicación —forzado `data-invalid` alcanza en input y control—; una excepción por patrón que engulle una clave con vida propia es una adjudicación falsa. **Defecto anotado**: el tinte inválido ES la clave de tono, o sea dos knobs con una sola llave; el arreglo es `invalid-{parte}-border` (la forma que `switch` acuñó el mismo día) y es contrato. - 2026-08-23 — **`avatar` 75 % → 90 %** (`f68bac4a6`), contrato 84 → 88 claves, centinela **83/88** con 5 adjudicadas. **Su contrato existía y NINGÚN instrumento lo veía**: es la única entrada de `base.ts` construida por una IIFE (un helper local genera sus 24 ámbitos compuestos), así que su mapa vive en el `return {`, un tabulador más adentro — el censo la daba «sin entrada» y **el centinela moría** (`no recipe block for avatar`). Los dos lectores leen ya la IIFE. Lo demás: **`group-overlap-{xs..xxl}`**, porque la clave única `--avatar-group-overlap` la RE-DECLARABA la receta en seis bloques `[data-size]` —sentada en el elemento, ganaba al `:root` de un tema: medido, no movía nada desde el asiento del tema, y **el centinela lo daba por vivo porque escribe el token también sobre el nodo** (falso positivo nuevo, §13)—; **`group-max` RETIRADO** de la receta y del envoltorio (el tope lo aplica `data-has-max` + `:nth-child(n + M)`, y la variable inline no la leía nadie); el respaldo `, white` de `--_avatar-badge-fg`, muerto contra su propio token; y **seis literales de identidad firmados**. Los 3 privados que quedan son el CONMUTADOR de variante sobre el forward de paleta THM-2: 90 % es su techo. Artefactos: diff de computed 0 en tres bases (576 + 384 + 3.072 valores), capturas 2× idénticas al byte, `rtl:check` 0, `docs:check` 0, `check` sin un solo error en `avatar`. **Queda R-5.3 preexistente**: cuatro claves `*-color-custom*` donde la gramática exige `fg` — renombrarlas cambia tres escotillas públicas, así que se lista. - 2026-08-23 — **REVISIÓN DE LOS 26 COMMITS SIN REVISAR (§7.7 incluida)**: `REVIEW-theming-2026-08-23.md`. 33 componentes, los de los bloques del 22 y el 23. **5 errores reales, 5 desviaciones de protocolo, y la §7.7 con 5 hipótesis y 5 refutaciones.** Todo arreglado el mismo día: - **E5 — `float-panel` tenía DOCE rojos sin adjudicar y el guard fallaba en él** (`233ef989b`). Su commit del 22 los dejó escritos como «lo rojo que queda» y el handoff dio por cerrado el bloque viejo: la frase era FALSA. Seis son el plano `overlay` (§12.9), tres estados que la demo no monta, uno el puente de paleta y dos la ley del congelado. **Sólo se ve corriendo el guard sobre el ledger ENTERO, no sobre lo que tocas.** - **E1 — `badge` acuñó la clave con el nombre de la PROPIEDAD** (`min-block-size-{k}`) donde la ranura es `height` (`bfe7727b0`). 24 componentes dimensionan por `…-height-{k}`; era el único. **R-5.3 no guarda el vocabulario dimensional** — §13. - **E2 — una adjudicación mal clasificada**: `background.parallax-travel` no era «estructural», es que **el guard no fotografía `translate`** — §13. - **D1 — la sesión de la mañana se saltó los pasos 8 y 9 en los 22 componentes**: ni README «Talla y tema» ni pestaña `Tokens` (`e522e04e6`, tabla DERIVADA del contrato y del ledger, 21 pestañas verificadas en el navegador). - **§7.7**: base en worktree con `server.fs.allow` (sin él no hidrata y se compara contra una base muerta), **31 de 33 con 0 diffs sobre ~48.000 valores**; los dos con diffs (`image`, `audio-player`) son ruido reproducible del instrumento —dos corridas del MISMO código dan 1 y 11—; píxel-arriba verde en los cuatro que tocaron `:hover`; sin dobles animaciones (ningún commit toca `animation`); contrato sin duplicados ni huérfanos. - 2026-08-23 — **`file-upload` 76 % → 83 %** (`0c06d42aa`), 1 clave acuñada y **1 RETIRADA**. `invalid-dropzone-border` (con la PARTE en el nombre: tres partes con borde) era el único knob a un primitivo a pelo. **`button-bg` no pintaba**: la regla compartida de los tres botones la declaraba y los tres la pisan después (el trigger con el sólido de la paleta, `ItemRemove` y `ClearTrigger` con `transparent`) — medido sobre los tres nodos, cero movimiento, así que se retira la declaración Y la clave, y el diff 0 lo prueba. **Cuatro literales de identidad firmados** (el ancho del campo, los dos de la miniatura, el de la barra). **CUARENTA claves de tono** (8 × 5 ranuras) adjudicadas por patrón: la cascada de paleta, medida sobre el trigger y el dropzone en hover para los ocho tonos. Los 9 privados son 8 puentes THM-2 + **el canal de valor** (`--_file-upload-progress-value`, inline por fila), así que **su techo honesto no es el 100 %**. R-5.4 54/98, exit 0. Diff 0 sobre **11.448 valores en 17 estados** y capturas 2× idénticas byte a byte. ⚠ **Truco de medición nuevo**: el barrido de estados que la demo no arranca (variante × talla × invalid × disabled × dragging) reconstruye el «antes» **inyectando el CSS de HEAD en la página** —gana por orden a igual especificidad—, sin tocar el árbol que otras tres sesiones están usando; con su muta-prueba (19 diffs al alterar el control, 1 al alterar sólo el borde inválido). - 2026-08-23 — **`switch` 76 % → 81 %** (`bbdc77c07`), 1 clave. `invalid-track-border` era el ÚNICO knob que iba a un primitivo a pelo (`--color-threat-element`). **El nombre NO es el `invalid-border` del catálogo** (6 recetas lo usan): esta tiene DOS partes con borde y las nombra —`disabled-track-border` es su hermana de estado, `thumb-border` es la del pulgar—, así que el nombre desnudo no diría cuál tiñe; la forma modificador+parte+ranura ya existe (`invalid-input-border`, `invalid-control-border`). **Las DOCE claves de tono no pintan y aquí cae también `neutral`**: la cascada de paleta ya conocida, más que **soma estampa `data-color` SIEMPRE** (color resuelto, `neutral` por defecto), así que el bloque genérico casa en toda instancia y el fallback que salva al tono por defecto en otras recetas tampoco llega — medido en checked (`--switch-neutral-solid` no mueve nada, `--palette-solid` repinta). Adjudicadas por PATRÓN. Otras trece adjudicaciones son estado que la demo no monta (5 `disabled-*`, `invalid-track-border`) y dos huecos del instrumento (los 5 `size-*-thumb-translate` pintan sobre `transform`, que el guard no lee, y valen 0 hasta checked; el par de transición ES la transición que congela). Los 4 `private` que quedan son 2 CONMUTADORES + 2 lecturas del puente THM-2: **su techo honesto no es el 100 %**. R-5.4 27/52, exit 0. Diff 0 sobre **1.344 valores en 36 estados** (416 de la sonda estándar + 928 de una sonda de estados propia: checked / invalid / readonly / disabled / RTL / 5 tallas, cada uno con hover y foco) y capturas 2× idénticas. ⚠ **Defecto anotado, no arreglado**: el tinte inválido es el paso `element` de la escala (`oklch(0.9555 …)`, casi blanco) y el `:hover` (0,4,0) lo tapa — mismo valor en checkbox / radio-group / toggle, así que es escala del sistema y mueve píxel en cuatro sitios. - 2026-08-23 — **`search-field` 71 % → 100 %** (`f24abdb60`), contrato **17 claves sin tocar**, centinela **7/17 → 17/17** sin una sola adjudicación. **CERO claves acuñadas: su 29 % de deuda eran CUATRO literales que son IDENTIDAD**, ahora firmados con `/* literal: … */` (la válvula de recipe-contract §3): el `line-height: 1` del icono —un glifo solo no tiene interlineado, la misma anotación que su hermana `field.css`—, el `100%` doble del `::before` —el anillo ES la caja del indicador, que ya mide por `--_search-field-loading-size`— y el `border-radius: 50%`, que la §4 pedía acuñar como `loading-indicator-radius`: **sería un knob que SÓLO puede romper el componente**, porque el arco es un BORDE que gira y con cualquier otro valor la ruleta deja de leerse como una — la clase 4 de F2-B, «el 50 % de un círculo», al pie de la letra. Censo 15 knobs → 11 (público 10, literal 4 → 0, excepción firmada 0 → 4; el `--opacity-muted` del indicador bajo `prefers-reduced-motion` es capa de sistema, fuera del ratio). **Lo que enseñó**: (1) **diez de sus diecisiete tokens no se habían medido NUNCA** — la demo no monta dos estados: el indicador de carga sólo existe con `loading` y son **NUEVE** claves suyas (el commit y el comentario del guard dicen «ocho»; `base.ts` le da nueve, y 7 del icono + 9 + 1 = 17), y la X sólo se desvanece con el campo VACÍO, y el escenario arranca con valor (`clear-hidden-opacity`); con los dos interruptores en `prepareWith` **EL ORDEN IMPORTA**: el clic en la X va PRIMERO, porque la propia regla que enciende le quita después el `pointer-events` al botón que la disparó; la sonda necesita el de `loading` por lo mismo, 4 → **5 nodos**, sin perder ninguno; (2) **los dos privados NO son deuda** — `--_search-field-icon-size` y `--_search-field-loading-size` son el conmutador por talla y sus cinco fuentes son, cada una, una clave pública: **58 de las 162 recetas** (cifra del commit; el censo confirma las 162, pero ningún guard reproduce el 58) resuelven así su eje `size` —incluidas `empty-state` y `banner`, cerradas al 100 % este mes—, y pasarlas al ámbito `size:` del TSC cambiaría la especificidad de la emisión sin ganar un punto de alcance. ⚠ **Un default medido, ANOTADO, no cambiado**: las dos escalas del mismo renglón divergen en los dos últimos peldaños — medido en Chrome con transiciones y animación congeladas, forzando `data-size` sobre el nodo real, icono → indicador: xs 14→14, sm 16→16, md 18→18, **lg 20→18**, **xl 32→20**; el contrato lo declara de frente (`loading-indicator-size-lg` apunta a `--size-md-icon-size` y `-xl` a `--size-lg-icon-size`), así que en `xl` la ruleta es DOCE píxeles menor que la lupa que tiene al lado. No es deuda de alcance —las diez claves alcanzan—, es diseño que huele a copia a medias: mueve píxel, D-TH.5 lo saca de este eje (anotado en el veredicto §5 y en el README). Diff de computed vacío (1.216 valores · 8 estados · 5 nodos), capturas 2× antes/después idénticas byte a byte. Guards: audit PASS · eidos-lint 0 invalid (9 morfo-backed, 4 eidos-only) · vitest `src/uix/eidos` 35/36 (el rojo `skin-media-player`, preexistente) · rtl 0 · docs 0. README con su sección «Talla y tema» y pestaña Tokens en la demo (17 claves). - 2026-08-23 — **`image-picker` 71 % → 100 %** (`1a41312b6`), contrato 13 → **14** claves, centinela **14/14** con 0 adjudicadas — de 13/13 a 14/14 SIN una sola adjudicación. **Una sola clave: `hover-button-bg`**, el único knob que iba a un primitivo global a pelo (`--color-surface-default`), y **no es el velo del sistema, por eso se acuña**: los dos botones flotan SOBRE la foto, su relleno de reposo es translúcido (`color-mix(… 88%, transparent)`) y el hover lo vuelve OPACO — un cambio de RELLENO, no una capa encima. Medido desde el píxel hacia arriba con las transiciones congeladas: el cambio cae en el `