@ -772,6 +772,301 @@ commits de corrección.
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 `<button>` (el nodo con forma, radio 6 px, `srgb .988/.88`
→ `oklch(.9911 0 0)` ) y NINGÚN ancestro recibe `background-image` , porque el
arquetipo `action` no trae velo — `archetypes.css` sólo vela `trigger` ,
`item` y `option` ; no se bendice un duplicado, que es lo que paró el
`highlighted` de `listbox` . **El nombre lo decidió el catálogo** : la §4 lo
llamaba `hover-rotate-bg` por el PRIMER selector de la regla y miente dos
veces (pinta rotate **y** remove, y el contrato ya llama `button-*` a ese
par desde que nació); `hover-{parte}-{slot}` es la forma de las 77 claves
`hover-*` de `base.ts` (52 nombres distintos). Los **cuatro literales se
firman, no se acuñan** (válvula de recipe-contract §3): el `100%` doble del
hijo del canvas es identidad (la `<Image>` ES la caja del canvas, ya con
`inset: 0` ), el `line-height: 1` es un botón de un solo glifo y el
`Highlight` de `forced-colors` es la paleta del SO, que el UA sustituiría.
Censo 20 knobs → 16 (público 12 → 13, global 1 → 0, literal 4 → 0,
excepción firmada 0 → 4; sistema 3, fuera del ratio). **Lo que enseñó** : el
instrumento medía **UN nodo** — el picker arranca VACÍO y todo lo que la
receta pinta (preview, canvas, toolbar y los dos botones de icono) sólo
existe en el estado `ready` , al que no llega ningún trigger: hay que cargar
un fichero. Con el chip «Load sample image» de la demo la sonda pasa de 1 a
**6 nodos** (`DEMO_VARIANTS` en la sonda, `prepareWith` en el centinela), y
éste necesita además `openMarker` porque su re-apertura por token volvería
a pulsar el chip y remontaría la vista previa a mitad de corrida. **Anotado
y FUERA por D-TH.5**: ese hover neutro es una invención por componente (§38
/ R-4.3 mandan la capa `--state-*` ), pero migrarlo cambia el default y no
basta con tocar la receta — el arquetipo del par es `action` , que HOY no
recibe velo, así que o `action` gana velo en `archetypes.css` (mueve píxel
en todo el catálogo) o el par cambia de arquetipo, que es morfo. Diff de
computed vacío (1.344 valores en 7 estados × 6 nodos), capturas 2×
antes/después idénticas.
- 2026-08-23 — ** `form` 71 % → 86 %** (`9c3f17fa6`), contrato 54 → **64**
claves, centinela **22/64** con 42 adjudicadas (19 nominales + 23 por un
patrón). Diez claves que iban a los primitivos A PELO: el resumen de errores
(`error-summary-{bg,fg,border}` — es el señalizador de nivel de página, y su
superficie, su tinta y su borde SON el tono risk), el acento de formulario
inválido (`invalid-accent`, un privado con UNA sola fuente, no un
conmutador), las tres superficies del árbol de AutoFields, las dos del
widget y el subrayado a media tinta de los enlaces del resumen
(`link-underline-fg`, el par del `-offset` que ya existía); más un literal
de identidad firmado. **R-5.3 cazó el nombre en el acto** : lo acuñé
`link-underline-color` y la tinta es `fg` (theming §6.7 r7, firmado el 20),
renombrado antes de commitear — el guard de nombres funciona; es el
vocabulario DIMENSIONAL el que no cubre (ver `badge` , el mismo día). **Lo
que enseñó**: ⚠⚠ **23 claves del contrato NO PINTAN, y no es deuda de valor:
es una cascada.** `[data-form-submit]` , `[data-form-reset]` y los dos
botones del array llevan también `data-button` porque COMPONEN el `Button`
canónico, y `[data-button]` casa con la MISMA especificidad (0,1,0) pero
carga DESPUÉS: gana `button.css` . Medido propiedad a propiedad sobre el nodo
real —altura, `padding-inline` , tamaño y peso de letra, interlineado, hueco,
radio, grosor de borde, el par de transición y la opacidad deshabilitada—:
**ninguna se mueve** . Es el defecto que `gradient-picker` documentó (36 de
sus 45 claves) y que `emoji-picker` encontró hoy en su trigger; allí la
respuesta fue RETIRAR las declaraciones muertas, y aquí son 23 claves
PÚBLICAS: decisión del autor, no del que ejecuta. Quedan adjudicadas POR
PATRÓN (`^(action-|transition-|disabled-opacity$)`, una razón arquitectónica
escrita una vez) y anotadas en §13 con la pregunta de fondo, que es la de
§12.9 con el plano overlay: qué manda, el componente compuesto o la receta
que lo compone — si mandan las recetas se sube la especificidad y no se
retira nada; si mandan los compuestos, hay ~62 claves públicas que retirar
en tres componentes. ⚠ el commit, su título y la nota de §13 dicen **24** y
«las 20 + el patrón»: el guard adjudica 19 nominales + 23 por patrón = 42,
que es lo que exige el 22/64 — off-by-one del autor, a corregir en
`next-features.md` . Las demás adjudicaciones son ramas que la demo no monta:
el layout `grid` (forzado alcanza — y su `grid-template-columns` sólo CAMBIA
si el valor altera el número de columnas: con 90px pasa de una columna de
502px a cuatro de 110,5), el resumen de errores VACÍO (sin `<ul>` que
indentar ni `<a>` que subrayar) y toda la rama de ARRAY de AutoFields — el
modo `auto` de la demo monta `group` y `field` , nunca `array` , `item` ni
`widget` . Fuera por doctrina: los tres privados del panel son un CONMUTADOR
(`plain` contra `panel` ) y los cuatro `--_form-action-*` leen el forward de
paleta THM-2. Diff de computed vacío (832 valores · 7 estados), capturas 2×
antes/después. Guards: censo `--only` 86 % · audit PASS · `--names` 0
desviadas · eidos-lint 36 morfo-backed / 0 invalid / 0 class-hooks · rtl 0 ·
docs 0 · suite eidos 439/440 (el rojo conocido).
- 2026-08-23 — ** `skip-link` 71 % → 100 %** (`ebe3fe6ed`), contrato **8 claves
sin tocar**, centinela **0/8 → 8/8** con **cero adjudicaciones** . **Ninguna
clave nueva**: su 29 % de deuda eran DOS literales que son la TÉCNICA:
`inline-size: 1px` / `block-size: 1px` , la mitad del sr-only canónico —la
otra mitad es el par `clip` / `clip-path` —, ahora firmados con
`/* literal: */` , la válvula de recipe-contract §3: salen del ratio como
ausencia ESCRITA, no como deuda, y el censo baja de 8 knobs a 6. No se
acuñan porque un tema que los mueva no cambia una apariencia, ROMPE el
mecanismo — y a `0 × 0` algunos motores sacan el enlace del árbol de
accesibilidad, que es lo único que este componente no puede permitirse;
misma clase que `text-blur` y `text-scramble` . Los otros 5 knobs ya pasaban
por token público, y las dos ausencias que quedan son doctrina aplicada: la
tipografía se HEREDA (no hay `font-*` declarado; acuñarla fijaría el
default, D-TH.5) y el anillo de foco es del SISTEMA (`--focus-ring-*`,
recipe-contract §2). El `background:` en shorthand no mata ninguna capa de
estado: su parte lleva `archetype: 'provider'` , que no recibe velo. **Lo que
enseñó**: la OCTAVA clase de punto ciego del instrumento — toda su
superficie existe SÓLO bajo `:focus` (sr-only mientras no lo tiene, píldora
cuando sí), así que el guard leía 0 de 8: no lo enfocaba nunca, el `blur`
que hace tras abrir lo habría deshecho igual, y clicarlo tampoco vale porque
su handler manda el foco a la región de destino y la píldora se va por el
camino de entrada. Nace `openBy: 'focus'` , y con él `openingIsFragile` — el
conjunto de aperturas que el blur y el aparcado del puntero NO deben
deshacer, hasta hoy sólo `hover` . Es aditivo: las cuatro condiciones que
decían `openBy !== 'hover'` pasan a consultarlo y ninguna configuración
existente cambia de rama. Re-verificados tres del ledger: `dialog` 37/44 (la
cifra exacta de su commit), `tooltip` 15/23, `context-menu` 27/28 — cero
regresiones. Diff de computed vacío (192 valores en 7 estados, más los 52
del estado ENFOCADO —el único que se ve— medidos aparte con un script
propio: la sonda estándar mide reposo, tallas, abierto y hover, nunca foco),
capturas 2× antes/después idénticas byte a byte.
- 2026-08-23 — ** `dialog` 67 % → 89 %** (`bb89174a8`), contrato 37 → **44**
claves, centinela **37/44** con 7 adjudicadas. Las siete son las dos tintas
SEMÁNTICAS que le faltaban. Cuatro para los tonos EVALUATIVOS
(`risk-border`, `risk-title-fg` , `threat-border` , `threat-title-fg` ): dialog
no tiene forward de paleta THM-2 y su trato de tono es deliberadamente
estrecho —el borde y la tinta del TÍTULO, nunca la superficie del cuerpo,
para que el texto largo no compita con la señal—, pero iban a
`--color-risk-*` a pelo. Y tres para el tinte de SALIDA (`saved-border`,
`failed-border` , `dismissed-border` ): el morfo escribe `data-last-action`
antes de que `data-state` pase a `closed` , así que el borde superior dice
CÓMO se cerró mientras se va; el tercero cubre `cancelled` / `dismissed` /
`dismissed-outside` , que comparten valor — todo cierre sin resultado.
`{tono}-border` es el nombre del catálogo (48 claves) y `failed-border` el
de `proof-of-human` . **El 11 % que queda son los cuatro
`--_dialog-content-*-override` , y no es deuda**: escotillas POR INSTANCIA
que el wrapper escribe INLINE desde las props `width` / `minWidth` /
`maxWidth` / `height` / `minHeight` — un público encima lo pisaría el inline
y mentiría, la clase de `--_background-gradient-image` y del triple de
`s-text-virtual-list` . Más el `width: 100%` de la hoja inferior, firmado
como identidad. ⚠ **Nada de tipografía de superficie, por §12.9** : el
contenido lleva `data-depth='modal'` y un plano declara `font-family` /
`line-height` a la misma especificidad y más tarde en la cascada; las claves
de título y descripción que ya existían son de PARTES internas, no de la
superficie del plano. **Lo que enseñó** : ⚠⚠ ** `overlay-opacity` es un FALSO
POSITIVO del guard** — `dialog-overlay.svelte` escribe
`--dialog-overlay-opacity` en el `style` INLINE del velo y el guard escribe
su centinela en el `style` de cada nodo, así que se pisa a sí mismo y ve un
cambio. Medido como lo haría un TEMA (sólo desde `:root` , animaciones
congeladas) el fondo del velo **no se mueve**
(`color(srgb 0.1098 0.098 0.0902 / 0.2518)` antes y después): la clave está
en el contrato y ningún tema la alcanza. Cara opuesta de
`drag-drop.preview-z` , misma familia que el velo de `drawer` que no pinta; a
§13 con el arreglo del guard, y retirar o rescatar la clave es decisión
aparte, porque cambia quién controla la opacidad del velo. Y dos de las
siete adjudicaciones costaron una segunda pasada **por el VALOR del
atributo**: la rejilla de posición usa `top-left` / `top-right` (físicos),
no `top-start` , y el botón de cierre NO lleva `data-position` en esta demo
aunque todas las reglas que leen `close-inset` lo exigen. Diff de computed
vacío (480 valores · 8 estados · 8 nodos con el diálogo abierto), capturas
2× antes/después. Guards: censo `--only` 89 % · audit PASS · eidos-lint 15
morfo-backed / 0 invalid / 0 class-hooks · rtl 0 · docs 0 · suite eidos
439/440 (el rojo conocido).
- 2026-08-23 — ** `pin-input` 65 % → 100 %** (`4b9354bba`), contrato 20 → **22**
claves, centinela **16/22** con seis adjudicadas. Su 35 % de deuda eran TRES
defectos, ninguno de diseño: un prefijo ABREVIADO —los privados se llamaban
`--_pin-cell-*` dentro de un componente llamado `pin-input` , así que el
censo no los reconocía como suyos y los cuatro knobs que los leen contaban
como deuda aunque DERIVAN de públicos; cuarto caso de la clase tras
`color-picker` , `metrics` y `time-picker` —, un respaldo A SÍ MISMO
(`font-family` leía `var(--style-label-font-family,
var(--style-label-font-family))`, un no-op con forma de defecto) y la tinta
del dígito a `--color-content-primary` a pelo. Las dos claves nuevas son
esas dos costuras (§2-A): `font-family` y `cell-fg` . **Lo que enseñó** : los
**veintidós respaldos escritos a mano** duplicaban lo que el contrato ya
declara en `:root` —código muerto, y retirarlos dio 0 diffs, que es la
prueba— pero UNO MENTÍA: `disabled-opacity` respaldaba `0.55` cuando
`--opacity-disabled` vale **0.4** . Es la ley de los `, 1.4` contra un
`--font-line-height-sm` de 1.45 que este mes ya salió en dos componentes,
aquí con más deriva: **un respaldo no es una red de seguridad, es una
segunda fuente que nadie actualiza**. Y las seis adjudicaciones enseñaron
DÓNDE vive cada estado: `filled` y `active` son atributos de la CELDA, pero
`invalid` y `readonly` se estampan en el CONTENEDOR y cascadean (soma los
OR-fusiona con el Field que las envuelve), así que forzarlos en la celda no
hace nada. Diff de computed vacío (1.856 valores · 8 estados · 8 nodos),
capturas 2× antes/después.
- 2026-08-23 — ** `spin-field` ADJUDICADO: 67 % es su techo** (`2ebeb9b16`),
64 % → 67 % por UN literal firmado, contrato 20 → **20** claves, centinela
**13/20** con siete adjudicadas. **Cero claves nuevas: el techo estaba
escrito en la cabecera de su propio CSS.** Ocho de sus knobs leen los
privados RESUELTOS del Field (`--_field-control-height`,
`--_field-control-padding-inline` , `--_field-control-gap` ) porque los
steppers y el scrubber IGUALAN la fila del input a cualquier talla —«never a
parallel `--spin-field-*` chrome that would alias the Field base», dice la
cabecera—, así que acuñar ahí sería el antipatrón que el fichero prohíbe por
su nombre ([S6] no-token-aliases). Préstamo con dueño: se tema en la ficha
de `field` , y el censo lo puntúa como deuda porque no sabe distinguirlo — el
mismo artefacto que `listbox` (68 %) y `radio-cards` (61 %) ya registraron.
Los dos factores del apilado (`* 0.62` el ancho del botón, `* 0.34` el
glifo) viven dentro de un `calc()` sobre ese mismo privado de Field, así que
un token propio no cambiaría quién manda; y el `line-height: 1` de la celda
de glifo es identidad, FIRMADO — lo único que mueve la cifra (16/25 → 16/24
knobs en ratio). **Lo que enseñó** : (1) los tres `active-control-*` no
responden a `:active` NI forzando el pseudo por CDP — su estado es el
`[data-scrubbing]` que soma pone en el SCRUBBER mientras se arrastra, y
forzado el atributo sobre el nodo real los tres alcanzan; (2) es una CAPA,
no un componente: sin wrapper ni ruta propia, el guard lo mide dentro de
`number-field` , declarado ahora en `urls` para que nadie tenga que saberlo;
(3) una corrida de sonda dio 5 «nodos ausentes» que no eran nada —una
recarga que no montó la talla `xl` —: repetida, 0 diffs y 0 ausentes contra
el mismo ANTES, y un «missing node» tampoco se cree a la primera. Las otras
cuatro adjudicadas: `focus-control-bg` (pseudo forzado por CDP),
`control-disabled-opacity` (0.48 → 0.137) y el par de transición medido sin
congelar (0,12s → 11,5s). Diff de computed vacío (2.400 valores · 8 estados
· 5 nodos), `component:audit` sin hallazgos, `docs:check` 0, el ledger en
sus 63 hunks previos, captura del anfitrión `number-field` .
- 2026-08-23 — ** `emoji-picker` 61 % → 100 %** (`863c6993c`), contrato 17 →
**21** claves, centinela **17/21** con 5 adjudicadas. Siete acuñadas, **tres
retiradas por muertas** y tres literales de identidad firmados. Los nombres
los dio el catálogo: `open-trigger-fg` (`accordion` lo nombra igual),
`grid-padding-inline` + `grid-padding-block-end` (el eje lógico completo,
como `card-group` / `combobox` / `command` ) y las tres del marcador de tono,
la familia `timeline.marker-{size-*,radius,bg}` — se llama `tone-marker-*` y
no `marker-*` porque el pie lleva además un selector de TONO, y `marker` a
secas sería ambiguo dentro del propio componente. Las cinco adjudicaciones,
todas forzadas sobre nodos reales: las tres del marcador (la demo monta CERO
celdas `data-tone-capable` ; con el atributo puesto el `::after` alcanza —
4px → 37px, 9999px → 37px, y el fondo), `preview-name-fg` (los hijos de la
vista previa son `<span>` desnudos, sin atributo del componente, fuera del
conjunto medido) y `open-trigger-fg` . **Lo que enseñó** : (1) **el cromo de
reposo del trigger NO es suyo** — el nodo lleva también
`data-popover-trigger` , y
`[data-popover-trigger]:not([data-archetype='field-trigger'])` es
** (0,2,0)** contra el (0,1,0) de esta receta: el radio, la tinta y el tamaño
de letra los pinta `popover.css` . Retiradas las tres declaraciones, **0
diffs**, la prueba de que estaban muertas (`trigger-fg` no llegó a existir:
se acuñó y se midió muerta en el mismo pase). Es el defecto que
`gradient-picker` documentó (36 de sus 45 tokens) y la misma respuesta;
sobreviven dos, `trigger-size` porque popover dimensiona con `height` y no
con el par lógico, y `open-trigger-fg` porque su selector añade
`[data-state='open']` y gana por orden. (2) ⚠ ** `open-trigger-fg` OSCILA
entre corridas** — 17/21, 16/21, 16/21 con el mismo código, medido vivo
CUATRO veces (la última replicando paso a paso el `staticPass` del guard):
el panel se cierra en algún punto de la pasada y, durante la animación de
SALIDA, el content sigue en el DOM mientras el trigger ya perdió
`data-state='open'` , que es lo que la regla selecciona, así que `reopen()`
mira el CONTENT, cree que está abierto y no reabre. Un `openMarker` sobre
el trigger NO basta (el clic de reapertura cae en esa misma ventana) y se
retiró; el arreglo real es que `reopen()` espere a la CONDICIÓN en vez de
dormir un número fijo — §13. Su entrada de ledger saldrá **STALE** en las
corridas donde alcanza, y está escrito en la razón para que nadie la borre
por eso. (3) **el instrumento de captura estaba roto desde siempre** :
`__shot.ts` tenía el puerto 5173 CABLEADO, así que cualquier sesión en otro
puerto moría con `ERR_EMPTY_RESPONSE` — probablemente por lo que los pasos
6 y 13 del protocolo se saltaron en 33 componentes sin que nadie lo notara.
Ahora toma la URL como argumento, con un `--open` para los componentes cuya
superficie sólo existe abierta. Diff de computed vacío (6.080 valores · 8
estados · 176 nodos con el panel abierto), capturas 2× antes/después,
estrenando el instrumento arreglado.
- 2026-08-23 — ** `progress` 69 % → 87 %** (`2e708adae`), 2 claves. `row-gap` +
`column-gap` : el `gap` en atajo tenía DOS valores distintos, así que es un knob
por eje (molde `chart.legend-row-gap` ). **Seis literales de identidad