You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/next-features.md

1523 lines
99 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
title: Next features — initiative registry
type: registry
audience: human + agent
authority: backlog of user-decided future initiatives — records scope, sequencing and dependencies; it does NOT record verdicts (those live in the RFCs / changelog / book-deviations when executed)
status: living — items enter by explicit user decision during audits and sessions; remove an item when it ships (and link where it landed)
created: 2026-07-06
---
# Next features — initiative registry
Rules of this file: one entry per initiative, dated, with its origin (which
audit/session raised it), its scope in one paragraph, and its dependencies.
Entries are **registered, not started** — starting one is a session decision.
Detail lives in the linked doc, never copied here.
## 1. Tonal-ramp contrast parity (measure, then construct) — 2026-07-06 · Stage 1 DONE 2026-07-19 · Stage 2 CLOSED 2026-07-20
> **Stage 1 SHIPPED (2026-07-19).** Measured with `scripts/contrast-audit.ts`
> (33 scales × 2 modes); drift + user verdicts ratified as doctrine in
> [`theming/reference.md §40`](./theming/reference.md) + [`changelog.md §44`](./theming/changelog.md).
> Outcome: `text·11` = secondary tier, `text-strong` = AA-guaranteed; the border
> vocabulary is Radix-subtle by design (only `solid·9` clears 3:1 by
> construction) with a decorative-vs-load-bearing split; the focus ring is a
> config axis (`primitives.focusRing` + `color.focus`, single `outline` per §32)
> whose shipped default is soft — hardening is a config-value choice, kept as-is.
> **Stage 2 CLOSED (2026-07-20) — no solver needed.** The 6 decisions were
> locked (D1 band · D2 measured-pass · D3 flip-only · D4 opt-in · D5 runtime-first
> · D6 now), then **execution disproved the premise**: the template morph does not
> _compute_ text contrast, it **inherits** it (steps 11/12 copy the donor's L-curve
> verbatim; L-driven contrast is ~chroma-invariant under gamut-mapping), so any
> scale generated from a §40-compliant donor library clears the ratified text
> floors **by construction**. Verified on three banks — authored base, leave-one-out
> regeneration, and 45 out-of-distribution seeds — **0 hard-gate failures**
> (min WCAG 9.7:1). The luminance solver would have had nothing to resolve for
> realistic inputs → dropped as speculative. **Delivered instead:** the ratified
> pair table as shared data (`$color` → `CONTRAST_PAIRS`), the audit refactored to
> consume it + a morph-generated regression bank (`scripts/contrast-audit.ts`), and
> a CI guard that locks the inheritance (`eidos/lib/contrast-invariant.test.ts` —
> fails if a future donor/generator breaks it). With D2, the base stays verbatim
> ground-truth; **no base→seeds migration**.
> **Plan + decision + outcome record:**
> [`process/contrast-stage2-plan-2026-07.md`](./process/contrast-stage2-plan-2026-07.md);
> chronicle [`changelog.md §45`](./theming/changelog.md).
**Origin**: A.7 follow-up (theming audit F). The on-solid pair is now computed
at generation with a single criterion ([`rfcs/rfc-color-engine.md`](./rfcs/rfc-color-engine.md)
§8, `eidos/lib/on-solid.ts`). M3 still guarantees more: contrast **by
construction across its whole tonal ramp** (container/on-container pairs at
fixed tone deltas — ΔT 40 ≈ 3:1, ΔT 50 ≈ 4.5:1). Our equivalent promises
(text-11 on bg-2/element-3, border-7 vs bg-2 at the 3:1 non-text floor, text
over composited soft surfaces a2/a3) rest on Radix-style curation, unmeasured.
**Stage 1 — pair-contract verification** (the A.7 pattern generalized): a
canonical table of slot pairs + floors (a user-decided doctrine session),
a measurement pass over pairs × 33 scales × 2 modes at generation
(`on-solid.ts` generalizes; the compositing math exists in `$color`), and the
resulting drift list goes to user verdicts (retune vs annotated exception).
Infrastructure is small; the fallout is the unknown.
**Stage 2 — by-construction generator (CLOSED 2026-07-20, solver dropped).** The
plan proposed a luminance solver that resolves each step to satisfy Stage 1's
pair table for a seed. Execution disproved its premise: the morph **inherits**
text contrast from the donor (steps 11/12 copy the donor's L-curve verbatim;
L-driven contrast is ~chroma-invariant under gamut-mapping), so every scale from
a §40-compliant donor library clears the ratified floors by construction. Measured
across three banks (authored base, leave-one-out regeneration, 45 out-of-distribution
seeds): **0 hard-gate failures, min WCAG 9.7:1.** The solver had nothing to
resolve for realistic inputs → dropped (speculative). What shipped: the pair
table as shared data (`$color` `CONTRAST_PAIRS`), the audit consuming it + a
morph-generated regression bank, and a CI guard (`eidos/lib/contrast-invariant.test.ts`)
that locks the inheritance. The BASE stays verbatim ground-truth (D2); no
base→seeds migration.
**Sequence**: Stage 1 first (it is the spec for Stage 2). No M3-style `on-*`
vocabulary expansion — our slots already encode the pairs; the table
formalizes them without inflating the namespace.
## 2. Per-theme palette-contrast cascade emission — 2026-07-06
**Origin**: A.7 execution. The per-instance `palette-contrast` cascade lives
in the STATIC foundation — one polarity for all themes. Today polarity is
computed per configured theme and unanimous (33/33 scales agree across
base-light/base-dark); on disagreement the `*-light` theme wins
(`computeLightSolidScales`, documented in
[`rfcs/rfc-color-engine.md`](./rfcs/rfc-color-engine.md) §8). **Trigger**: the
day a real configured theme disagrees on a scale's polarity, emit the cascade
per-theme instead of statically. Not before — no speculative machinery.
## 3. Component audit (post-system) — 2026-07-06
**Origin**: user decision during the theming audit — "de momento no tocamos
los componentes, merecen una auditoría adicional una vez que se haya resuelto
estas". Gate: the system-level audit (F/G) closes first. Registered items so
far, to seed its scope:
- **Named-styles adoption by controls** (from A.3): controls/recipes that
hand-declare typography instead of consuming `--style-*` /
size-bundle typography — likely by error, to be confirmed per component.
- **Letter-spacing bundle adoption**: ~34 recipes consume `font-size` /
`line-height` from the size bundle but not `letter-spacing`; plus ~7 raw
letter-spacing literals in component CSS.
- **Tier-A `data-depth` stamping coverage**: overlays that should stamp their
plane (dialog / drawer / tooltip / menubar / combobox) per
[`rfcs/rfc-depth.md`](./rfcs/rfc-depth.md) §5.
- **Calendar family → shared layer/archetype** (user design verdict,
2026-07-07): the calendar-grid vocabulary (accent set, day geometry, the
holiday/event marks fixed in audit B.2) becomes a FIRST-CLASS shared layer
— today the family borrows it informally (measured: range-calendar
consumes 79 `--calendar-*` tokens, month-grid 65, year-grid 65,
date-range-picker 38, date-picker 4 — ~250 cross-component borrows), and
**chronos will draw from it too** when its track lands. Materialization to
decide at execution: a morfo part-archetype for the day cell (the 26-set
grows) and/or a `calendar-surface` shared recipe à la list-surface
(`--list-item-*` precedent). This supersedes "per-component composition
repairs" for the B.2 borrowers: the marks reach the whole family through
the shared layer instead.
- **Field family composition — segments under `[data-field]`** (user design
verdict, 2026-07-07, audit B.1): segment height derives from the SIZE axis
at the FIELD family level — `field` recipe declares `segment-height` with
size-scoped declarations (`calc(var(--field-control-height-{k}) -
var(--space-2))`; the TSC `size:` scope axis exists, unused so far), and
the per-component copies die (each x-field today re-declares the whole
`height-{xs..xl}` family the field recipe already has). **MANDATED by the
B.1-(i) verdict**: date/time/color-field MUST incorporate the Field
wrapper (segments under `[data-field][data-size]` — probed live: today
they don't) as part of the component audit; the three broken
per-component `segment-height` tokens and their consumers were removed
(they had computed `auto` since birth — zero visual change).
- Whatever F/G registers as "component-level adoption gap" in
`docs/audit/theming-audit.md` when that report lands (deliverable G of the
2026-07 theming audit).
## 4. `architecture/sema.md` hold-sync — 2026-07-06
**Origin**: A.4/A.5 execution (holds single-source). `SEMA_HOLDS_BY_INTENT`
(`src/uix/sema/holds.ts`) is now THE only hold table (resolver + visual
channel consume it; `sema-map.ts`'s per-family hold column died). The chapter
[`architecture/sema.md`](./architecture/sema.md) still narrates per-family
holds in ~34 places. **Blocked**: the file is the author's WIP (uncommitted
edits) — sync it when the author's pass lands, not before.
## 6. THM-2 rollout — per-instance palette to all surfaces — SHIPPED 2026-07-12; scope boundaries REVERTED by the open-color-cage initiative (2026-07-18/19)
Shipped in `05f0031c` (media-player Batch-4 + 14 surfaces + vocab + 4 props)
and `cc7e9a16` (avatar root). Every per-component palette cascade now collapses
into the shared `--palette-*` layer (size win, behavior-preserving for roles);
the vocabulary gained the alpha `surface`/`surface-hover` + `hover` slots.
> **REVERSED (2026-07-18/19) — "abrir la jaula del color".** THM-2 originally
> kept semantic controls (checkbox/radio/stepper/editable/select/file-upload)
> and `Avatar.Badge` role-restricted "by design". A later user decision reverses
> that: `color` accepts the FULL system (role / intent / 33 scales / raw CSS =
> `ComponentColorProp`) on **every** component, no exceptions — content-ink
> primitives keep their axis as an additive union. Enforced by two guards in
> `recipe-css-contract.test.ts`. Full record + the closed queue (incl. the
> chart SVG resolver and the Card/Avatar custom-color fix):
> [`process/open-color-cage-2026-07.md`](./process/open-color-cage-2026-07.md).
> Only `qr-code` stays out (its `color` is the QR module ink, not a scale).
## 5. Book editorial note — "el hold es suelo, no tijera" — 2026-07-06
**Origin**: A.4/A.5 verdict. The mechanism (hold = floor for the expression,
never a scissor; expression waits animation-finish after the hold, capped by
an absolute `MAX_EXPRESSION_WAIT_MS`, never derived from the hold) is recorded
in [`decisions/book-deviations.md`](./decisions/book-deviations.md) D.12 as a
candidate **editorial appendix** for the book (_Diseñando lo que ocurre_,
HOMOGENEIZADO edition) — the book gives qualitative regions and this is the
materialization doctrine worth feeding back to the source.
## 7. Chat block v2 — 2026-07-18
**Origin**: the `chat-*` v1 initiative (comparative study + plan, session
2026-07-18; user scoped v1 to the room core). v1 shipped `chat-log`
(Feed + end-anchored VirtualList), `chat-message`, `chat-composer`
(Textarea + FileUpload composition) and `chat-typing`, plus the
`$libs/chat` helpers and the VirtualList `anchor: 'end'` extension. The
deferred surface, registered with disposition (each item also lives in its
component README §Gaps):
- ~~**`chat-list`** — the conversation-list pane (rooms, unread badges,
typing-in-list).~~ **✅ SHIPPED 2026-07-19** — composes `Listbox`
(single-select + keyboard) with an avatar/title/preview/meta row; unread
bold + `Badge`, typing-in-list composes `ChatTyping`. Virtualization for
very long lists deferred (pairs with the VirtualList bidirectional item).
- ~~**`emoji-picker`** — a real picker component (color-picker scale:
categories, search, skin tones).~~ **✅ SHIPPED 2026-07-21** — composes
`Popover` + `Command` (search + grid keyboard + combobox/listbox a11y, its
documented "grid mode") + two `ToggleGroup`s (categories + skin tone). Data is
the vendored `$libs/emoji` (baked from emojibase, MIT — `scripts/generate-emoji-data.ts`):
~1,900 emoji, 316 tone-capable. The grid double-registers Command's
List/Item so it keeps the a11y but wears its own grid-cell styling. Delegated
(no own events). Full-grid virtualization + per-emoji tone + image emoji
(Twemoji/Noto) deferred. The quick-set Popover in `ChatMessage.ReactionAdd`
stays as the inline tapback bar (different UX).
- **Mention autocomplete (`@`)** — combobox-in-textarea for
`chat-composer`; pairs with the future `words`/`palabras` integration.
- **Threads UI** — the side-panel composition over the already-shipped
`Feed.Thread` semantics.
- ~~**Jump-to-message highlight** — `signal-emphasize-target` on
`chat-message` (the morfo event was deliberately left out of v1).~~
**✅ SHIPPED 2026-07-19** — `emphasized` prop → `data-emphasized` row flash
(visual-only, no sema event: reference apps flash silently; the app scrolls
via a reply's `onJump` then toggles `emphasized` for ~1.4s).
- ~~**Read-by list** — "seen by N" avatar row (Teams/Messenger pattern) on
`chat-message`.~~ **✅ SHIPPED 2026-07-19** — `ChatMessage.ReadBy` part
(the app composes `AvatarGroup`; the sender's `onMessageSeen` feeds it).
- **VirtualList chat-mode refinements** — bidirectional sparse ranges for
jump-into-history far from the loaded window.
- ~~**Typing avatars** — faces next to the `chat-typing` dots (Messenger).~~
**✅ SHIPPED 2026-07-19** — `ChatTyping.Avatars` slot (compose `AvatarGroup`).
- **Delegate-family ↔ agent chat** — the Aura reservation (§F6 of the
scene/pack initiative) once agent conversations land.
**Deps**: none to start `chat-list`/`emoji-picker`; mentions depend on the
`words` integration decision. The transport stays app-land (`$connection`).
## 8. `blocks` tier — reusable page-function compositions — 2026-07-21
**Origin**: ecosystem analysis session 2026-07-21; user decision: a new tier
for **blocks** — named compositions of canon components performing a page
function (sticky site header, hero, footer, app shell, docs shell…). Scope:
tier infrastructure (`src/uix/blocks/` + `$blocks`, doctrine at
`architecture/blocks.md`, B contract, `blocks-check` guard), **7 canon base
components first** (`sticky` · `empty-state` · `result` · `callout` ·
`prose` · `anchor-nav` · `sidebar` — full 9-phase route each), then the
block catalog (10 site + 10 app + 3 docs). Admission rule mirrors packs:
any behavior with contract surface is promoted to canon BEFORE the block
composes it. Detail + per-item fiches: the execution plan at
[`process/PLAN-blocks.md`](./process/PLAN-blocks.md).
**Status**: D-BLK decisions SIGNED 2026-07-21 (D-BLK.1 amended by the user:
the tier lives at `src/uix/blocks/`, not `src/blocks/`). **F0 SHIPPED
2026-07-21** (`069759604`: doctrine + `$blocks` + `blocks:check` + gallery).
**Reference study SHIPPED 2026-07-21** (`aff42fee0`:
[`process/RESEARCH-blocks-references.md`](./process/RESEARCH-blocks-references.md),
6 research tracks; every phase-0 now checks against it). Amendments E-1…E-5
SIGNED 2026-07-21: F1 = **8** canon components (+`nav-tree`, data-driven docs
navigation tree); parity-floor = v1 scope policy; F2 = **14** site blocks
(+banner · team · contact · content-section); distribution registered as §9;
⌘K stays app-land.
**F3 STARTED 2026-08-19** — `app-shell` shipped with a signed phase 0 (four
decisions: the block wires no services, two scroll models by prop, the top bar
inside the inset, generated bypass links) and a new canon component in front of
it, `SkipLink` (WCAG 2.4.1). It closes ledger row **A-95**, the one thing the
tier could not fix from inside any block: with the shell owning the page's
heights, an in-flow notice and a pinned header overlap by **0px** (measured;
before: 49px, with the hit test landing on the strip).
**Deps**: `scheduler` block gated on `chronos` landing; chat-room block
explicitly NOT here (canon `chat-*` family, §7 above).
## 9. Blocks distribution — registry + llms.txt + MCP — 2026-07-21
**Origin**: the blocks reference study (E-4 signed 2026-07-21; dossier
[`process/RESEARCH-blocks-references.md`](./process/RESEARCH-blocks-references.md)
§P6). The ecosystem's 2026 table stakes for component/block distribution are
agent-enumerable catalogs: a registry protocol (shadcn `registry.json` /
`registry-item.json`), `llms.txt` (+ `llms-full.txt` / `.md` content
negotiation) over the docs corpus, and an MCP server so agents can list and
install blocks (sv-blocks precedent). Scope when started: (a) `llms.txt`
generation from the docs corpus (cheap — the corpus is already Markdown,
pairs with F4 docs blocks); (b) a blocks/components registry manifest; (c)
an MCP server exposing catalog + morfo contracts (pairs with the
contract-introspection superation angle and the palabras F-B agentive
track). Registered, not started.
**Deps**: F2 catalog existing (something to distribute); F4.3 props-table
work shares the morfo-introspection substrate.
## 10. Barcode — deferred symbologies + GS1 profiles — 2026-07-29
**Origin**: the `Barcode` component (shipped 2026-07-29; design record +
dispositions in
[`eidos/components/barcode/README.md`](../src/uix/eidos/components/barcode/README.md)
§Gaps). v1 covers the Retail + Logistics set — Code 128 (A/B/C auto), EAN-13/8,
UPC-A/E, Code 39, ITF/ITF-14 — from their published ISO standards, in the
zero-dependency `$libs/barcode` encoder.
**Shipped the same day, out of this registry: the ISBN input profile.** An ISBN
barcode IS an EAN-13 (ISO 2108, Bookland 978/979), so it entered as a profile
over `ean13` — separators tolerated in the numeric symbologies, ISBN-10 read and
converted after validating its own base-11 check digit — and explicitly NOT as a
ninth `symbology` value, which would make `data-symbology` claim a standard that
does not exist. The doctrine is recorded in the component README §ISBN.
Four dispositions stay **diferir**, each a self-contained follow-up over the
same encoder:
- **Codabar · MSI · Pharmacode** — the niche tail JsBarcode still ships. Pure
table work: each is a symbology module + its vectors.
- **GS1-128 (FNC1 + Application Identifier parsing)** — a _profile_ of Code 128,
not a new encoder: the FNC1 code value plus an AI grammar so the HRI can print
the bracketed form. The natural pair for a logistics-facing app, and the same
shape the ISBN profile already established.
- **EAN-2 / EAN-5 add-ons + the upper `ISBN 978-…` line** — the periodical /
book supplements and the book-cover convention; they extend the EAN geometry
(a second text band, a composed add-on symbol) rather than replacing it. Hard
constraint recorded with them: **we do not hyphenate**. The hyphen positions
are not computable from the number — they need the International ISBN
Agency's prefix-range tables, versioned data with an expiry date, which is a
maintenance clock a zero-dependency library must not swallow. The hyphenated
string comes from the consumer's catalogue.
- **2D — DataMatrix · PDF417 · Aztec** — explicitly NOT this component: each is a
whole different encoder (own Reed–Solomon, matrix layout). They belong beside
`QrCode` as their own components, sharing the display-primitive shape.
**Deps**: none — the encoder's `BarcodeResult` (module lattice + guards + HRI
groups in module space) already generalises; the 2D entries would instead reuse
the `$libs/qr` shape.
## 11. Background — parallax, the scrim scale, and the pack seam — 2026-08-18
**Origin**: the `Background` component (design record, measurements and
dispositions in
[`eidos/components/background/README.md`](../src/uix/eidos/components/background/README.md);
plan and signed decisions in
[`process/PLAN-background.md`](./process/PLAN-background.md)). It ships as the
HOST of a surface's background layers — a child that pins itself behind its
parent's content, with the parent adopted by a foundation rule rather than
wrapped. Patterns, gradients, scrims, images and video are in; so is the WCAG
2.2.2 pause control.
**Scope of this registry entry** — what the component deliberately does NOT own
yet, each a self-contained follow-up:
- **Parallax and the scroll seam** — `animation-timeline: view()` with a
`ScrollProgress` fallback, `attach='fixed'`, the pointer `depth` axis. The
infrastructure landed with the component (`$adom`'s `ScrollProgress` and
`prefersReducedData`); the layers that consume it did not. It is the one
follow-up with a mechanism already chosen and measured against browser
support, so it is also the one most likely to move first.
- **The scrim `strength` scale is not ordered by weight** — measured: `subtle`
veils MORE than `overlay`, and `overlay` and `muted` resolve to the same
number. The names come from the semantic opacity scale, which says how opaque
an ELEMENT is; borrowing them for a VEIL imports an ordering that means
something else. Renaming or renumbering is an API change, not a fix in
passing.
- **A bright photograph is illegible at every scrim weight** — white copy over
the heaviest veil measures below AA. Either the scale gains a step above
`--opacity-subtle`, or the doctrine becomes "the answer is a graded scrim",
stated once and demonstrated.
- **A shared `<video>` state port** — the framework has a load-state contract
for `<img>` (`ImageProvider` in `soma/layers`) and none for `<video>`:
`ScrollFrames` listens by hand and so does `Background.Video`. Two consumers
is the threshold this codebase uses to extract a layer.
- **The `Ambient` pack honouring the pause** — the canon publishes the context
(`paused`, `reduced`, `seen`); the pack reads it. A task OF the pack, because
the dependency runs pack → framework and never back.
**Deps**: the parallax entry needs no new service — the two `$adom` ports it
consumes already shipped. The scrim entries are the author's call on the token
vocabulary, so they block on a decision, not on code.
## 12. El contrato de cascada del velo de estado — 2026-08-20
**Origen**: el eje `theme-reach` (F2-A, sesión del 2026-08-20). Al tokenizar
componente a componente aparecieron, medidas una a una, seis incidencias que
son **la misma**: nadie fijó nunca cómo compone el velo de `archetypes.css` con
las reglas de una receta. No se tocan dentro del eje de theming — mueven píxel
y son decisión aparte — pero cada una está medida y localizada.
**La raíz**: `archetypes.css` pinta el velo neutro como
`background-image: linear-gradient(var(--state-hover), var(--state-hover))`, y
lo hace con DOS pesos distintos según el arquetipo — `:where(...)` (0,0,0) para
`trigger`, y especificidad PLENA (0,5,0) para `item` / `option`, «INTENTIONAL
cross-component accent», dice su comentario. Una receta que declare fondo sobre
el mismo nodo entra en una relación que nunca se escribió.
### Las seis, con su medición
1. **El atajo `background:` cancela el velo — 131 declaraciones en 45
componentes.** El atajo fija `background-image: none`; sobre un arquetipo
velado por `:where()` eso lo mata para siempre, no sólo en hover. Medido con
`scripts/__statelayer-analysis.ts`, que comprueba contra el morfo compilado
que el atajo cae sobre el nodo VELADO y no sobre cualquiera. Por clase de
regla: **66 BASE** (mecánicas: pasar a `background-color` no mueve el
reposo), **32 ACENTO** (selected/checked/open: §38 pide `background-color`
para que el velo COMPONGA encima), **23 HOVER** (la migración de la firma 3)
y **10 OTRA** (disabled, dragging, variantes). **Cero valores con
gradiente**, así que el único riesgo técnico del paso a longhand no existe
en este catálogo. El píxel que se mueve: **32 nodos velados que hoy no
reaccionan al ratón** pasarían a hacerlo. Esto explica la adopción 21/135
que midió la auditoría del 2026-07-01: el velo estaba escrito y
estructuralmente derrotado.
2. **La prop `hoverable` de `table` no suprime nada.** El velo de `item` es
incondicional y a (0,5,0), así que una fila se ilumina al pasar el ratón
aunque `hoverable` esté apagada — verificado con hover real, y ocurre desde
antes de tocar la receta. O el arquetipo decide y la prop sobra, o la fila
no es un `item` para el sistema. Decisión de MORFO. **Precisión medida
2026-08-21** (revisión adversarial, `elementsFromPoint`): en `table` el
arquetipo `item` está declarado en la fila Y EN LA CELDA
(`morfo/components/table.ts:132` y `:155`), y el nodo que recibe el velo
bajo el puntero es la CELDA — la decisión de morfo debe contemplar los dos
portadores, no sólo la fila.
3. **En `tree-grid` el empate lo decide el ORDEN DE CARGA.** El velo de `item`
y el hover de la receta pesan EXACTAMENTE lo mismo (0,5,0), así que gana la
hoja que cargue después — y en el dev server varía entre recargas: seis
corridas de la misma configuración dieron cuatro «sin velo» y dos «con
velo». Un empate resuelto por orden es la misma fragilidad que el canon ya
documenta para las bandas de `z-index`.
4. **En `tree-grid` la banda mata el hover.** La regla de `striped` pesa
(0,8,0) — siete atributos más el `:nth-of-type` (la cifra (0,7,0) que se
anotó primero contaba uno de menos; recontada 2026-08-21) — contra los
(0,5,0) del hover y usa el atajo: en las filas pares no ocurre NADA al
pasar el ratón. El hover del componente es inconsistente entre filas.
5. **Un token de receta no puede ganar al arquetipo.** La regla de `item` fija
también `color` en el estado seleccionado, a (0,4,0) contra los (0,3,0) de
la receta (la cifra (0,5,0) anotada primero era la de la variante `:hover`
de la misma lista; recontada 2026-08-21): un `table.selected-row-fg` no
podía mover nada nunca. Se retiró
con su declaración muerta (un token que no mueve nada es un token que
miente). Mientras el arquetipo pinte tinta, ningún componente puede
tematizar la suya en ese estado.
6. **`scroll-area.thumb-bg-hover` no tiene velo al que migrar.** Su thumb lleva
`archetype: 'thumb'`, que `archetypes.css` no vela, así que la firma 3 no
puede ejecutarse ahí sin cambiar el arquetipo — decisión de MORFO. Es el
único knob de los seis de la cola que sigue sin resolver.
7. **En `grid-list` el hover pinta DOS VECES sobre el MISMO nodo** (medido
2026-08-21 al tokenizarlo). A diferencia de `table` / `tree-grid` —donde el
velo cae en la celda y el plano en la fila, nodos distintos— aquí el plano
de la receta (`background:` atajo, (0,3,0) → `background-color`) y el velo
del arquetipo ((0,5,0) → `background-image`) caen ambos en
`[data-grid-list-row]`: el atajo fija `background-image: none` pero pierde,
así que se ven los dos. Por eso el commit de `grid-list` **no acuñó**
`hover-row-bg`: sería un token que no puede ganar (mismo criterio que
`listbox.highlighted`). Y como en `table`, **fila Y celda llevan
`archetype: 'item'`** (`morfo/components/grid-list.ts:118` y `:141`), así
que bajo el puntero se apilan velo de celda + velo de fila + plano — tres
capas. El par de portadores del punto 2 aplica igual aquí.
8. **En `tree-view` el velo DERRAMA sobre el subárbol entero** (medido y
capturado 2026-08-21 al tokenizarlo). `archetype: 'item'` está declarado en
el **`<li>` branch** (`morfo/components/tree-view.ts:103`), que contiene el
`branch-control` Y el `branch-content` con todos los descendientes — así que
al pasar el ratón por la fila de una carpeta abierta se tiñe **la carpeta
entera**: el nodo velado mide **336 px** (la rama completa) frente a los
36 px de la fila que el usuario señala. Un branch anidado acumula además el
velo de sus ancestros, así que sus filas se ven más oscuras que las
hermanas. Es la tercera variante de la misma familia: `table` lo tiene en
fila Y celda (punto 2), `grid-list` en fila Y celda sobre el MISMO nodo
(punto 7), y aquí en un **ANCESTRO** del nodo señalado. Mover el arquetipo
del `<li>` al control es morfo, y mueve píxel en toda la demo.
Ojo al tokenizar la familia: en `tree-view` el `branch-control` **no lleva
archetype**, así que ahí el plano de la receta es la única pintura y su
`hover-row-bg` SÍ es un token legítimo — al revés que en `grid-list`. La
misma regla CSS cubre los dos nodos.
9. **El plano de profundidad `overlay` impone TIPOGRAFÍA, no sólo elevación**
— medido con CDP el 2026-08-21 al tokenizar `link-preview`.
`[data-depth='overlay']` (`generated/base.css:7088`) declara
`font-family: var(--style-label-font-family)` y
`line-height: var(--leading-ui)` junto al fondo, el borde y la sombra. Un
componente que estampe el plano para su elevación —**veinte lo hacen**—
hereda además esa tipografía, y su propia declaración empata a **(0,1,0)**
y **pierde por orden de carga** (la hoja generada va después del chunk de la
receta). En `link-preview` el par `font-family` / `line-height` de la receta
NO pinta: el plano impone 1.25 donde ella pide `--leading-normal` (1.5).
Cuarta ocurrencia de la clase «empate resuelto por orden de carga» (§12.3, y
las dos del calendario y de `tree-grid`).
⚠ **El par se deja DECLARADO a propósito.** Se retiró primero —daba 0 diffs,
porque no pintaba— y se RESTAURÓ el 2026-08-22 al revisar la sesión:
retirarlo no es neutral, **fija el empate a favor del plano** y con él el
1.25, que es justo la decisión que este punto tiene pendiente. Un empate por
orden de carga se registra, no se resuelve dentro de un commit de
tokenización. Los dos tokens quedan adjudicados en el ledger de R-5.4 con esa
razón, y la medición del orden se hizo en DEV (estable en cinco recargas);
en un bundle de producción está sin verificar.
Lo que hay que decidir: si un PLANO DE PROFUNDIDAD tiene competencia sobre
la tipografía o sólo sobre la superficie. Si sólo sobre la superficie, esas
dos líneas salen del plano y cada componente recupera las suyas; si el plano
manda, entonces ningún overlay del catálogo debe declarar familia ni
interlineado y hay que barrer los que lo intentan. Mueve píxel en los veinte
en cuanto se toque.
### Qué habría que decidir
En una frase: **quién manda cuando el arquetipo y la receta pintan el mismo
eje**. Las tres piezas concretas: si el velo de `item` baja a `:where()` como
el de `trigger` (y entonces la receta manda, y `hoverable` vuelve a significar
algo); si las recetas pasan a longhand en bloque (66 BASE + 32 ACENTO, mecánico
y sin gradientes de por medio); y si un arquetipo puede fijar `color` sobre un
componente que quiere tematizarlo.
**Deps**: ninguna de código. Es decisión del autor sobre píxel, y el
instrumental para medirla ya existe (`__statelayer-analysis.ts` +
`__theming-probe.ts` + `__tg-rowhover.ts`).
## 13. Huecos de instrumento y de demo del eje theme-reach — 2026-08-20
**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.
- ~~**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
`[data-command-input]`, y un clic de ratón sobre un input de texto SÍ casa
`:focus-visible` — la regla de foco (0,2,0) se quedaba con `border-color` y
el token de reposo leía muerto. El guard (`theming-sentinel.ts`) ahora hace
`blur()` tras abrir. La revisión midió la tasa completa del instrumento
viejo: **22 falsos negativos de 26 «no effect»** en F2-A, con tres causas
(el clic-foco, escribir sólo en `[data-{c}]` cuando las partes cuelgan de
`{c}-root`, y no leer `::before`/`::after`). Las tres, arregladas.
- ~~**El centinela no ve `::placeholder`.**~~ **ERA FALSO, y ARREGLADO
2026-08-21** (al tokenizar `textarea`): `getComputedStyle(el, '::placeholder')`
SÍ devuelve el valor — medido, el color centinela volvió tal cual. La
afirmación anterior nunca se comprobó y le costó a
`command.input-placeholder-fg` una entrada de ledger «verificada a mano» que
ha salido STALE y se ha borrado. El pseudo entra ahora en el snapshot del
guard, junto a `::before`/`::after`.
- **El centinela da FALSOS POSITIVOS cuando la receta re-declara el público en
el ELEMENTO** (medido 2026-08-23 en `avatar`). El guard escribe el token en
`:root` **y sobre cada nodo del componente** —para alcanzar un panel portalado
que no ve la raíz— y el comentario que justifica eso dice que escribir en
todas partes «no puede fingir una victoria a nivel de PROPIEDAD». Cierto para
una propiedad; **falso para una propiedad personalizada** que la receta vuelve
a declarar en un bloque `[data-size]`: ahí el inline del guard gana justo lo
que un tema pierde. `--avatar-group-overlap` leía VIVO y desde `:root` no
movía un píxel. No hay arreglo obvio (quitar la escritura sobre el nodo
resucita el punto ciego del portal); mientras tanto, **un público que la
receta re-declare sobre el elemento se mide a mano desde `:root`**.
- **El barrido de tallas del centinela para en `xl`.** Dos componentes tienen
paso `xxl` (`metrics`, `avatar`) y sus claves leen muertas: ocho excepciones
de ledger escritas por ese motivo. Añadir el paso es una línea, pero deja
STALE las seis de `metrics`, así que se hace con la re-verificación del
ledger entero, no en el commit de un componente.
- **El centinela no ve lo que pinta un componente COMPUESTO.** Los tokens que
media-player reenvía al Slider (`--media-player-track` →
`--slider-track-bg`) pintan en nodos `[data-slider-*]`, fuera del espacio de
atributos del componente medido. Sigue vivo; adjudicado por token en el
ledger de R-5.4.
- ~~**La sonda no pasa el ratón por un `<tr>`.**~~ **ARREGLADO 2026-08-21**:
`tr` entra en el filtro de hover de la sonda, y el guard R-5.4 añade un pase
de hover propio para los tokens `hover-*` (así adjudicaron
`table.hover-row-bg` y `gradient-picker.hover-preset-border`).
- **La sonda medía una demo aún CARGANDO.** El auto load-more de `feed`
mantiene `[data-busy]` ~3 s y su firma `commit-settle` anima `box-shadow`
sobre el nodo medido — los «5 diffs entre dos corridas del mismo código» del
handoff eran esto, no una animación sin localizar. **ARREGLADO 2026-08-21**:
sonda y guard esperan a que `[data-busy]` caiga; dos corridas del mismo
código dan 0 diffs y 0 nodos ausentes (6.264 valores).
- **Una sonda sobre una demo que no monta la parte compara CERO valores y
pasa.** Ocurrió con el skin de audio: `/uix/components/audio-player` da 404
porque la demo es la de `media-player` con el chip `media`, y la sonda cargaba
en modo vídeo. Resuelto con `scripts/__probe-audio-skin.ts`, pero la clase de
fallo es general: **una parte condicional necesita que la sonda la monte**.
Afecta al menos a captions, buffering y paneles portalados de media-player, al
estado vacío y la barra de carga de `command`, y al overview de
`date-range-picker`.
- **`date-range-picker` no expone `kind='month'` / `'year'` en su demo**
(norma N-6), así que la mitad de su API no se ve ni se mide.
- **Un componente COMPUESTO puede tener su cromo entero muerto sin que nada
avise.** `gradient-picker` re-declaraba altura, padding, tipografía, color,
fondo, borde, radio, hover y foco de su trigger, y NADA de ello pintaba:
`popover.css` gana con `[data-popover-trigger]:not([data-archetype='field-trigger'])`
(0,2,0 contra 0,1,0). Lo cazó el centinela —36 de 45 tokens sin efecto— y
retirarlo dio 0 diffs. **EL GUARD EXISTE desde 2026-08-21: R-5.4**
(`npm run theming:sentinel -- <c> <url>`, `scripts/theming-sentinel.ts` +
ledger `theming-sentinel-exceptions.ts`, doctrina en recipe-contract §4).
Nació con la segunda ocurrencia de la clase: el `item-gap` de `carousel`
contra un estilo INLINE de soma, invisible para todo análisis estático.
Los ocho componentes de F2-A pasan con su adjudicación escrita.
- **`--gp-current-gradient` es un nombre ABREVIADO en SOMA.** Lo estampa
`gradient-picker-provider.svelte.ts` (no el wrapper de eidos, como decía la
ficha), y viola theming §6 r5. No es knob de tema —es un canal de valor— pero
el nombre es deuda; renombrarlo toca soma, otra capa.
- **Hooks por CLASE en una receta de eidos.** `gradient-picker` usa
`.gradient-picker-trigger-swatch` y `.gradient-picker-trigger-label` en vez
de `data-*`; `eidos-lint` los cuenta (`class-hooks: 2`) y el centinela no los
ve, porque filtra nodos por atributo.
- **El foco del trigger de `gradient-picker` es opaco** (`--color-primary-solid`)
mientras el resto del catálogo lleva la mezcla suave del sistema
(`--focus-ring-color`, 52 %). Quedó sin tocar porque alinearlo mueve píxel —
y de hecho quedó sin token, porque la regla entera está muerta bajo popover.
- ~~**El censo PENALIZA consumir una capa compartida correctamente.**~~
**RESUELTO PARA LA FAMILIA CALENDAR 2026-08-21** y abierto para el resto: el
censo tiene ahora `LAYER_VOCABULARY` (mismo precedente que D-TH.2-b), así que
el `--calendar-*` que leen `range-calendar` / `month-grid` / `year-grid`
cuenta como `system`. **`list-surface` NO está registrado a propósito**: sus
consumidores puentean por PRIVADOS (`--_listbox-item-*: var(--list-item-*)`),
otra forma, y activarlo mueve diez componentes de golpe — sigue siendo
decisión aparte. Y aparece un techo NUEVO debajo: con el vocabulario de la
capa fuera del denominador, a los tres sólo les quedan los **forwards de
paleta THM-2** (`--_x-palette-*`, que el censo cuenta como `private` en TODO
el catálogo) y literales de layout, así que siguen leyendo 0 %. Reclasificar
el forward de paleta es la misma pregunta, un piso más abajo, y afecta a
todos.
- **El censo PENALIZA consumir una capa compartida correctamente.** `listbox`
adopta los cinco ejes de ritmo que `list-surface` posee — que es exactamente
lo que la doctrina de capas manda — y el censo los cuenta como `private`, así
que su alcance queda en 68 % en vez de ~86 %. Es el mismo defecto de medición
que D-TH.2-b arregló para los primitivos tipográficos (`--style-*` pasó a
contar como `system`): la capa ES la superficie de tema de ese eje. Afecta a
los consumidores de `list-surface` (select · combobox · command · listbox ·
los menús) y de `viewport-placement` (affix · fab · menu-dial). Extenderlo es
una decisión, no una corrección al paso.
- **`listbox` pinta el `highlighted` DOS veces**: la receta pone un
`--color-surface-overlay` plano y encima el arquetipo `item` añade su velo del
8 % — medido. El plano es el duplicado que §38 deprecó; se dejó sin token para
no bendecirlo, pero retirarlo mueve píxel.
- **La demo del bloque F2-A ya enseña los tokens** (`TokensPanel`), pero los
~155 componentes sin contrato siguen sin superficie donde verlos hasta que se
tokenicen.
Lo que la capa `calendar-surface` añadió (2026-08-21):
- **⚠ El font-size de los selectores month/year del calendario es una MONEDA AL
AIRE.** `[data-calendar-month-select][data-button]` (0,2,0) empata con
`[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0) — la
MISMA regla de `popover.css` que dejó muerto el cromo entero del trigger de
`gradient-picker` — y gana la hoja que cargue después: dos corridas del mismo
código dan 16px o 14px en los mismos nodos (24 diffs de instrumento, medidos
con la sonda dos veces). El pin del calendario existe justamente para que
caja, etiqueta y chevron lean UNA talla, así que perderlo es visible.
Arreglarlo es subir el peso del pin de forma determinista — decisión, porque
fija el píxel en un lado. Segunda ocurrencia de la clase §12.3 (empate por
orden de carga) y segunda víctima de la misma regla de popover.
- **Los tres consumidores de la capa siguen leyendo 0 % de alcance**, ahora por
los forwards de paleta THM-2 — ver el punto del censo más arriba.
Lo que la revisión adversarial de F2-A añadió (2026-08-21):
- ~~**PENDIENTE DE FIRMA — el `striped` de `table` es inerte con RowDetail.**~~
**FIRMADO Y EJECUTADO 2026-08-21**: `:nth-child(even of [data-table-row])`
cuenta filas de DATOS y la paridad ya no cae en los `<tr>` de detalle (la
demo no bandeaba NADA; ahora las filas 2 y 4 tiñen — 12 diffs medidos, 2
filas × 6 estados, y nada más se movió en 13.195 valores). El selector
conserva el (0,6,0) del viejo para que el hover siga ganando su empate (no
se importa el defecto §12.4 de tree-grid) y un `:where(:not([data-selected]))`
de peso cero deja el acento seleccionado al mando — verificado los tres.
La excepción del ledger se retiró; `striped-row-bg` adjudica VIVO (42/45).
- **El gap entre diapositivas de `carousel` es un canal de valor de soma.**
Soma lo estampa INLINE desde la prop `gap` (necesita el número para el
`flex-basis`), así que ninguna declaración de receta puede ganarle — el trío
`item-gap*` se retiró con 0 diffs. Si algún día debe ser temable, es soma
quien tendría que leer un token; decisión de otra capa.
- **Dos `header-z` llevan el literal `'2'`** (`table`, `tree-grid`) mientras los
otros catorce `*-z` del catálogo leen la escalera `--z-index-*`. Alinearlos es
decidir qué peldaño significa «cabecera pegajosa dentro de un scroller».
- **El spinner de `media-player` conserva `720ms` a pelo** mientras el spinner
idéntico de `feed` ganó dos tokens públicos de duración
(`sentinel-spinner-duration` / `-reduced-duration`). El mismo trato o una
anotación `functional:`, pero no las dos varas.
Lo que `grid-list` añadió (2026-08-21):
- **⚠ PENDIENTE DE FIRMA — la selección de fila de `grid-list` nunca ha
pintado.** La receta seleccionaba `[data-grid-list-row][data-selected]`, un
atributo que ni el morfo declara ni soma estampa: soma estampa
`data-state='selected'` + `aria-selected='true'`. La regla no casaba jamás,
así que su acento (`--_grid-list-palette-element`) no llegó nunca al píxel y
una fila seleccionada muestra sólo el velo NEUTRO del arquetipo —
visualmente indistinguible de una fila con el ratón encima (capturado). La
declaración muerta y su forward huérfano se retiraron en el commit de
tokenización con **0 diffs de computed**, que es la prueba de que estaban
muertos. **Repararlo mueve píxel** (re-apuntar a `[data-state='selected']`
haría aparecer un acento que el componente nunca tuvo), y choca de frente con
§12.5: el arquetipo `item` fija también `color` a (0,4,0) sobre el estado
seleccionado. Es la misma decisión del velo, no una corrección al paso.
El acento del SelectionCheckbox (`_palette-solid`) sí vive y se conservó.
- **`SHARED_LAYERS` declara a `grid-list` consumidor de `menu-indicator`, y no
lo es.** `theming-census.ts:432` lo lista, pero `lib/menu-indicator.css` no
tiene un solo selector que apunte a grid-list (sus 134 líneas targetean
`[data-context-menu-*]` / `[data-dropdown-menu-*]`), la receta no importa la
capa y soma no renderiza indicator: el checkbox **espeja** ese visual
pintándolo por su cuenta. La entrada hace que el informe generado escriba
«Consume la capa compartida `menu-indicator`» en la §4 de la ficha, que es
falso y dirige mal a quien la lea. Revisar las seis entradas de esa lista
contra los selectores reales de cada capa (`listbox`, `menubar` y
`navigation-menu` están en la misma entrada y tampoco aparecen en el fichero).
Lo que `textarea` añadió (2026-08-21):
- **⚠ El borde de foco de `textarea` no se ve al hacer CLIC — sólo con Tab.**
Medido: la regla de hover `[data-textarea-input]:hover:not([data-disabled]):not([data-readonly])`
pesa **(0,4,0)** y la de foco `[data-textarea][data-focused] [data-textarea-input]`
**(0,3,0)**, así que con foco y puntero a la vez gana el hover — y hacer clic
deja el puntero encima por definición. Verificado con las dos reglas
apuntando a colores distintos: sale el del hover. Debajo, `[data-invalid]`
pesa **(0,2,0)**: un campo inválido pierde su borde rojo tanto al enfocarlo
como al pasar el ratón. El anillo de foco del SISTEMA (`outline`) sí se ve
siempre, así que no es un fallo de accesibilidad, pero el orden de las tres
reglas de `border-color` está invertido respecto a lo que significan.
Arreglarlo mueve píxel: no entra en el commit de tokenización.
- ~~**El guard mide con el PUNTERO encima del componente.**~~ **ARREGLADO
2026-08-21**: `reopen()` corre antes de CADA token y hace clic en
`[data-{c}-input]` cuando el componente no tiene parte `content` — el
`blur()` que añadió la revisión adversarial quita el foco pero **no el
puntero**, así que `:hover` seguía casando durante toda la corrida y toda
regla de hover pisaba a sus vecinas de reposo. Es la mitad que aquel arreglo
dejó atrás. En `textarea` costaba tres falsos negativos
(`input-border`, `focus-input-border`, `invalid-input-border`); con
`page.mouse.move(0, 0)` tras cada blur, `input-border` revive solo. Corridos
los diez componentes con ledger: ningún otro cambia, un único STALE
(`command.input-placeholder-fg`, por el arreglo del `::placeholder`).
- **Hueco de demo: `card-group` no monta su encabezado ESTÁTICO.** La página
renderiza el título sólo en su forma de disclosure (`[data-button]`), así que
la mitad `[data-static]` del cromo del título —tinta, familia, tamaño,
interlineado y el hueco inferior— no se ve ni se mide: cinco tokens
adjudicados por eso. Misma clase que `date-range-picker` con
`kind='month'|'year'` (norma N-6).
- ~~**El test del SUELO del censo que F0 pedía no existe.**~~ **ESCRITO
2026-08-22** — `src/uix/eidos/theming-reach-floor.test.ts`: alcance global
≥ 56 %, literales ≤ 564, globales ≤ 1.156 y ≥ 14 componentes al 100 %, con
las dos muta-pruebas que el plan exige (un literal inyectado y cinco públicos
cambiados por primitivos, cada uno con su rojo). Medido de paso: **los techos
son el detector FINO y el porcentaje el grueso** — cinco knobs de ~4.700 no
mueven un porcentaje redondeado, pero sí un techo. La entrada original decía: El plan (§5, F0.d)
encarga «un test que fije el suelo: alcance global ≥ el de hoy y literales ≤
614 — el número SUBE o el test falla», con su muta-prueba. No hay
`theming-census.test.ts` ni equivalente bajo `src/uix/eidos/`, así que hoy
**nada impide que el alcance BAJE** entre sesiones: sólo se detectaría
mirando la cifra a mano. Con el censo ya endurecido (ver arriba) es el
momento de escribirlo.
- **⚠ `knob.arc-width` significaba DOS cosas con DOS defaults distintos** —
medido 2026-08-22 al tokenizarlo: el inset de la cara del dial (`--space-3`)
y la separación superior del puntero (`--space-2`). Un tema que lo escribiera
movía las tres declaraciones a la vez, pero SIN tema cada una valía algo
distinto. Separado en `arc-width` + `pointer-inset` con 0 diffs. Queda como
aviso de clase: **un mismo nombre con defaults distintos por sitio es una
colisión aunque el override funcione** — es la definición que el propio
generador usa para marcar `⚠`, y conviene barrer el catálogo buscando más.
- **⚠ `mockup` no tiene ruta de demo** (`/uix/components/mockup` da 404), como
le pasaba a `picker-shell`. Con 29 knobs al 38 % es el mayor de los que
quedan sin tocar de su tramo, y sin demo no hay sonda ni guard: habría que
medirlo prestado o darle página.
- **⚠ R-5.1 y R-5.2 no están implementadas** en `component-audit` (0 disparos
hoy; sólo existe R-5.3 de gramática y R-5.4 de alcance). El plan las da por
hechas en F0 «en `warn`» y F3 las quiere en `error`, así que **F3 no puede
cerrarse sin escribirlas primero**. Cuando lleguen, los componentes con
alcance parcial necesitan su marca `R-5.x exception:` en el README — ya
puestas en los 16 de F2-B que no llegan al 100 %.
- **Cuatro componentes salen NEEDS-WORK en `component:audit`** y son
PREEXISTENTES (ninguno tiene un commit de F2-B en su código): `badge`,
`gradient-picker`, `mockup` y `motion`. Anotado para que nadie los atribuya
al eje de theming al leer el informe.
- **⚠ Un worktree de comparación NO hidrata si su `node_modules` es una
junction fuera del root** — Vite responde **403** a `@fs/…` y SvelteKit no
arranca, así que la página SSR se ve pero ningún componente monta. Mordió en
la revisión adversarial de F2-B, que comparó contra una base muerta sin que
nada lo delatara. Se arregla con `server: { fs: { allow: [...] } }` en el
`vite.config.ts` del worktree, y la regla que generaliza es **comprobar que
un componente monta ANTES de comparar**. (Memoria
`worktree-devserver-fsallow`; vuelve a morder cada vez.)
- **⚠ `--radius-xs` NO EXISTE, y una receta lo referenciaba.** La escala de
radios del sistema es `sm | md | lg | xl | xxl` (+ `none`, `full`,
`default`), sin `xs`. `text-focus` escribía
`border-radius: var(--radius-xs, 3px)`: **pintaba el fallback**, así que
nadie lo notó — hasta que tokenizarlo sin fallback dejó la variable vacía, la
declaración inválida y las cuatro esquinas cuadradas (**48 diffs**, medidos
2026-08-22). Corregido a `3px` verbatim, el valor que de verdad pintaba.
Lo que esto dice del catálogo: **un `var(--fantasma, fallback)` es una
referencia muerta que funciona por accidente**, y el guard G2 que el plan
menciona («referencias-fantasma contra el contrato derivado») o no existe o
no cubre los fallbacks. Barrer el catálogo buscando `var(--…, …)` cuyo primer
nombre no esté en el contrato derivado es un pase mecánico que se paga solo.
- **⚠ `filter` y `backdrop-filter` faltaban en la lista de propiedades del
guard** — arreglado 2026-08-22. `text-focus.glow-color` vive dentro de un
`drop-shadow()` y leía «no effect» estando vivo; `KNOB_PROPS` del censo sí
las tenía, así que el censo contaba el knob y el guard no podía verlo.
Verificados los 30 componentes con ledger tras el cambio: cero regresiones,
cero STALE.
- **⚠ El guard CONGELA las transiciones, y eso esconde los tokens cuyo trabajo
ES la transición.** La congelación existe por una razón buena y documentada
(una propiedad transicionada devuelve su valor INICIAL justo tras escribir el
token, y un token vivo parecía muerto), pero deja ciego al guard ante
`text-circular.char-transition`, que guarda una transición completa. Medido
sin congelar: alcanza. Si aparecen más tokens de este tipo —transición,
animación, easing— merecen un pase propio sin congelación, como el que ya
tiene el hover.
- **⚠ El valor centinela GRANDE no prueba nada contra una propiedad capada.**
`skin-media-player` calcula su ancho como `min(100%, base × scale)`: el
`1234px` del guard se capa al contenedor y los siete tokens de tamaño leen
«no effect» aunque estén vivos. Con valores PEQUEÑOS (111/122/133px y escalas
0.11/0.22/0.33) alcanzan exactamente. Es primo del caso ya registrado «un
valor centinela igual al real lee como no efecto» (`9999px` contra un
`--radius-full` que ya era 9999px): **el centinela tiene que ser imposible
EN LA DIRECCIÓN en que la propiedad puede moverse**. Un guard que probara
ambos extremos —uno muy grande y uno muy pequeño— cazaría las dos familias.
Lo que la REVISIÓN ADVERSARIAL de F2-B destapó (2026-08-22):
- **`drag-drop.preview-z` era un token que MENTÍA, y lo escondió una
adjudicación hecha sobre un nodo sintético.** El ledger decía «forzado →
alcanza (100 → 4321)», pero ese nodo lo había creado yo con JS. El preview
REAL —el que soma monta durante un arrastre de verdad— lleva
`z-index: 9999` **INLINE** (`drag-drop-provider.svelte.ts:663,671`), así que
ninguna cascada puede ganarle: el token resolvía a 4321 y el nodo computaba 9999. Retirado con 0 diffs, que es la prueba. Misma clase que el `item-gap*`
de `carousel` que retiró la revisión de F2-A: **canal de valor de soma, no
superficie de tema**.
La lección para el protocolo: **una adjudicación «forzado → alcanza» vale lo
que valga el nodo que se forzó**. Si lo creaste tú, no lleva lo que soma le
pone. Verificado el resto de la misma clase: `clipboard` (indicador real tras
un clic real) y `picker-shell` (pie real) coinciden con lo adjudicado y no
llevan inline; `skeleton-line` lleva inline pero de `inline-size`, no del
eje tokenizado.
- **El orden de carga NO es el mismo en dev que en producción.** Medido con
`npm run build` + preview y CDP: en dev la receta de `link-preview` va
ANTES del plano `overlay`, en producción queda EN MEDIO de sus dos reglas.
El ganador coincide por casualidad —hay dos reglas del plano y una queda
siempre al final—, pero la fragilidad de §12.9 es real y ahora está medida en
los dos entornos, no supuesta.
- **⚠ Un worktree de comparación NO hidrata si su `node_modules` es una
junction fuera del root**: Vite responde **403** a `@fs/…` y SvelteKit no
arranca, así que la página SSR se ve pero ningún componente monta. La primera
pasada de esta revisión comparó contra una base muerta y dio «77 casos, 1
diferencia» sin que eso significara nada. Se arregla con
`server: { fs: { allow: [...] } }` en el `vite.config.ts` del worktree. Es la
memoria `worktree-devserver-fsallow`, y vuelve a morder cada vez.
Lo que la cola pequeña añadió (2026-08-22):
- **⚠ El censo no distingue «0 % por deuda» de «0 % POR NATURALEZA», y ya son
cinco componentes.** Medidos uno a uno el 2026-08-22, `aspect-ratio`,
`text-blur`, `cascade`, `motion` y `date-picker` tienen alcance 0 % y
**ninguno tiene contrato que escribir**: el knob de `aspect-ratio` está
PRESTADO de `box` (la ficha ya lo marca ⤴) porque es una faceta suya, no un
componente; los de `text-blur` son el `1px` de la técnica sr-only; los de
`cascade` y `motion` son el `opacity` del gate antiparpadeo, mecánica del
canal de motion cuyo valor tematizable vive en `EidosConfig.motion`; y los de
`date-picker` son la corrección `max-content` que impide que el pie del
popover desborde, con un único valor correcto. Sus veredictos §5 quedan
escritos.
Mientras el censo los cuente igual que a un componente con deuda real, la
cifra global miente por abajo y el gate de F3 («censo 100 %») es inalcanzable
por construcción. Lo que hace falta es una clase más —`structural`, junto a
`system`— o una marca en la ficha que los saque del denominador, como
`LAYER_VOCABULARY` hizo con la familia calendar. Es la misma pregunta que ya
está abierta para las capas compartidas, un piso más abajo.
Lo que `picker-shell` añadió (2026-08-21):
- **⚠ `picker-shell` no tiene ruta de demo propia** (`/uix/components/picker-shell`
da 404) y sus partes se llaman `data-picker-header` / `-body` / `-footer` en
vez de `data-picker-shell-{parte}` — genéricas **a propósito**, dice su
receta, «so every picker gets the same visual contract for free». La suma de
las dos cosas deja al instrumental ciego: la sonda genérica devolvió **0
nodos**, que es el antipatrón de comparar cero valores y pasar. Se tokenizó
con sonda dirigida (abre el popover del `date-picker`, captura los seis nodos
reales en las cuatro `data-picker-size`: 576 valores, 0 diffs) y el guard
R-5.4 ganó un `COMPONENT_OVERRIDES` para poder abrirlo y filtrarlo — acotado
a este componente, con los otros catorce verificados sin cambio. Lo de fondo
sigue abierto: **una demo propia**, y decidir si el precio de los nombres
genéricos (un contrato visual común para todos los pickers) vale la ceguera
del instrumental.
- **El eje de talla de `picker-shell` es `data-picker-size` sobre un ancestro
PORTALADO**, así que el TSC no puede emitir su cascada (`scope: 'size:xs'`
generaría `[data-picker-shell][data-size='xs']`, que no casa nunca). Es el
único componente del eje cuyas coordenadas por talla quedan como públicos
planos que la receta consume desde sus propios selectores. Si algún día el
TSC gana un eje de scope configurable, éste es su caso de prueba.
Lo que `code-block` añadió (2026-08-21):
- **⚠ La demo de `code-block` NO ENSEÑA el `code-block`: el harness del sitio
de documentación le pinta el `<pre>` por encima.**
`[data-uix-docs] pre` (`web/routes/uix/uix.css:801`) pesa **(0,1,1)** y gana
a `[data-code-block-pre]` **(0,1,0)** — y `[data-uix-docs]` es el shell de
TODAS las páginas, así que la regla alcanza cualquier demo. Medidas **siete
propiedades pisadas**: `padding` (16px 20px del harness contra los 12px de la
receta), `border` (1px sólido contra ninguno), `border-radius` (8px contra
0), `background-color` (gris contra transparente), `font-family`,
`font-size` (13px contra 14px) y `line-height` (18.2px contra 20.3px). El
fondo, el borde y el radio que se ven en esa página son del sitio, no del
componente. Es la clase «la demo puede tapar el componente» (precedente
`proof-of-human`), pero aquí son siete propiedades y le toca justo al
componente cuya razón de ser es mostrar código. Por eso `pre-padding` lee
muerto en el guard y está adjudicado con esa medición.
Acotar la regla del harness es tocar el CSS del SITIO, que sirve a las ~162
páginas y a los bloques de código de las fichas: no entra en el commit de un
componente. Decisión aparte.
**No es un caso aislado — la misma familia de reglas pisa también a `code`**
(medido 2026-08-21 al tokenizarlo, con CDP): `[data-uix-docs] code` declara
`font-family`, `font-size`, `padding` y `border-radius`, y gana (0,1,1)
contra la regla BASE del componente (0,1,0). Sus reglas de VARIANTE (0,2,0)
sí ganan, así que el efecto es selectivo y por eso sólo su `radius` lee
muerto — 3px del sitio donde el componente pide 4px. Cualquier decisión sobre
la regla del harness cubre los dos componentes.
- **⚠ El guard de claves duplicadas del handoff INSPECCIONA EL VACÍO.** El
comando documentado en `CONTINUE-theming.md` («Trampas que costaron un commit
cada una») es
`grep -oE "^ '?[a-z0-9-]+'?: \{" src/uix/eidos/lib/recipes/base.ts | …`,
y **`grep -E` no interpreta ` ` como tabulador** — lo lee como una `t`
literal, así que el patrón no casa NADA: 0 coincidencias sobre 130 bloques
reales. El guard salía siempre «vacío = OK» sin haber mirado nada, que es la
definición del antipatrón que el propio proyecto tiene registrado. Con un tab
real en el patrón encuentra los 130 (comprobado: cero duplicados en el árbol,
así que no hay daño que reparar). El handoff queda corregido con la versión
que sí mira.
Lo que `text-gradient` destapó (2026-08-22) — **PENDIENTE DE FIRMA, apuntado
para resolver después** (autor, 2026-08-22):
- **⚠ La familia text-effects ACUÑA tipografía propia contra el canon.** Cinco
claves del contrato: `text-focus.word-size` (`var(--font-size-4xl, 3rem)` —
`4xl` es un paso que la escala canónica `xxs..xxxl` de
`lib/primitives/typography.ts` NO tiene: idioma Tailwind del import de julio
`63b371d5c`, pinta el fallback) y `word-weight` (900) ·
`text-circular.weight` (900) y `font-size` (1.5rem) ·
`text-gradient.font-weight` (500, subida el 22 en `97a6f03d7`). Son opiniones
del seed (react-bits) que el protocolo «valor verbatim» convirtió en contrato
sin la pregunta previa: ¿un tratamiento de pintura sobre texto real tiene
tipografía propia? La doctrina fijada en F2-B (regla 2, cinco precedentes:
`label`, `code`, `kbd`, `announce`, `code-block`) dice que una receta
CONSUME la capa tipográfica (`--style-*`) o HEREDA (`link` → `inherit`):
un TextGradient dentro de un `h1` ES un h1.
**Son SIETE claves en CUATRO de los cinco componentes** (medido 2026-08-22 al
adjudicar `text-scramble`): las cinco de arriba más `text-scramble.font` y
`.size`, éstas aún sin declarar en `base.ts`. Y la de `text-scramble` es la
peor de todas porque además apunta fuera del canon: `var(--font-mono, …)`,
nombre que **no existe** —el canónico es `--font-family-mono`—, así que pinta
`ui-monospace` y **el componente ignora la mono del tema** (`Azeret Mono`).
Medido en navegador. `text-blur` es el único limpio de la familia: hereda.
**Resolución propuesta**: retirar las cinco claves y heredar del contexto.
Mueve defaults (TextFocus deja de imponer 3rem/900, TextCircular 1.5rem/900,
TextGradient 500) → firma del autor. Con ello muere el fantasma `4xl` sin
tocar el módulo de tipografía, que es canon y está **prohibido modificar**
(autor, 2026-08-22). Refuerza además el barrido mecánico que `--radius-xs`
ya pedía: `var(--nombre, …)` cuyo nombre no emite ningún generador.
- **⚠ El barrido mecánico está HECHO: 9 referencias fantasma con fallback en el
catálogo de componentes** (2026-08-22). Es lo que pedían el hallazgo de
`--radius-xs` y el de `--font-size-4xl`, y ya no hay que suponer cuántas
quedan. Método: recoger las **5.793** custom properties declaradas en TODO
`src/uix/eidos` (fundación + capas + recetas + `generated/`) y buscar en las
recetas los `var(--x, fallback)` cuyo `--x` sea vocabulario de SISTEMA y no
esté declarado — el sistema nunca lo escribe en runtime, así que un nombre
ausente es una errata. (Los `var(--x)` SIN fallback ya los guarda
`recipe-css-contract`; el hueco eran justo los que llevan fallback, que
«funcionan por accidente».)
| referencia inexistente | dónde | qué pinta en su lugar |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--color-content-tertiary` ×3 | `float-panel.css:227` · `proof-of-human/clock.css:87` · `proof-of-human/rotate-align.css:114` | `--color-content-secondary`. **Es el caso histórico** que `recipe-contract` §1 cita como origen de la regla («shipped a month that way») y sigue vivo en tres recetas |
| `--style-{body,prose,label,caption}-font-variation-settings` ×4 | `text.css:32,55,65,75` | `normal` — los estilos con nombre emiten 6 claves y ésta no está, así que **el eje variable no llega nunca al primitivo `Text`** |
| `--font-family-sans` | `palabras.css:296` | `sans-serif` del sistema. El canónico es `--font-family-primary` (palabras es track WIP) |
| `--font-mono` | `text-scramble.css:11` | `ui-monospace`. El canónico es `--font-family-mono` |
Ninguna se arregla aquí: dos son tipografía y esperan la firma 1.bis, y las
otras dos familias son decisión de diseño (¿existe un slot `tertiary`?
¿el eje variable debe emitirse por estilo?). El barrido, en cambio, **debería
ser un guard**: la lógica es la misma que la del test de fantasmas sin
fallback que ya existe, con el filtro de vocabulario de sistema encima.
Lo que `chart` destapó (2026-08-22):
- **~~El censo penaliza consumir la capa tipográfica~~ — RESUELTO el mismo día,
y la nota estaba equivocada.** Escribí que las doce declaraciones de `chart`
que pasaban a `var(--style-caption|label-font-size)` seguían contando
`global` porque el censo sólo exime `--style-*` a los seis primitivos. El
censo medía BIEN: lo que faltaba era la **costura**. PLAN §2-A pide que todo
knob lea `--{c}-{slot}`, y leer el canon a pelo desde el CSS deja a un tema
sin poder retocar ESE componente sin mover el rol en toda la app. Con seis
claves `--chart-{caption,label}-*` cuyo VALOR es `var(--style-{rol}-*)`,
`global` cayó a cero y el alcance a 89 %. **La lección: cuando la métrica y
la doctrina discrepan, sospecha de la lectura antes que del instrumento.**
El molde canónico ya estaba escrito en la vertebración tipográfica
(`--accordion-trigger-font-family: var(--style-label-font-family)`), y D-TH.2
aclara que sólo los PRIMITIVOS quedan fuera. `listbox` (68 %, ritmo de fila
de `list-surface`) es la misma pregunta un piso más arriba: **una capa
compartida no se lee a pelo, se cose** — pendiente de revisar con este molde.
- **⚠ Un READOUT de dato no tiene estilo con nombre en el canon.** El número del
gauge (`--font-size-xl`), el total de un funnel, el valor de un `Stat`: no
son encabezados (`h2` está en ese tamaño pero significa otra cosa) ni texto de
cuerpo. Su costura toma el bundle de talla, que es correcto pero no dice nada
del ROL. Si aparecen más, la respuesta es un estilo con nombre nuevo en el
módulo de tipografía — firma del autor, y el módulo no se toca sin ella.
- **⚠ La clase `private` del censo ES, en su mayor parte, el PUENTE DE PALETA
THM-2** (medido 2026-08-22). De las 687 declaraciones que leen un privado en
el catálogo, **130 leen un `--_{c}-palette-*`**, y son las que dominan la
columna `private` de los componentes con paleta: `tag-group` 14/14,
`stepper` 11/11, `radio-group` 10, `checkbox` 8, `file-upload` 8,
`calendar`/`editable`/los tres grids 7 cada uno.
**No es deuda y no debe acuñarse**: el puente lo alimenta la capa compartida
por instancia desde `[data-color]`, y un público encima
(`--{c}-selected-bg: var(--_{c}-palette-surface)`) dejaría que un tema lo
fijara y **matara en silencio el `color=` de cada instancia** — la misma clase
de daño que «declarar un público de facto mata una escala» (`knob`). El
componente ya expone sus nueve roles × variante como claves públicas: ésa es
su superficie de color.
Consecuencia para la métrica: **el techo de un componente con paleta no es el
100 %**, y su cifra no compara con la de uno sin ella — el denominador incluye
un puente que por diseño no se acuña. Tres ya adjudicados con esta razón
(`tag-group` 65 %, `stepper` 79 %, `timeline` 73 %); los demás la heredan
cuando les toque. Si algún día se quiere una cifra comparable, la respuesta es
una clase `bridge` en el censo, no acuñar los tokens.
- **⚠ La capa `list-surface` no es alcanzable por un tema — MEDIDO
2026-08-22.** Ni por su propio nombre ni por el puente que acuñan sus
consumidores. Con el menú abierto y la fila a 36 px: un tema que escribe
`--list-item-height: 1234px` en `:root` no mueve nada, y uno que escribe
`--dropdown-menu-item-height` tampoco. Es cascada: la capa declara sobre
`[data-list-surface][data-size]` y el puente sobre `[data-{c}-content]`; las
dos ganan a `:root`.
Por eso `listbox` **no** se cosió como sus hermanos: copiar el puente habría
subido la cifra sin dar alcance (un token que miente). La salida es la de
`calendar-surface`: que el vocabulario de la capa viva en una entrada de
receta y el gancho sólo lo RESUELVA por talla. Toca los diez consumidores de
golpe → eje propio.
Y deja un aviso sobre el instrumento: **el centinela da por vivos los tokens
de esta forma** porque escribe inline en el nodo, que gana a la regla del
gancho. Para un token que un tema sólo puede escribir en `:root`, «mueve un
computed» no equivale a «un tema lo alcanza».
- **⚠ La FIRMA DE SALIDA está escrita a mano tres veces** (medido 2026-08-22).
`data-last-action` × 3 intents × 4 lados tiñe el borde durante la animación
de cierre, y la misma matriz vive en `popover` (22 reglas), `drawer` (22) y
`dialog` (9), leyendo `--color-{fulfill,threat,neutral}-element`. **Y ya ha
derivado**: dialog tiñe sólo `border-top-color` mientras popover y drawer usan
el shorthand con `--border-width-medium`, así que en dos de tres el GROSOR
también cambia al salir.
La tabla morfo→eidos la sitúa como mapeo TRANSVERSAL (« `events[].prewrite` →
tinting the exit anim by cause»), misma fila que los arquetipos y los
selectores `[data-event=…]`: es el canal visual de sema materializado por
eidos, como la capa de estado vive en `archetypes.css`. Coserlo por componente
sería triplicar un vocabulario que debería vivir UNA vez; **la salida es una
regla transversal sobre el arquetipo `content`**, con los colores de intent
(que son canon) — 53 knobs a `system` y un tema que retoca la firma una sola
vez. Lleva una firma dentro: ¿la salida tiñe sólo el color o también el
grosor?
- **⚠ El test de huérfanos cuenta un COMENTARIO como consumo.**
`--popover-bg` no lo leía ninguna regla; el único sitio del corpus que lo
nombraba era un comentario de `popover-arrow.svelte`. El test
`does not leave declared public recipe variables orphaned` busca el nombre
como TEXTO en los ficheros del componente, así que un comentario lo mantiene
«vivo» para siempre. Retirado el token y corregido el comentario
(2026-08-22); el test ganaría con despojar comentarios antes de buscar,
igual que hace el censo.
- **⚠ `color-picker` arrastra 23 tokens sin adjudicar, ANTERIORES al eje**
(2026-08-22). Su costura los dejó en 23 —eran 24 antes—, pero el bloque
viejo necesita su propio pase de medición: las muestras (7 claves), la rueda
de tono (`hue-0..360` + `saturation-floor`), el ancho del panel por talla,
el cuentagotas y las dos de transición. Dos causas ya descartadas: no es que
el panel no abra (abre y sobrevive al blur, al parqueo del ratón y al forzado
de `data-size`) ni que el guard no vea sus nodos (ya se le dieron las dos
familias que le faltaban).
- **~~Un falso negativo del centinela sin causa~~ — RESUELTO en la revisión
adversarial del bloque (2026-08-22).** `float-panel.resize-grip-fg` movía
aislado y leía muerto en una corrida completa. La causa: **la pasada de HOVER
deja el puntero sobre el último nodo que tocó**, y una regla `:hover` gana a
la de reposo con la que comparte nodo — el asa re-apunta su color al acento
al pasar el ratón. Así que CUALQUIER token de reposo probado después de un
token `hover-*` podía leer muerto. Es la misma clase que el envenenamiento
por clic-foco que F2-A arregló, una pasada más tarde. Arreglado aparcando el
puntero tras la pasada de hover; el propio guard delató entonces DOS
excepciones STALE (`float-panel.resize-grip-fg` y `media-player.track`),
retiradas. Confirma la regla de F2-A: **un falso negativo siempre tiene
causa.**
- **⚠⚠ EL VELO DEL `drawer` NO PINTA — defecto real, medido y visto**
(2026-08-22). Su regla es
`background: color-mix(in srgb, var(--drawer-overlay-bg) var(--drawer-overlay-opacity), transparent)`,
y **soma escribe `--drawer-overlay-opacity: 1` INLINE** en el nodo del velo (es
su progreso de arrastre, un número sin unidad). `color-mix` exige un
PORCENTAJE ahí: con `1` la función entera es inválida y el fondo cae a
`transparent`. Computado en el velo abierto: `rgba(0, 0, 0, 0)`; escribiendo
`80%` en el propio nodo pinta. **En la captura se ve**: el fondo detrás del
cajón está DESENFOCADO pero no atenuado — el `backdrop-filter` funciona y el
tinte no existe.
No se arregla aquí porque mueve píxel y toca soma (§7.8). El arreglo puede
ser de un lado o del otro: que soma escriba un porcentaje, o que la receta
no use ese slot como porcentaje. Y la lección general: **un valor que soma
escribe inline y una receta consume tiene que compartir UNIDAD**, o el fallo
es silencioso.
- **⚠ Las tallas del `drawer` son canal de valor en el eje que manda**: soma
escribe `width` (o `height`, según el lado) INLINE en el contenido, así que
`content-width-{sm,md,lg}` no puede ganar desde `:root` para un cajón
izquierda/derecha —el eje cruzado sí alcanza (`height-sm` forzado: 260px →
976px)—. Misma clase que el `item-gap` de carousel y el `preview-z` de
drag-drop, que se RETIRARON; aquí no, porque el token sigue siendo la fuente
del valor que soma resuelve. Decidir si se retira o si soma lo lee es firma.
- **⚠ El eje `size` del `color-picker` MUERE EN EL PORTAL — dos veces**
(2026-08-23). El panel viaja en un portal, fuera de `[data-color-picker]`,
así que ningún privado declarado en la raíz llega hasta él. Consecuencias
medidas:
1. **`--_color-picker-content-width` se declara TRES veces y no lo consume
NADIE.** La anchura del panel la fija
`--_popover-content-width-override: calc(var(--color-picker-content-width-md) + 2rem)`,
cableada al paso `md`. Un picker `sm` y uno `lg` abren el MISMO panel de
344 px, y `content-width-sm` / `content-width-lg` son dos públicos que no
mueven nada (el `md` sí alcanza, por el override).
2. **El tamaño de las muestras dentro del panel está clavado a `md`**: la
regla del contenido redeclara `--_color-picker-swatch-size: var(--color-picker-swatch-size-md)`
sin variantes por talla. Fuera del portal los tres pasos alcanzan
(20 / 24 / 28 px → 1234 px); dentro, sólo el `md`.
El arreglo honesto es una escala por talla EN EL ÁMBITO DEL CONTENIDO (que sí
lleva su `data-size`), no en la raíz. Mueve píxel para `sm` y `lg`, así que
es firma. Misma familia que la capa `list-surface` inalcanzable desde
`:root`: **el portal es una frontera de cascada, y un privado declarado en la
raíz no la cruza.**
- **Punto ciego genérico del centinela: los tokens que SON la transición.** El
guard congela las transiciones antes de medir —el arreglo que hizo medibles
`radio-cards` y compañía— y con ello `transition-duration` / `transition-ease`
no pueden moverse jamás. Medidos sin congelar en `color-picker` alcanzan
(0.12s → 11.5s). Cualquier componente con esos dos públicos los reportará
muertos: o el guard los mide en una pasada sin congelar, o cada uno paga su
adjudicación escrita.
- **⚠⚠ §12.9 tiene MECANISMO: el plano `overlay` no «impone» tipografía donde
el componente calla — GANA donde el componente habla** (medido en `menubar`,
2026-08-23). Acuñadas `panel-font-family` y `panel-line-height` sobre una
declaración que la receta YA tenía, el centinela las dio muertas:
`[data-depth='overlay']` declara las dos con la MISMA especificidad (0,1,0) y
más tarde en la cascada. Del mismo bloque, `color` y `font-size` —que el
plano no declara— sí alcanzan. Consecuencia para la cola: **mientras §12.9 no
se firme, un token de `font-family` o `line-height` en cualquier superficie
`overlay` MIENTE**, y son veinte componentes. Retiradas del contrato de
menubar y devueltas a su fuente literal, con la razón escrita en el CSS.
- **⚠ El hover de `menubar` mata la capa del sistema — y es del eje
navigation-menu, no de éste** (medido 2026-08-23). `[data-menubar-trigger]:hover`
pinta `background: var(--color-surface-overlay)`: en hover el
`background-image` computa `none` con `--state-hover` vivo. Dos razones, las
dos ya escritas en `navigation-menu.css` cuando retiró esta MISMA regla: el
shorthand resetea `background-image`, y la regla del arquetipo va en
`:where()`, especificidad CERO. El eje navigation-menu tiene `menubar` en su
cola por esto; aquí no se acuña un `hover-trigger-bg` porque fosilizaría la
invención (doctrina: el hover es del SISTEMA).
- **⚠ Un token de `::placeholder` lee MUERTO sobre un input sin `placeholder`**
(medido en `field`, 2026-08-23). Sin el atributo no existe la caja del
pseudo-elemento, y `getComputedStyle(nd, '::placeholder')` devuelve entonces el
estilo del ELEMENTO — que no sigue al token. Poniendo un placeholder, alcanza
(`oklch(0.61 0 0)` → `rgb(1,2,3)`). Es genérico: cualquier componente con un
token de placeholder lo sufrirá si su demo arranca con el input vacío. El guard
podría sembrar un placeholder antes de esa medida.
- **⚠ DOS recetas escriben `:global()` en un CSS PLANO, y el navegador descarta
la regla entera** (medido 2026-08-23): `onion-menu.css` (`.onion-menu-icon
:global(svg)`, que cree pintar el glifo al 70 % de su caja — computa 14px, el
tamaño propio del Icon) y `timeline.css:331` (`[data-timeline-marker]
:global(svg)`). `:global()` es un envoltorio de SVELTE: fuera de un
`<style>` de componente el selector es inválido. `image-picker.css` ya
documenta la trampa en su propio comentario, así que la lección estaba escrita
y se repitió. Arreglar el selector EMPIEZA A PINTAR lo que la regla dice, así
que mueve píxel en los dos y es firma. Mientras tanto, los literales se quedan
con el defecto escrito encima y sin token (uno acuñado se retiró porque
mentía).
- **⚠ Congelar las transiciones no basta cuando el nodo está ANIMADO** (medido
en `onion-menu`, 2026-08-23). `rim-glow-opacity` leía 0,482956 —el valor que
la animación tenía en ese instante— y el token parecía muerto; con la
animación congelada también, computa 0,45 y sigue al token. El guard no puede
congelar animaciones en general (Presence necesita la suya para montar), así
que es una medida A MANO cuando el nodo pulsa.
- **⚠⚠ LA CASCADA DE PALETA ANULA LOS TONOS DEL COMPONENTE — 419 claves
públicas del catálogo** (medido 2026-08-23 en `button`, `badge` y `callout`).
El forward emite, por componente, un bloque por tono
(`[data-button][data-color='risk'] { --button-palette-solid: var(--button-risk-solid) }`)
y, **al final**, uno genérico
(`[data-button][data-color] { --button-palette-solid: var(--palette-solid, var(--button-primary-solid)) }`).
Los dos tienen especificidad (0,2,0), así que **gana el último**: cualquier
instancia con `data-color` resuelve por el `--palette-*` GLOBAL, que
`[data-color='{tono}']` llena desde `--color-{tono}-*`. Consecuencia medida
sobre `data-color='risk'`: `--button-risk-solid` no mueve nada ni en `:root`
ni en el nodo, mientras `--palette-solid` y `--color-risk-solid` en el nodo
repintan. Las siete `{c}-primary-*` son el RESPALDO de la regla genérica y
sólo actúan en una instancia SIN `data-color`.
**Tamaño**: 49 recetas emiten el forward genérico y el contrato tiene **419
claves de tono** (`{tono}-{track|element|border|solid|solid-hover|text|contrast}`),
repartidas entre ~13 componentes que además declaran reglas por tono. Son
públicas y no alcanzan: R-5.4 las llamaría mentiras, y no son deuda de nadie
en particular — es el orden de emisión.
**Arreglo propuesto (firma)**: emitir el bloque genérico ANTES de los de tono.
Entonces una instancia con tono lee `--{c}-{tono}-*` —cuyo valor por defecto
ES `var(--color-{tono}-*)`, así que **no movería un píxel**— y las escalas y
el custom siguen cayendo por el genérico. **Contrapartida**: cambia el
contrato de la jaula del color — un ancestro que inyecte `--palette-*` dejaría
de pisar un tono SEMÁNTICO en esos componentes (seguiría pisando los
personalizados). Por eso no se toca aquí.
Mientras tanto el guard lo adjudica con un PATRÓN
(`SENTINEL_PATTERN_EXCEPTIONS`), para que la razón se escriba una vez.
**Enmienda 2026-08-23 (`switch`): donde soma estampa `data-color` SIEMPRE, el
respaldo tampoco existe.** El párrafo de arriba dice que las `{c}-primary-*`
salvan a la instancia SIN `data-color` — pero hay componentes cuyo proveedor
estampa el atributo en toda instancia, porque para ellos el color es un valor
RESUELTO y `neutral` es su default, no su ausencia. En esos, el bloque
genérico casa siempre y **cae también el tono por defecto**: medido en
`switch` en estado checked, `--switch-neutral-solid` no mueve nada y
`--palette-solid` sobre el nodo repinta. Amplía el alcance de la firma: no es
«los tonos no canónicos», es **los tonos**.
- **⚠ `combobox.content-font-family` es el TERCER token de tipografía que el
plano `overlay` anula** (medido 2026-08-23). Su panel lleva
`data-depth='overlay'`, así que la clave no mueve nada, igual que las de
`menubar`, `dropdown-menu` y `context-menu` —las tres retiradas ya—. No se
toca aquí porque no es el componente en curso: cuando §12.9 se firme, o se
retira o empieza a pintar, y la decisión es la MISMA para las cuatro.
- **⚠ Ya son CUATRO los componentes canónicos sin ruta de demo**: `picker-shell`,
`mockup`, `surface` y **`audio-player`** (404 los cuatro; los dos últimos
medidos el 2026-08-23). El de audio se mide dentro de `/uix/components/media-player`,
tras el chip `media: audio`, porque es la piel de ese chasis. Lo de abajo vale
igual para él: sin ruta no hay sonda ni guard, y el guard **miente en verde-rojo** (`/uix/components/surface` = 404, medido
2026-08-23). Sin ruta no hay sonda ni guard, y el guard **miente en verde-rojo**:
reportaba 0/25 sobre una página vacía, incluido un `radius` que está vivo. El
parche es el mismo que se le puso a picker-shell —medirlo donde SÍ se renderiza
(`urls` en el override)—, pero la solución de fondo es que un componente del
canon tenga su demo.
- **⚠⚠ EL PLANO `overlay` ANULA LAS TRES VARIANTES DE `tooltip`** (medido
2026-08-23). No es sólo la tipografía de §12.9: `[data-depth='overlay']`
declara `background`, `border` y `box-shadow` con la misma especificidad que
`[data-tooltip-content]` y más tarde en la cascada, así que gana. Medido con el
panel abierto: `--tooltip-bg` / `-border` / `-shadow` no mueven nada,
`--depth-overlay-surface` / `-border` / `-shadow` repintan.
**Consecuencia visual**: `solid`, `outline` y `ghost` computan el MISMO fondo,
el MISMO borde y la MISMA sombra. `outline` es indistinguible de `solid`;
`ghost` sólo aporta un `backdrop-filter` invisible tras una superficie opaca.
La receta tiene una máquina de variantes completa (privados por variante) que
el plano neutraliza entera.
Las cinco claves afectadas quedan ADJUDICADAS, no retiradas: son la fuente de
esa máquina y borrarlas dejaría los privados inválidos sin arreglar el fondo.
La decisión es la de §12.9 —qué manda, el plano o la receta— y aquí ya no es
sólo tipografía: es la superficie.
- **El censo pierde el eje `size` DE PREMIO por aplicar bien el protocolo**
(medido 2026-08-23 en `navigation-menu`). `hasSize` se decide grepeando
`data-size` en el CSS de la receta (`theming-census.ts:287`), y el paso §7.4
del protocolo ORDENA retirar los bloques `[data-size]` del CSS cuando la
cascada la emite el TSC. Resultado: todo componente que haga lo correcto
aparece con «eje `size`: no» en su ficha y sin `y` en la tabla del censo.
Ya le pasa a `sidebar` y `nav-tree`, y a `navigation-menu` desde hoy — los
tres con su cascada por talla viva en `generated/base.css`. La columna es
informativa (no entra en el ratio), pero el conteo «con eje `size`: N» del
encabezado cuenta al revés de lo que quiere medir. El arreglo es leer el
eje del CONTRATO (una clave `-{sm,md,lg}` o un `scope: 'size:{k}'`), no del
CSS.
- **La sonda y el centinela no ven las partes que el CONSUMIDOR compone**
(medido 2026-08-23 en `navigation-menu`). Las filas de su mega-menú son `<a>`
desnudos que la receta estiliza por descendencia
(`[data-navigation-menu-content] :is(a, …)`): sin atributo del componente, el
filtro `data-{c}-*` veía 10 nodos y ninguno era el panel ni una fila — 9 de
19 knobs fuera del diff. Es la MISMA clase que `prose` y `onion-menu`, pero
por composición del consumidor, no por HTML crudo ni por hooks de clase.
Resuelto para este componente con `EXTRA_NODES` / `extraNodes` (y el paso
`open` de la sonda, que no los honraba). **Queda la pregunta general**: una
receta que estiliza nodos sin atributo propio no es medible por defecto, y
nada avisa — el gate sale verde.
- **…y la TERCERA cara: las partes de un componente COMPUESTO** (medido
2026-08-23 en `waveform`). Su cromo entero es un `Slider` embebido que la
receta re-tinta —el playhead ES el thumb del slider—, así que esos nodos
llevan `data-slider*` y el filtro `data-waveform*` medía 4 nodos sin ninguno
de ellos. Con `extraNodes`, 8. Las tres caras del mismo agujero son ya: HTML
crudo (`prose`), partes que compone el CONSUMIDOR (`navigation-menu`) y partes
de un componente COMPUESTO (`waveform`, `picker-shell`). **Lo que falta es que
el instrumento lo DETECTE solo** — hoy hay que sospecharlo y contar los nodos.
- **El censo no ve los knobs que una receta reenvía a un componente compuesto.**
`--slider-thumb-bg: var(--color-content-primary)` en `waveform.css` es un
primitivo CRUDO pintando el playhead, y no aparece en ninguna columna: las
custom properties no están en `KNOB_PROPS` (D-TH.2). El caso se arregló a
mano al tokenizar (`playhead-bg`), pero **el perímetro deja fuera una clase
entera**: toda receta que re-tinta un componente embebido (media-player,
waveform, chronos, los pickers) puede pasarle primitivos crudos sin que el
censo ni `recipe-css-contract` lo vean. Medir cuántas hay antes de decidir si
D-TH.2 se ensancha.
- **Dos propiedades que la sonda NO puede medir: la `opacity` de un `<img>` con
animación de entrada y el `inline-size` del rango *buffered* de un audio**
(medido 2026-08-23 en `image` y `audio-player`). La sonda congela `transition`
pero **nunca `animation`** —congelarla impide que Presence monte los paneles—,
así que la imagen se muestrea a mitad de su fundido y da un valor distinto en
cada corrida; el *buffered* depende de lo que el audio lleve descargado.
Probado: dos corridas sobre el MISMO código dan 1 diff en `image` y **11** en
`audio-player`. Mientras no se resuelva, **un diff en esas dos propiedades se
repite antes de creerlo**, y lo honesto sería excluirlas del snapshot para
esos dos componentes.
- **Un guard de huérfanos que no mira `generated/` ve huérfano casi todo**
(medido 2026-08-23): un script estricto de un solo uso reportó 185 claves «no
leídas» que eran los pasos de escala por talla y los tonos de paleta — los lee
el CSS GENERADO, donde el TSC emite el nombre resuelto y el forward de paleta
resuelve los tonos. El test oficial (`recipe-css-contract.test.ts`) sí los ve.
- **El escenario de la demo de `background` cambia de ANCHO entre corridas**
(medido 2026-08-23, dos veces): 870 px una corrida, 1.350 px la siguiente, con
el mismo código. Produce 2 diffs de `inlineSize` en la sonda —del contenedor,
no del componente— que un lector apresurado leería como regresión. Mientras no
se estabilice, **una corrida de sonda con diffs SÓLO de `inlineSize` sobre el
provider/capa de background se repite antes de creerla**.
- **`audio-player` no tiene README propio** (`src/uix/eidos/components/audio-player/`
tiene css, svelte, index y types y nada más). Es el cuarto canónico sin ruta de
demo y además sin documento: cuando se le escriba, le toca su «Talla y tema»
con las 8 claves que tiene desde `83a897b92`.
- **`reopen()` mira el CONTENT, y el estado vive en el TRIGGER** (medido
2026-08-23 en `emoji-picker`). Su `open-trigger-fg` oscila corrida a corrida
(17/21 · 16/21 · 16/21 con el mismo código): 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. `reopen()` ve el content, cree que está abierto y no reabre.
Probado un `openMarker` sobre el trigger: **no basta**, porque el clic de
reapertura cae dentro de esa misma ventana y sus 250 ms de espera no llegan.
El arreglo es que `reopen()` **espere a la condición** (`waitForSelector` del
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**
(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
canónico. Censo de lo medido: `gradient-picker` 36 claves de 45 (F2-A, se
retiraron), `emoji-picker` 3 (retiradas hoy) y **`form` 24** (adjudicadas, no
retiradas: son demasiadas para decidirlo sin firma). La pregunta de fondo es
la misma de §12.9 con el plano `overlay`: **qué manda, el componente compuesto
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 ~63 claves públicas que retirar en tres componentes.
- **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
`…-min-block-size-{k}` (el nombre de la PROPIEDAD). Una clave así pasó el
guard y vivió unas horas; la cazó una lectura del catálogo (24 componentes
dimensionan por `-height-{k}`, uno por `-min-block-size-{k}`) y la tabla del
propio censo, que ya mapea `min-block-size → height`. Extender R-5.3 con esa
tabla —propiedad → ranura— cierra la clase entera.
- **⚠⚠ EL GUARD TIENE FALSOS POSITIVOS: escribe el centinela en el `style` INLINE
de cada nodo** (medido 2026-08-23). `staticPass` escribe el token en
`documentElement` **y en todos los nodos del componente**, y ahí es donde vive
el canal inline de los wrappers: si el wrapper escribe un token PÚBLICO en el
`style` del nodo, el guard lo pisa, ve el cambio y lo da por VIVO — aunque un
tema, que sólo puede escribir en `:root` o en una hoja, **no lo alcance
jamás**. Es la cara opuesta de `drag-drop.preview-z`.
Censo de lo afectado: siete wrappers escriben un `--{c}-*` público inline y
**CUATRO de ellos están en el contrato** — `dialog.overlay-opacity`,
`drawer.overlay-opacity`, `avatar.badge-color-custom` y
`avatar.ring-color-custom`. Medido en `dialog` con el diálogo abierto y las
animaciones congeladas: desde `:root` el fondo del velo NO se mueve
(`color(srgb 0.1098 0.098 0.0902 / 0.2518)` idéntico), desde el inline SÍ. **Su
token miente**, y el guard lo da por bueno.
*(Corrección 2026-08-23: la primera redacción decía «dos» y ponía los de
`avatar` entre los sin-contrato — su bloque de `base.ts` es una IIFE que el
censo no sabía leer hasta `f68bac4a6`, así que sus 84 claves eran invisibles.
Re-medidos aquí los dos: desde `:root` ni el fondo del badge ni el
`box-shadow` del anillo se mueven, y no puede existir instancia alcanzable —
la puerta `data-*-custom` que activa la regla y la escritura inline del token
viajan JUNTAS en el envoltorio. Son canal de valor filtrado al contrato; su
hermana `badge-color-custom-contrast` sí vive desde `:root`.)*
Los otros cuatro (`emoji-picker.columns`, `text-focus.border-color` /
`-glow-color`, `text-gradient.duration`) no están en ningún contrato, así que
sólo son canal.
El arreglo del guard: escribir el centinela **sólo en `:root` y en el host**,
nunca en el `style` de cada nodo — pero eso rompería la medida de los paneles
PORTALADOS, que es justo por lo que se escribe en todos. La forma correcta es
escribir en todos MENOS en los nodos cuyo `style` inline ya declara ese mismo
token, y avisar cuando eso ocurra. Con re-verificación del ledger entero.
- **El guard no fotografía `translate` ni `transform`** (medido 2026-08-23 en
`background`, y ya latente en `drawer.handle-active-scale`). El paralaje entra
por `translate` sobre la capa; con `data-parallax` forzado y speed 1 el token
SIGUE al scroll (14,6 → 60,5 px al mismo `scrollY`), pero el guard no puede
verlo. Tres tokens vivos leen muertos por esta causa y una adjudicación estuvo
MAL CLASIFICADA un día («estructural, el escenario no tiene scroll timeline» —
falso: lo tiene, `animation-timeline: view()`). Va con el hueco de
`mask-image`: los tres a `PROPS` en el mismo pase, y **re-verificando los ~59
con ledger** (el barrido de `REVIEW-theming-2026-08-23.md` §5 es la base).
- **El guard no fotografía `mask-image`** (medido 2026-08-23 en `background`).
Sus dos tokens de desvanecido (`fade-size`, `fade-at`) alimentan un
`mask-image` radial y leen muertos aunque están vivos — medidos a mano, el
mask cambia (`at 50% 35%` → `at 11% 50%`). Añadir `maskImage` a la lista de
propiedades del guard es una línea, pero toca el instrumento de TODOS: entra
con su re-verificación de los componentes con ledger.
- **Montar MÁS capas puede medir MENOS** (medido 2026-08-23 en `background`).
Al enseñar al guard a encender las capas opt-in del componente, añadir el foco
puntual, la velocidad de paralaje y la profundidad hizo BAJAR la corrida de 17
a 9 tokens: los tres repintan el `background-image` y el `translate` de la
misma capa donde se miden los patrones. La regla operativa: `prepareWith` sólo
debe encender lo que NO tapa lo ya medido, y eso hay que comprobarlo corriendo
el guard antes y después de cada control que se añada.
- **Los chips de la demo de `background` se aplican con RETRASO.** Una captura
por chips guardó cada estado con el patrón ANTERIOR (el estado «mesh» tenía el
glow) y dio 0 diffs por estar desfasada IGUAL en las dos corridas — un gate
verde sobre dos medidas equivocadas. Donde la demo no coopere, la prueba buena
es la de EQUIVALENCIA (el token nuevo resuelve al mismo valor que el primitivo
que sustituyó) más forzar el atributo en el DOM, las dos deterministas.
- **Un panel de HOVER se cierra por el propio guard.** `navigation-menu` abre
con `pointerenter` y `pointerleave` programa el cierre; el guard abre con
clic y luego aparca el puntero en (0,0), que es exactamente el gesto que lo
cierra. Producía no-determinismo (dos corridas del mismo código discrepando
en un token) y **las dos caras del error a la vez**: falso negativo en los
tokens de la fila y falso positivo en `content-link-padding-inline`, que leía
«vivo» por ensanchar el panel que sí se medía. Resuelto con `openBy: 'hover'`.
La regla general: **antes de creer un «no effect», comprobar que la superficie
seguía abierta al medirlo**.
- **El estado que el guard NECESITA para medir puede TAPAR el de reposo**
(medido 2026-08-24 en `select`). Su panel es portalado, así que el guard lo
mantiene abierto para medir los ~30 tokens de dentro — y
`[data-select-trigger][data-state=open]` re-apunta el borde y la tinta del
chevron, con lo que los dos tokens de reposo del trigger leen muertos. Miden
bien con el panel CERRADO. No hay arreglo dentro de una corrida: el
instrumento sólo puede estar en un estado a la vez; se adjudica.
- **⚠ UN COMPONENTE BARRE DESCENDIENTES Y SE COME EL TOKEN DE SUS HUÉSPEDES**
(medido 2026-08-24 en `chat-composer` dentro de `file-upload`, verificado en
la supervisión). `file-upload` declara
`[data-file-upload] [data-disabled] { opacity: var(--file-upload-disabled-opacity) }`
— un selector de DESCENDIENTE, (0,2,0), el mismo peso que
`[data-chat-composer-send][data-disabled]`, y se emite más tarde. Dentro de
un FileUpload, que es donde la composición **documentada** pone al composer,
el token del composer muere.
Medido desde los dos asientos sobre el mismo nodo deshabilitado: suelto y
envuelto computa `0.4` en los dos casos —los defaults coinciden, así que el
VALOR no delata nada—, pero envuelto `--chat-composer-disabled-opacity`
desde `:root` no lo mueve (0.4) y `--file-upload-disabled-opacity` sí
(0.4 → 0.321). Es invisible hasta que un tema mueva uno de los dos, y
entonces el huésped no responde a su propia clave.
**`file-upload` es HOY el único componente del catálogo con un barrido de
descendientes así** (`grep` sobre las 162 recetas: una sola ocurrencia), o
sea que no es una clase, es una regla concreta que se puede acotar. El
arreglo —restringirla a las partes propias— mueve el píxel de cualquier
huésped deshabilitado que hoy hereda, así que es firma, no commit de
theming. Emparenta con «un componente COMPUESTO gana a la receta que lo
compone» (gradient-picker / form / emoji-picker): misma cascada, dirección
contraria.
- **⚠ LA SONDA Y EL GUARD NO SE PUEDEN LANZAR CON `--import tsx/esm`** (medido
2026-08-24 supervisando `calendar`). Cualquier script del eje que use
`page.evaluate` muere con `ReferenceError: __name is not defined` dentro del
navegador: el transform de esbuild que trae `tsx` envuelve las funciones con
su ayudante `keepNames`, y ese ayudante existe en Node, no en la página — el
cuerpo de la evaluación viaja serializado y llega sin él. Reproducido en
`__theming-probe.ts` y en `theming-sentinel.ts`.
Se lanzan con **`node` a secas** (Node 24 desnuda los tipos de forma nativa),
que es justo lo que hace `npm run theming:sentinel`. Sólo los scripts SIN
`page.evaluate` —el censo, `component-audit`, `eidos-lint`— toleran el
`--import tsx/esm`. Cuesta una sesión entera si se confunde, porque el error
no nombra ni a tsx ni a la sonda.
- **Un token VIVO sobre un nodo APLASTADO lee igual que uno muerto** (medido
2026-08-24 en `select.separator-size`). El separador vive en la columna flex
del `ScrollArea` con `flex-shrink` 1 y el panel desborda (scrollHeight 670 vs
clientHeight 310): su `block-size: 1px` computa **0px** y ningún valor del
token lo mueve. Con `flex-shrink: 0` en el mismo nodo el token sigue
(1px → 9px). Es además **un defecto de píxel**: el separador de `select` no
pinta nada en un panel que desborde. El arreglo (`flex: 0 0 auto` en
`[data-select-separator]`) mueve píxel → fuera del eje de theming, pendiente.
- **Una entrada de receta de FAMILIA no se puede juzgar en la ruta de UN
componente** (medido 2026-08-24 en `calendar`). Su entrada de
`recipes/base.ts` es la de las cuatro superficies de rejilla de fechas —
`range-calendar`, `month-grid` y `year-grid` consumen `--calendar-*` y no
acuñan nada—, pero sus partes llevan `data-month-grid-*` /
`data-range-calendar-*` y el filtro `data-calendar` no las ve: **23 de sus 29
«tokens muertos» eran eso**. Con `urls` a las tres rutas + `extraNodes` +
barrido de `variant`: 45/74 → 68/74. Vale para toda capa híbrida que conserve
entrada de receta.
- **El tamaño de letra de los selectores de mes / año del calendario es NO
DETERMINISTA** (medido 2026-08-24). `[data-calendar-month-select][data-button]`
—la regla de `calendar-select.css` que existe precisamente para fijar la letra
del Button a la del calendario— y
`[data-popover-trigger]:not([data-archetype='field-trigger'])` de `popover.css`
empatan a **(0,2,0)**, así que gana el módulo que Vite inyecte el ÚLTIMO:
cuatro cargas de la misma página dieron 14px / 16px / 14px / 14px, con el orden
de las hojas invertido en la que dio 16. Consecuencia de medición: la sonda dio
**24 diffs corriendo dos veces el MISMO código**, así que todo diff en esos dos
nodos es ruido hasta que se resuelva. Es la familia del empate por orden de
carga de §12 (tree-grid); arreglarlo mueve píxel.
- **`calendar.select-min-width` no tiene nodo en NINGUNA página del repo**
(medido 2026-08-24): su único consumidor es
`[data-range-calendar-month-select]` / `-year-select`, y la demo de
`range-calendar` renderiza un heading de texto en su lugar, mientras
`DateRangePicker` importa los selectores del CALENDARIO (que llevan
`data-calendar-month-select`). Hueco de DEMO: la parte existe en soma y en
eidos y ninguna superficie la monta.
**Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando
estorben.

Powered by TurnKey Linux.