feat(theming)!: el sobre de trigger de popover es un SUELO - tercera ley 55

El "baseline button envelope" que popover.css pinta a sus triggers vivia a
(0,2,0) con hover a (0,5,0): le ganaba a las recetas de sus HUESPEDES (el
chip de anadir reaccion de chat-message perdia 9/9 propiedades disputadas y
salia cuadrado gris entre pildoras - SU receta lo pintaba en el mismo
selector-lista y PERDIA), mataba la capa de estado del sistema en hover
(chronos plain = caja gris) y dejaba el trigger de calendar en manos del
orden de carga (14/16/14/14px medidos - la moneda al aire).

El sobre baja a :where() - base y hover, con el :not(field-trigger) DENTRO
(where anula especificidad, no matching). El suelo pinta lo que el huesped
CALLA; la receta gana donde habla; la capa de estado compone otra vez.
Tercera aplicacion de la ley firmada (12.9 planos - B'/53 paleta - y
archetypes.css:9-21 ya lo decia para SU capa). Cambio de codigo: 2 lineas.

Medido (gate de dos niveles, 76 instancias, 16 rutas, 4 estados):
- No afectados: 3 field-trigger diff VACIO; los 66 desnudos ganan EXACTAMENTE
  los dos valores del SISTEMA que el sobre tapaba (velo de hover de
  archetypes + su transition). 167 nodos de referencia: 0.
- Los 7 movers, valor a valor por CDP hacia lo que SU receta declara: el chip
  vuelve pildora (36->26px, r9999) - chronos recupera su plain y su hover -
  calendar mes/anio pasa a su ghost compuesto Y SE VUELVE DETERMINISTA (8
  cargas en dos ordenes, mismo pixel; antes 2 de 6 caian del otro lado) -
  emoji/palabras/ntp a sus recetas (ntp revive 4 claves: 44->48/62).
- Capturas antes/despues x8 revisadas por el supervisor (chip, chronos,
  calendar, emoji, gradient, popover generico, palabras, ntp).
- Guarda 55 en active-eidos-config.test.ts validada por SEIS mutaciones (la
  sexta se anadio porque la primera pasada dejo pasar un verde falso; y el
  arnes mintio con -t sobre 0 tests - detector de cero-tests anadido).
- vitest 35/36 (rojo ajeno skin-media-player) - check 0 atribuibles -
  docs-check 0/0 - eidos-lint popover/chat-message 0 invalid.

Registrado en 13 y fichas: los parches-pin de chronos/calendar pasan a
REVISABLES (ahora redundantes o semi) - el suelo comparte peldano (0,0,0) con
archetypes.css (medido 16 cargas: archetypes cae despues y el resultado es
identico, pero es un empate real sin verificar en produccion) - la
dependencia sin registrar de palabras queda anotada junto a su regla (no
tiene README de eidos). changelog 55 escrito (la guarda citaba una seccion
que debia existir).

CON ESTA, LA LISTA "Lo que espera TU FIRMA" DEL CONTINUE QUEDA VACIA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 1f8ce293bb
commit 466b24f34f

@ -177,6 +177,15 @@ de 45) / `emoji-picker` (3). Los `reaction-*` NO mienten —alcanzan sobre los
chips normales—, así que no hay nada que retirar; lo que hay es una decisión de
diseño pendiente. Registrado, no tocado (D-TH.5).
**D-TH.5 — CERRADO 2026-08-25 por la firma «el sobre de trigger de popover es el
SUELO»** (changelog §55). Se resolvió donde estaba el defecto, no aquí: el sobre
de `popover.css` bajó a `:where(...)` (0,0,0) —tercera aplicación de la ley de
§12.9 y §53— y el bloque de esta receta gana ya sus siete propiedades. El chip
mide **34×26, radio 9999px, fondo `oklch(0.9911 0 0)`, 12px de letra, 8px de
padding**: la misma píldora que sus hermanas. Cero claves retiradas y cero
añadidas — el número del componente no se mueve, el centinela sigue en 77/81 con
las mismas cuatro adjudicadas.
**Lo que queda fuera y por qué**: cuatro identidades firmadas (§1.4) y siete
knobs de sistema transversal (§2, anillo de foco × 4, velo de estado × 2,
`--opacity-muted` del `sending`). No hay deuda: el componente está CERRADO en su

