El contrato ya estaba entero (79 claves) y NO se acuña ninguna: las tres
declaraciones que quedaban fuera lo están por doctrina, y así se firma.
Lo que entra:
- `width: 100%` de `fitted` firmado con su anotación `/* literal: */`
(recipe-contract §3): es IDENTIDAD — `fitted` ES «la lista llena el eje
inline», y un token ahí sólo podría tomar un valor sin desdecir la prop.
Sale del ratio: 96 % → 97 %.
- La propuesta §4.1 de la ficha (acuñar `indicator-width` / `-height` sobre
`var(--indicator-w, 0px)`) se RECHAZA por escrito. Esas vars son el canal
de VALOR de soma: la capa `MeasuredIndicator` escribe el rect del trigger
activo en cada movimiento, así que un público encima sería un knob que un
tema puede FIJAR — y fijarlo rompe el deslizamiento. Misma adjudicación
que `navigation-menu` lleva escrita para su propio `indicator-w` (y la
razón por la que su `-x` se retiró, muerto). El comentario de la receta lo
dice ahora en el sitio donde se lee.
- Guard R-5.4: entrada nueva en `COMPONENT_OVERRIDES` (barrido de
`data-variant` line/surface/pills/segmented + las dos superficies de URL
del propio demo, `perm.orientation=vertical` y `perm.disabled=true`):
40/79 → **70/79** sin adjudicar una clave de más.
- Ledger: 9 adjudicaciones, todas MEDIDAS a mano sobre los nodos reales —
los cuatro tokens de transición (el guard congela `transition`), los tres
del anillo de foco (el guard hace `blur` a propósito), `trigger-bg` (la
demo monta el Indicator y la regla que sube la etiqueta por encima de la
píldora gana 0,2,0; sin indicador alcanza) y `scrollable-fade`, que además
pinta por `mask-image`, fuera del juego de propiedades del guard.
- README con su sección «Talla y tema» (tabla derivada del contrato) y
pestaña `Tokens` en la demo (79 claves, verificada en el navegador).
- Ficha `docs/audit/theming/tabs.md` con su veredicto §5.
Comprobado y refutado: el trigger NO compone el `Button` canónico
(`tabs-trigger.svelte` → `Tabs.Trigger` de soma, un `<button>` desnudo), así
que la clase «el compuesto gana a la receta que lo compone» de toolbar / form
no le aplica y no hay declaración muerta que ceder.
Censo: 96 % → 97 % (77 knobs · 74 públicos · 2 globales · 0 literales ·
1 excepción firmada · 1 sistema). Sin cambios en `recipes/base.ts` ni en
`generated/base.css` — no hacía falta regenerar.
Diff de computed: **0 diffs sobre 2.016 valores en 8 estados** (8 nodos en
reposo, 7 en hover). Capturas 2× antes/después, idénticas.
Guards: centinela 70/79 verde · `component-audit --only tabs` PASS ·
`eidos-lint tabs` 0 invalid (13 morfo-backed, 44 eidos-only) ·
`vitest run src/uix/eidos` 35/36 (el rojo conocido es `skin-media-player`) ·
`rtl:check` 0 · `docs:check` 0 · `check` COMPLETED sin un solo error en
ficheros de tabs · prettier limpio sobre el contenido normalizado a LF.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
// read live. Only the focus corner has no mechanical route.
'focus-reset-radius':
'the corner only rounds under :focus-visible and the guard blurs the active element on purpose (the F2-A fix for click-focus poisoning); focused the real reset from script and confirmed :focus-visible matched -> reaches (4px -> 1234px). Same cause and same shape as rating-group.focus-radius'
},
tabs:{
// 2026-08-24. Nine, and NOT ONE of them is a token that lies: four are the
// transition the guard freezes, three the focus ring the guard blurs, one
// the composition the demo happens to mount, one a property the guard does
// not read. All nine measured by hand on the real nodes of the demo. The
// variant sweep + the two URL surfaces in COMPONENT_OVERRIDES took the run
// from 40/79 to 70/79 first, so what is left here is the residue, not the
// bulk.
'trigger-bg':
"the demo mounts `<Tabs.Indicator>`, and `[data-tabs-list]:has([data-tabs-indicator]) [data-tabs-trigger] { background: transparent }` (0,2,0) out-ranks the trigger's own `[data-tabs-trigger]` (0,1,0) — by design, the label has to read above the sliding pill. Measured 2026-08-24 with transitions frozen: with the indicator mounted the write moves nothing (rgba(0, 0, 0, 0)); switch the demo's `render Indicator part` OFF — the DEFAULT composition, the indicator is opt-in — and the same write reaches (rgba(0, 0, 0, 0) -> rgb(1, 2, 3)). Turning it off for the whole run only trades this key for `indicator-line-thickness`",
'trigger-transition-duration':
'the guard freezes transitions and this token IS the transition; measured unfrozen -> reaches (0.12s -> 11.5s on all four transitioned properties of the trigger)',
'trigger-transition-ease':
'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3) on all four)',
'indicator-transition-duration':
'same freeze, on the sliding indicator; measured unfrozen -> reaches (0.18s -> 11.5s on translate / inline-size / block-size; the fourth slot is the opacity fade, which is system)',
'indicator-transition-ease':
'same freeze; measured unfrozen -> reaches (cubic-bezier(0.32, 0.72, 0, 1) -> steps(3) on the same three)',
'trigger-focus-ring-offset':
'the ring only paints under :focus-visible and the guard blurs the active element on purpose (the F2-A fix for click-focus poisoning); walked the focus onto a real trigger with the keyboard, confirmed :focus-visible matched (1 node) -> reaches (1px -> 1234px)',
'TWO reasons, and either alone would be enough: the edge fade only exists under `[data-scrollable]`, an opt-in switch the stage boots OFF, and it paints through `mask-image`, which is not in the guard read set. Measured 2026-08-24 with the demo switch on and transitions frozen -> reaches (the mask reads `linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent)` and the write moves both stops to 1234px)'
so it re-measures on an RTL flip, which a wrapper with no access to the
resolved direction could not do.
## Talla y tema
**79 claves públicas** en `lib/recipes/base.ts`, y la pestaña **Tokens** de su
demo las resuelve en vivo sobre el escenario. El reparto, por dónde pintan:
| grupo | claves | notas |
| --- | ---: | --- |
| Pila del provider | `stack-gap`, `line-stack-gap` | el hueco lista↔panel; `line` lo aprieta |
| Marco `surface` | `provider-{radius,border-width,border,bg}`, `surface-provider-{padding,shadow}` | sólo bajo `variant='surface'` |
| Lista | `list-{gap,padding,radius,width,bg,border-width,border}`, `line-list-padding-inline-end`, `surface-list-bg`, `pills-list-bg`, `segmented-list-{bg,radius}` | `list-width` es el raíl VERTICAL |
| Trigger, por talla | `trigger-height-{xs,sm,md,lg}`, `trigger-padding-inline-{…}`, `trigger-gap-{…}`, `trigger-font-size-{…}` | cada uno al bundle `--size-{k}-*` |
| Foco y estado | `trigger-focus-ring-{offset,width,color}`, `disabled-opacity` | el trío de foco ESPEJA la familia del sistema — exento de la gramática de tinta |
width:100%;/* literal: identity — `fitted` IS "the list fills the inline axis"; a token here would be a knob that can only hold one value without unsaying the prop */