@ -116,6 +116,19 @@ no distingue lo que debería — se marca `⚠`.
**EJECUTADO 2026-08-22 — 46 % → 67 %**, 11 claves de costura para el
DISPARADOR + una retirada. Lo que queda fuera es UNA cosa, y no es suya.
**NOTA 2026-08-25 — el sobre de disparador pasa a SUELO** (firma, changelog
§55). Las doce claves de disparador + estado descritas abajo no cambian de
nombre ni de número: cambia su ESPECIFICIDAD. Las dos reglas que las consumen
—el reposo y su `:hover`— se emiten desde `:where(...)`, (0,0,0), porque el
propio fichero llama a esta regla _«a baseline button envelope»_ y un baseline a
(0,2,0) le gana a toda receta de huésped a (0,1,0). Medido: **69 de 76**
instancias del catálogo no mueven un valor de receta —el disparador desnudo, que
es para quien se escribió, sigue vestido entero— y las **7** que mueven lo hacen
hacia lo que su propia receta declara. Tercera aplicación de la ley de §12.9
(plano de profundidad) y §53 (escalera de paleta). `[data-state='open']` y
`:focus-visible` se quedan donde estaban: fuera de la firma, en
`next-features.md` §13.
**El disparador genérico es cromo propio**: el que no lleva el arquetipo
`field-trigger` (que tiene su tratamiento aparte) montaba su alto, su
padding, su borde, su radio, su superficie y su tinta sobre primitivos crudos.

@ -531,6 +531,52 @@ instrumental para medirla ya existe (`__statelayer-analysis.ts` +
**Origen**: la misma sesión. Cosas que hicieron que una medición mintiera o no
existiera. Se anotan porque el eje entero se apoya en esas mediciones.
- **Los PARCHES-PIN que la firma §55 vuelve redundantes o semi-redundantes**
(2026-08-25, NO tocados — cada uno es una decisión aparte). Cuando el sobre de
trigger de `popover.css` estaba a (0,2,0), tres componentes se pincharon un
selector propio ENCIMA para recuperar su cromo. Con el sobre en el suelo, esos
pines ya no defienden de nada — pero siguen pesando, y ahora ganan a reglas que
quizá deberían ganarles:
- `chronos.css:417` — `[data-popover-trigger][data-chronos-more-link]:not(…)`
a **(0,3,0)**, con su comentario diciendo por qué existe. Medido hoy: su
`background: var(--_button-bg)` (shorthand) le gana al velo de hover de
`archetypes.css` (0,0,0), así que el `+N more` NO recibe la capa de estado
del sistema; su afordancia de hover es el `text-decoration: underline` de su
propia receta, que es correcta para un enlace pero NO es lo que el pin
pretendía. **Retirarlo es un cambio de píxel: firma aparte.**
- `calendar` — sus selectores de mes/año NO llevan pin propio; el que empataba
era `[data-calendar-month-select][data-button]` (0,2,0), que hoy gana solo.
Nada que revisar.
- `chat-message.css:462` — `[data-chat-message-actions] [data-chat-message-reaction-add]`
(0,2,0) **era una trampa armada** (empataba con el sobre y ninguna superficie
del repo monta ese anidamiento). Hoy gana de forma determinista: medido
montando el nodo, con el sobre viejo reinyectado PIERDE (borde 1px, fondo
`0.9821`, 36px) y con el suelo GANA (borde 0, fondo transparente, píldora de
26px). **Ya no es deuda**; se anota porque documenta que la clase se cierra
entera con una sola decisión.
- **Las DOS reglas de estado de `popover.css` que la firma §55 dejó ARRIBA**
(2026-08-25): `[data-popover-trigger]:not(…)[data-state='open']` y
`…:focus-visible`, las dos a **(0,3,0)**. No estaban en la firma, así que no se
tocaron — pero el coste de dejarlas está medido: bajarlas también sube el
centinela de `natural-time-picker` de **48/62 a 50/62** (reviven `trigger-bg`
y `open-trigger-border`, que hoy pierden contra el repintado de popover cuando
el panel está abierto). El paralelo de §12.9 dice que sí deberían bajar —
ninguna de las dos es una petición explícita por elemento como el `frost`, y
`archetypes.css:19-21` ya declara que el anillo de foco «must lose to any
component rule». **Decisión pendiente, con su número.**
- **El suelo de `popover.css` COMPARTE peldaño con `archetypes.css`**
(2026-08-25, medido, sin defecto observado). Al bajar a (0,0,0), las dos reglas
del sobre empatan con `:where([data-archetype='trigger'])` y su gemela de
hover, que declaran `transition` y el velo de estado. El orden lo decide el
documento. **Medido 16 cargas limpias en 4 rutas: archetypes SIEMPRE cae
después** (los índices de hoja varían de `sheet0` a `sheet19` y el resultado no
cambia), así que hoy el sistema gana su `transition` y su velo — que es lo que
la firma quería. Pero es un empate de verdad, y **no se ha verificado en
producción** (bundle de Vite en vez de dev), donde el orden de emisión es otro.
Si alguna vez se invierte, un trigger desnudo perdería el velo de hover.
El arreglo limpio sería que `popover.css` dejara de declarar hover propio y se
apoyara en la capa del sistema — otra firma.
- ~~**El centinela da falsos negativos sobre una propiedad transicionada.**~~
**CAUSA ENCONTRADA Y ARREGLADA 2026-08-21** (revisión adversarial): no era la
transición. El paso «abrir lo que se pueda abrir» hacía CLIC en
@ -1523,7 +1569,9 @@ Lo que `chart` destapó (2026-08-22):
marcador tras el clic) en vez de dormir un número fijo. Mientras tanto, su
entrada de ledger sale STALE en las corridas donde alcanza: está escrito en la
propia razón para que nadie la borre por eso.
- **⚠⚠ UN COMPONENTE COMPUESTO GANA A LA RECETA QUE LO COMPONE, y ya van TRES**
- **✅ ~~⚠⚠ UN COMPONENTE COMPUESTO GANA A LA RECETA QUE LO COMPONE, y ya van
TRES~~ — RESPONDIDA 2026-08-25 para la mitad `data-popover-trigger`, por la
firma «el sobre de trigger de popover es el SUELO»** (changelog §55).
(medido 2026-08-23). Cuando una receta estiliza un nodo que también lleva
`data-button` / `data-popover-trigger`, los dos selectores casan a la MISMA
especificidad (0,1,0) y decide el ORDEN DE CARGA — que gana el componente
@ -1534,6 +1582,17 @@ Lo que `chart` destapó (2026-08-22):
o la receta que lo compone**. Si la respuesta es «la receta», el arreglo es
subir la especificidad de esas reglas y no retirar nada; si es «el compuesto»,
hay ~62 claves públicas que retirar en tres componentes.
**La respuesta firmada es «LA RECETA»**, y no se ejecutó subiendo la
especificidad de cada víctima sino BAJANDO la del baseline a `:where()`, que es
la misma herramienta de §12.9 y §53. Nótese que el enunciado de arriba se
quedaba corto en un punto medido: el sobre de `popover.css` no estaba a
(0,1,0) sino a **(0,2,0)** —y su `:hover` a (0,5,0)—, así que no era un empate
por orden de carga, era una derrota limpia; el empate por orden sí existía
entre el sobre y `[data-calendar-*-select][data-button]`, los dos a (0,2,0)
(reproducido: 2 de 6 cargas limpias en la cara equivocada). **Lo que queda
abierto** es la otra mitad, `data-button` contra la receta que lo compone
(`form` 23 claves adjudicadas): esa firma no se ha hecho, y `button.css` NO se
tocó aquí.
- **R-5.3 no guarda el vocabulario DIMENSIONAL** (medido 2026-08-23 en `badge`).
Cubre la ranura de tinta (`fg` vs `color`) y la posición del modificador, pero
no que una clave POR TALLA se llame `…-height-{k}` y no

@ -180,8 +180,33 @@ guarda `active-eidos-config.test.ts` · ledger `scripts/theming-sentinel-excepti
§13): la tinta de tono de la etiqueta activa, enmascarada por
`[data-archetype='item'][data-highlighted]` (0,3,0) — **no** por
`aria-selected`, atribución que esta firma corrige midiendo.
6. **El chip de añadir reacción de `chat-message`** lo pinta `popover.css`:
los chips de reacción son píldoras redondas y el de añadir un cuadrado gris.
6. ~~**El chip de añadir reacción de `chat-message`** lo pinta `popover.css`:
los chips de reacción son píldoras redondas y el de añadir un cuadrado
gris.~~ — **✅ FIRMADA Y EJECUTADA 2026-08-25** (opción b, «la ley del
suelo»): el arreglo NO fue de `chat-message` sino del sobre de
`popover.css`, que se autodescribe como _«a baseline button envelope»_ y
estaba a **(0,2,0)** con su `:hover` a **(0,5,0)**. Baja a
`:where(...)` — (0,0,0) — reposo y hover. **Tercera aplicación de la ley**
de §12.9 y de B′/§53, y la primera que cae en una receta escrita a mano.
**76 instancias en 16 rutas, cuatro estados**: 69 no mueven un valor de
receta (los 64 botones desnudos del sitio de docs, `gradient-picker`, la
demo `data-perm-step` y los tres field-trigger excluidos) y **7 mueven, las
7 hacia lo que su propia receta declara** — el chip vuelve a ser **píldora**
(34×26, radio 9999, 12px), `chronos` recupera su `plain`, `calendar` pasa a
ghost y **deja de ser no determinista** (reproducido antes: 2 de 6 cargas en
14px en vez de 16px; después: 8 cargas en dos órdenes, el mismo píxel),
`emoji-picker`, `palabras` y `natural-time-picker` recuperan lo suyo. **0
movimientos** en 167 nodos de referencia. Centinela: `natural-time-picker`
**44/62 → 48/62**; ninguna entrada del ledger salió STALE (ninguna estaba
adjudicada por este sobre). Cierra de paso el empate latente de
`chat-message.css:462`. Guarda por mutación en
`active-eidos-config.test.ts` (§55, 6 mutaciones, 6 mordidas). Doctrina en
`theming/changelog.md` §55; lo que queda abierto —los dos estados a (0,3,0)
y los pines redundantes— en `next-features.md` §13.
**CON ESTO LA LISTA QUEDA VACÍA: no espera NINGUNA firma tuya en este eje.**
Los seis puntos están ejecutados. Lo que siga tiene que abrirse como entrada
nueva, no heredarse de aquí.
### Lo último que se hizo (2026-08-24 noche): §12.9 FIRMADA

@ -521,6 +521,82 @@ excepción abre la mano para MEDIR, no para contabilizar.
## 8. Registro
- 2026-08-25 — **FIRMA: el sobre de trigger de `popover` es el SUELO, no el
techo** (opción b del expediente V4′; doctrina en
[`theming/changelog.md`](../theming/changelog.md) §55). **Tercera aplicación de
la misma ley** —§12.9 el plano de profundidad, §53 la escalera de paleta— y la
primera que cae en una receta ESCRITA A MANO en vez del CSS generado.
**Qué cambia**: `popover.css` se autodescribe como _«a baseline button
envelope»_ y emitía ese sobre a **(0,2,0)**, con el `:hover` a **(0,5,0)**. Un
baseline a (0,2,0) le gana a toda receta de huésped a (0,1,0) — eso no es un
baseline, es un techo. Las dos reglas pasan a
`:where([data-popover-trigger]:not([data-archetype='field-trigger']))` y su
gemela de `:hover`. El `:not()` se queda **DENTRO** del `:where()`: anula la
contribución de especificidad, nunca el emparejamiento, así que los cinco
triggers de picker siguen excluidos igual, ahora a (0,0,0); sacarlo fuera
dejaría la regla a (0,1,0), un empate nuevo con toda receta de huésped.
**Sonda §7.2/§7.4** (76 instancias de trigger en 16 rutas × reposo · hover ·
`:focus-visible` · abierto, transiciones y animaciones congeladas, más 167
nodos de referencia medidos en paralelo):
**69 instancias no mueven un solo valor de RECETA** — los 64 botones desnudos
del sitio de docs, `gradient-picker`, el `data-perm-step` de la demo y los tres
field-trigger excluidos (estos tres, con diff literalmente vacío). De esas 69,
**66 recuperan dos valores del SISTEMA** que el (0,2,0)/(0,5,0) les tapaba: el
velo de hover de `archetypes.css` y su `transition`. Es la firma trabajando
—«la capa de estado del sistema vuelve a componer en hover»—, no daño
colateral, y **la sonda de V4′ no podía verlo** porque no medía hover ni
`background-image`.
**7 identidades mueven, y las 7 hacia lo que su propia receta declara**,
verificado además por atribución CDP (`getMatchedStylesForNode`) propiedad a
propiedad: `chat-message` chip → **píldora** (36→26px, radio 6→9999px, fondo
`0.9821`→`0.9911`, padding 12→8, letra 14→12, ancho 42→34), `chronos` `+N more`
→ tinta primaria y **sin caja gris al hover** (fondo `0.931`→transparente,
borde `0.7921`→transparente), `calendar` mes y año → **ghost** (fondo y borde
transparentes, tinta primaria, padding 12→16), `emoji-picker` → su icon-button
(fondo→transparente, borde 1→0, padding 12→0), `palabras` → radio 6→4, tinta
`content-subtle`, padding 12→8, letra 14→12, `natural-time-picker` → fondo
`0.9821`→`0.9911` y letra 14→16.
**`calendar` deja de ser una moneda al aire.** El empate era
`[data-calendar-month-select][data-button]` (0,2,0) contra el sobre (0,2,0),
las dos declarando `font-size`; **reproducido antes por CDP: 2 de 6 cargas
limpias cayeron en 14px y 4 en 16px**. Después: **8 cargas en dos órdenes de
carga distintos** (directa y navegando desde `/popover`) → el mismo píxel
siempre. Es la deuda que `next-features.md:1847` declaraba envenenando el
instrumento de medida de todo este eje.
**Centinela**: `natural-time-picker` **44/62 → 48/62** — reviven
`trigger-padding-inline`, `trigger-fg`, `trigger-radius` y `trigger-font-size`,
que su README ya registraba como inertes. `chat-message` 77/81, `calendar`
68/74, `emoji-picker` 17/21, `gradient-picker` 9/25, `popover` 35/46 y
`chronos` 6/9 **sin cambio** (comparados antes/después reconstruyendo la
cascada vieja en el fichero, nunca por `git stash`). **Ninguna entrada del
ledger salió STALE, y ninguna estaba adjudicada por este sobre**: los
componentes que V4′/V1 citaban habían RETIRADO sus claves en vez de
adjudicarlas. Los 3 muertos sin adjudicar de `chronos` (`num-row`,
`timegrid-block-size`, `disabled-opacity`) son **previos y ajenos** —medidos
idénticos con la cascada vieja—, deuda de otro eje.
**Cierra la trampa latente de `chat-message.css:462`**: el bloque «los chips
dentro de la píldora sueltan su cromo» está a (0,2,0) y **empataba** con el
sobre. Medido montando el nodo que ninguna superficie del repo monta: con el
sobre viejo reinyectado a (0,2,0) el bloque PIERDE (borde 1px, fondo `0.9821`,
36px); con el suelo GANA (borde 0, fondo transparente, la píldora de 26px). La
misma decisión resuelve las dos, sin tocar `chat-message.css`.
**Guarda por MUTACIÓN** en `active-eidos-config.test.ts` (§55): **6 mutaciones,
6 mordidas** — reposo desenvuelto · hover desenvuelto · `:not()` izado fuera
del `:where()` · suelo vaciado (anti-vacío) · una QUINTA regla de trigger fuera
del suelo (cierre estructural) · y las dos reglas de estado bajadas en silencio
(la sexta se añadió PORQUE la primera pasada la dejó pasar verde). Censo de la
propia guarda: exactamente **4 reglas** de trigger en la hoja.
**Lo que NO se tocó, y por qué**: `[data-state='open']` (0,3,0) y
`:focus-visible` (0,3,0) no estaban en la firma — coste de dejarlos, medido:
el centinela de `natural-time-picker` se quedaría en 50/62 si bajaran. El pin
(0,3,0) de `chronos.css:417` sigue en pie (ahora semi-redundante: le gana al
velo del sistema). Los dos, en `next-features.md` §13.
**Registrado además**: `palabras` dependía del sobre **sin figurar en ningún
sitio** —y no puede figurar en el censo, porque **no tiene bloque de receta**
(`theming:sentinel -- palabras` muere con «no recipe block»)—, así que la
dependencia queda escrita como comentario en `palabras-chrome.css`, junto a la
regla, con los tres ejes que sigue heredando (`height`, `border`,
`background`).
- 2026-08-25 — **FIRMA: las cuatro claves de `avatar` fuera de gramática dicen
ya la verdad** (opción A-c). Las cuatro eran las **únicas 4 desviaciones de
4.558 claves públicas** del catálogo, y la medida demostró que **no son la

@ -2235,6 +2235,80 @@ no es un contrato.
---
**Última revisión**: 2026-08-24 (§54 una palabra, un significado). Si algo
en este doc no coincide con el código, el código gana — pero abre un issue para
que actualicemos el doc.
## 55. El sobre de trigger de `popover` es el SUELO, no el techo (2026-08-25)
**FIRMA — TERCERA aplicación de la misma ley.** §12.9 la firmó para el plano de
profundidad, §53 (B′) para la cascada de paleta, y ésta es la primera que cae en
una receta ESCRITA A MANO en vez del CSS generado. `popover.css` se
autodescribe como _«a baseline button envelope»_ y lo emitía a **(0,2,0)**, con
el `:hover` a **(0,5,0)**. Un baseline a (0,2,0) le gana a TODA receta de
huésped a (0,1,0): eso no es un baseline, es un techo.
**Lo que pintaba de más, medido**: el chip de añadir reacción de `chat-message`
salía **cuadrado gris al lado de sus propias píldoras** (las siete propiedades
del sobre declaradas por su receta y las siete perdidas); los selectores de mes
y año de `calendar` **empataban a (0,2,0)** con su propia regla
(`[data-calendar-month-select][data-button]`) y leían **14 px o 16 px según qué
chunk cargara el último** — reproducido: **2 de 6 cargas limpias** cayeron en la
cara equivocada; y el `:hover` a (0,5,0) **mataba la capa de estado del sistema**
en todo huésped que pinta su propia superficie.
**La regla baja a `:where(...)`** — reposo y hover. `archetypes.css:9-21` ya
había escrito esta ley para la capa transversal (_«These are DEFAULTS — a
component recipe must ALWAYS be able to override them»*) y nombró sus **dos
excepciones deliberadas**; este sobre hacía trabajo de default y no estaba en
ninguna de las dos.
**El `:not([data-archetype='field-trigger'])` se queda DENTRO del `:where()`.**
`:where()` anula la CONTRIBUCIÓN de especificidad, nunca el emparejamiento: los
cinco triggers de picker siguen excluidos exactamente igual, ahora a (0,0,0).
Sacarlo fuera dejaría la regla a (0,1,0) — un empate nuevo con toda receta de
huésped, que es la moneda al aire que §12.9 y §53 se firmaron para prohibir.
**Los números** (76 instancias de trigger en 16 rutas, reposo · hover · foco ·
abierto, transiciones congeladas):
- **69 no mueven un solo valor de receta** — los 64 botones desnudos del sitio de
docs, `gradient-picker`, el `data-perm-step` de la demo y los tres
field-triggers excluidos. El baseline sigue haciendo su trabajo justo donde se
escribió para hacerlo. De esas 69, **66 sí recuperan dos valores del SISTEMA**:
el velo de hover de `archetypes.css` y su `transition`, que el (0,5,0)/(0,2,0)
les tapaba. Es la firma trabajando, no un daño colateral.
- **7 identidades mueven, y las 7 hacia lo que su propia receta declara**: el
chip de `chat-message` vuelve a ser **píldora** (26 px, radio 9999, fondo de la
píldora, 12 px de letra — idéntico a sus hermanas), `chronos` recupera su
`plain` (tinta primaria, sin caja gris al hover), `calendar` pasa a **ghost** y
**deja de ser no determinista** (8 cargas en dos órdenes → el mismo píxel),
`emoji-picker` recupera su icon-button transparente, `palabras` su radio y su
tinta, `natural-time-picker` su fondo y su cuerpo de letra.
- **0 movimientos** en 167 nodos de referencia medidos en paralelo (`[data-button]`
que no son trigger, píldoras de reacción, celdas de calendario y de chronos).
- **Centinela**: `natural-time-picker` **44/62 → 48/62** — reviven
`trigger-padding-inline`, `trigger-fg`, `trigger-radius` y `trigger-font-size`,
que su README ya registraba como inertes. Ninguna entrada del ledger salió
STALE: **ninguna estaba adjudicada por este sobre** (`gradient-picker` y
`emoji-picker` habían RETIRADO sus claves en vez de adjudicarlas).
**Cierra de paso la trampa latente de `chat-message.css:462`** — el bloque
«los chips dentro de la píldora sueltan su cromo» está a (0,2,0) y **empataba**
con el sobre; hoy no muerde porque ninguna superficie monta un `ReactionAdd`
dentro de `Actions`, pero estaba armada. Medido montando el nodo: con el sobre
viejo reinyectado a (0,2,0) el bloque PIERDE (borde 1 px, fondo `0.9821`,
36 px); con el suelo GANA (borde 0, fondo transparente, la píldora de 26 px).
La misma decisión resuelve las dos.
**Lo que NO baja, y es decisión, no descuido**: `[data-state='open']` (0,3,0) y
`:focus-visible` (0,3,0). No estaban en la firma. Medido lo que cuesta dejarlos:
bajarlos también subiría `natural-time-picker` a **50/62** (reviven
`trigger-bg` y `open-trigger-border`). Registrado en `next-features.md` §13.
Guarda por **mutación** en `active-eidos-config.test.ts` (§55, 6 mutaciones,
**6 mordidas**): reposo desenvuelto · hover desenvuelto · `:not()` izado fuera
del `:where()` · suelo vaciado · una QUINTA regla de trigger fuera del suelo ·
y las dos reglas de estado bajadas en silencio.
---
**Última revisión**: 2026-08-25 (§55 el sobre de trigger de popover es el
suelo). Si algo en este doc no coincide con el código, el código gana — pero
abre un issue para que actualicemos el doc.

@ -1,3 +1,5 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { renderStaticCss, renderThemeCss } from './lib/render-css'
import { computeLightSolidScales } from './lib/on-solid'
@ -569,6 +571,107 @@ describe('ActiveEidos config', () => {
expect(css).toContain(":where([data-depth='sheet']) {");
});
/**
* §55 — THE POPOVER TRIGGER ENVELOPE IS THE FLOOR (signed 2026-08-25).
* Third application of the law: §12.9 above made the depth PLANE a floor,
* §B′ below made the palette cascade a ladder, and this one lands on the
* only baseline that lived in a HAND-AUTHORED recipe — so it is read from
* the file, not from `renderStaticCss()`.
*
* `popover.css` calls its trigger rule "a baseline button envelope" and
* shipped it at (0,2,0), hover at (0,5,0). That beat every host recipe at
* (0,1,0): the chat-message add-chip rendered a grey square beside its own
* pills, the calendar month/year selects TIED at (0,2,0) with their own rule
* and read 14px or 16px depending on which chunk loaded last (measured: 2 of
* 6 fresh loads landed on the wrong face), and the (0,5,0) hover erased the
* system's hover layer on every host that paints its own surface.
* `archetypes.css` (lines 9-21) had already written this law for the
* transversal layer and named its TWO deliberate exceptions; this envelope
* did a default's work and was in neither.
*/
it('§55 — popover trigger envelope rides the floor: rest + hover in :where(), never bare', () => {
const css = readFileSync(
resolve('src/uix/eidos/components/popover/popover.css'),
'utf8'
).replace(/\/\*[\s\S]*?\*\//g, '');
// popover.css has no nested at-rules (asserted below), so a flat split is
// exact — and it never credits a wrapper for what it holds.
const rules = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({
selector: m[1].replace(/\s+/g, ' ').trim(),
body: m[2]
}));
expect(css, 'nested at-rule in popover.css — the flat split stopped being exact').not.toMatch(
/@(media|supports|container)/
);
const BASE = "[data-popover-trigger]:not([data-archetype='field-trigger'])";
// (a) the REST envelope rides the floor — and it is the block that carries
// the envelope. An empty floor proves nothing.
const rest = rules.find((rule) => rule.selector === `:where(${BASE})`);
expect(rest, `no \`:where(${BASE}) {\` block in popover.css`).toBeDefined();
for (const token of [
'--popover-trigger-bg',
'--popover-trigger-height',
'--popover-trigger-radius',
'--popover-trigger-padding-inline',
'--popover-trigger-font-size',
'--popover-trigger-border'
])
expect(rest!.body, `floor block declares no ${token}`).toContain(`var(${token})`);
// (b) the HOVER rides the floor too. It was the (0,5,0) rung, the one that
// repainted the surface instead of letting the system's hover layer
// compose over whatever the host already painted.
const hover = rules.find(
(rule) => rule.selector.startsWith(':where(') && rule.selector.includes(`${BASE}:hover`)
);
expect(hover, 'no `:where(… :hover …) {` block in popover.css').toBeDefined();
expect(hover!.body).toContain('var(--popover-hover-trigger-bg)');
expect(hover!.body).toContain('var(--popover-hover-trigger-border)');
// (c) …and NEITHER bare form is back. `:where()` zeroes the specificity
// CONTRIBUTION, never the matching, so the `:not()` stays INSIDE it: the
// five field-trigger pickers are excluded exactly as before, at (0,0,0).
// Hoisting it out — `:where([data-popover-trigger]):not(…)` — would read
// (0,1,0), a fresh tie with every host recipe, i.e. the coin flip §12.9
// and §B′ were signed to forbid.
expect(css, 'the bare (0,2,0) envelope is back').not.toContain(`\n${BASE} {`);
expect(css, 'the bare (0,5,0) hover is back').not.toContain(`\n${BASE}:hover`);
expect(
css,
'the `:not()` was hoisted out of the `:where()` — (0,1,0), a new tie'
).not.toContain(':where([data-popover-trigger]):not(');
// (d) the two state rules the signature deliberately left ABOVE the floor
// keep their specificity. Mirror of the frost assertion in §12.9 and of
// the tone rung in §B′: same wrapper, opposite intent. Demoting them is a
// separate decision with its own measurement, not a tidy-up — a `:where()`
// here would drop `[data-state='open']` onto the floor and re-open the
// question this signature answered for two rules only.
expect(css, "`[data-state='open']` was demoted — outside the signed scope").toContain(
`\n${BASE}[data-state='open'] {`
);
expect(css, '`:focus-visible` was demoted — outside the signed scope').toContain(
`\n${BASE}:focus-visible {`
);
// (e) structural closer: EVERY rule aimed at the trigger is either on the
// floor or one of those two. A fifth one is a tie waiting to happen,
// whoever wrote it.
const triggerRules = rules.filter((rule) => rule.selector.includes('[data-popover-trigger]'));
for (const rule of triggerRules) {
if (rule.selector.startsWith(':where(')) continue;
expect(
[`${BASE}[data-state='open']`, `${BASE}:focus-visible`],
`popover.css: \`${rule.selector} {\` aims at the trigger off the floor`
).toContain(rule.selector);
}
// A guard that inspected NOTHING passes green: the census is the other half
// of the assertion. Four trigger rules — two on the floor, two above it.
expect(triggerRules).toHaveLength(4);
});
it('emits the Fase 4 frost atmosphere (opt-in [data-frost] + blur tokens)', () => {
const css = createThemeBaseEidos().renderStaticCss();
// Blur is a canonical primitive (`--blur-*`); the planes consume it instead

@ -107,13 +107,25 @@ compuesta DENTRO de un Feed, y su propia demo la monta suelta (medida en la de
`chat-log`); y `quick-react-hover-scale` — `transform` no está en la lista de
propiedades del guard y la celda es un botón portalado con regla `:hover`.
**Defecto conocido, NO tocado aquí**: el chip de añadir reacción lleva también
~~**Defecto conocido, NO tocado aquí**: el chip de añadir reacción lleva también
`data-popover-trigger`, y `[data-popover-trigger]:not([data-archetype='field-trigger'])`
pesa (0,2,0) contra los (0,1,0) de esta receta — así que su fondo, su borde, su
radio y su tinta los pinta `popover.css`, no `reaction-*`. Se ve en la demo: los
dos chips de reacción son píldoras redondas y el de añadir, un cuadrado gris. Es
la clase `gradient-picker` / `emoji-picker`; arreglarlo mueve píxel, así que
queda registrado, no corregido.
queda registrado, no corregido.~~
**RESUELTO 2026-08-25 — firma «el sobre de trigger de popover es el SUELO»**
(changelog §55). El arreglo NO fue de este componente: el sobre de `popover.css`
bajó a `:where(...)` (0,0,0), así que el bloque `[data-chat-message-reaction],
[data-chat-message-reaction-add]` gana ya las siete propiedades que siempre
declaró. **El chip es una píldora**, idéntica a sus hermanas — medido: 34×26,
radio 9999px, fondo `oklch(0.9911 0 0)`, letra 12px, padding 8px. Y al hover
recupera la capa de estado del sistema (el velo sobre su propio fondo) en vez
del repintado gris que le metía el `:hover` (0,5,0) del sobre. De paso queda
resuelto el empate latente de `chat-message.css:462` — el bloque «los chips
dentro de la píldora sueltan su cromo» está a (0,2,0) y **empataba** con el
sobre; hoy gana de forma determinista.
## Tokens (`--chat-message-*`)

@ -360,6 +360,15 @@
font-size: var(--font-size-xs);
color: var(--color-content-subtle);
}
/* This button is a `Popover.Trigger`, so it also wears the baseline envelope of
* `popover.css` — which, since 2026-08-25 (changelog §55), is a FLOOR at
* (0,0,0): the rules below win everything they name. What they do NOT name,
* the floor still paints, and today that is three axes, measured: `height`
* (36px), `border` (1px + `--popover-trigger-border`) and `background`
* (`--popover-trigger-bg`). This is a REGISTRATION, not a defect: the chrome is
* inherited on purpose. It is written down because `palabras` has no recipe
* block in `lib/recipes/base.ts` — `npm run theming:sentinel -- palabras` dies
* with "no recipe block" — so no census and no sentinel would ever surface it. */
[data-palabras-outline-trigger] {
display: inline-flex;
align-items: center;

@ -29,6 +29,18 @@ puede escribir, y el guard R-5.4 comprueba que ninguna miente.
| `description` | `description-font-family` · `description-font-size` · `description-line-height` · `description-fg` |
| `arrow` | `arrow-width` · `arrow-height` |
> **2026-08-25 — el sobre de trigger es un SUELO, no un techo** (firma,
> changelog §55). Las doce claves de `trigger` y `estado` de arriba se emiten
> ahora desde `:where([data-popover-trigger]:not([data-archetype='field-trigger']))`
> y su gemela de `:hover` — **especificidad (0,0,0)**. Siguen pintando entero al
> `Popover.Trigger` DESNUDO, que es para lo que se escribieron (medido: 69 de 76
> instancias del catálogo no mueven un valor de receta), y dejan de pisar al
> huésped que declara lo suyo. `[data-state='open']` y `:focus-visible`
> conservan su especificidad — fuera de esta firma, registrado en
> `next-features.md` §13. El `:not()` va DENTRO del `:where()`: anula la
> contribución de especificidad, no el emparejamiento, así que los cinco
> triggers de picker siguen excluidos igual.
**Lo que el guard R-5.4 da por silencioso, con su razón medida**
(`scripts/theming-sentinel-exceptions.ts`):

@ -11,8 +11,35 @@
* BOTH the inline field-icon triggers AND the standalone picker triggers (e.g.
* the time-picker clock button) that lack that marker. This generic envelope must
* NOT win over them (equal specificity → would otherwise depend on load order).
*
* ── THE ENVELOPE IS THE FLOOR, NOT THE CEILING (signed 2026-08-25, §55) ───
* Third application of the law §12.9 signed for the depth plane and §53 (B′)
* for the palette ladder: a BASELINE is emitted at specificity ZERO, so the host
* recipe wins wherever it SPEAKS while the baseline keeps painting everything
* the host leaves UNSAID — which is what "baseline" means. `archetypes.css`
* (lines 9-21) already wrote this law for the transversal layer — "These are
* DEFAULTS — a component recipe must ALWAYS be able to override them" — and
* named its TWO deliberate exceptions; this envelope does a default's work and
* was in neither. At (0,2,0) it beat every host recipe at (0,1,0): the
* chat-message add-chip rendered a grey square beside its own pills, the
* calendar month/year selects TIED at (0,2,0) with their own rule and read
* 14px or 16px depending on which chunk Vite emitted last, and chronos' plain
* button needed a (0,3,0) pin to read plain. Measured cost of the demotion:
* of 76 trigger instances across 16 routes, 69 do not move a pixel — every
* bare trigger, which is 100 % of the reason this envelope exists.
*
* `:not(...)` stays INSIDE the `:where()`. `:where()` zeroes the specificity
* CONTRIBUTION, never the matching, so the five field-trigger pickers stay
* excluded exactly as before — now at (0,0,0). Hoisting it out would make the
* rule (0,1,0): a fresh tie with every host recipe, i.e. the coin flip §12.9
* and §53 were signed to forbid.
*
* Signed scope is the REST envelope and its `:hover` — the latter was at
* (0,5,0) and killed the system's hover layer on every host that paints its
* own surface. `[data-state='open']` and `:focus-visible` keep their
* specificity: not part of this signature, registered in `next-features.md`.
*/
[data-popover-trigger]:not([data-archetype='field-trigger']) {
:where([data-popover-trigger]:not([data-archetype='field-trigger'])) {
display: inline-flex;
align-items: center;
justify-content: center;
@ -30,7 +57,11 @@
border-color var(--duration-fast) var(--ease-default);
}
[data-popover-trigger]:not([data-archetype='field-trigger']):hover:not([disabled]):not([data-disabled]) {
:where(
[data-popover-trigger]:not([data-archetype='field-trigger']):hover:not([disabled]):not(
[data-disabled]
)
) {
background: var(--popover-hover-trigger-bg);
border-color: var(--popover-hover-trigger-border);
}

Loading…
Cancel
Save

Powered by TurnKey Linux.