uix(prose): temable — 1 % → 83 %, la costura por ELEMENTO y dos cegueras del instrumento

35 claves nuevas (2 → 37). Censo prose 1 % → **83 %** con `global` a CERO;
global del eje **59 % → 60 %** (1.064 → 1.021 knobs `global` en el catálogo).

Prose estiliza HTML CRUDO (`h1`, `code`, `table`, `kbd`…) a través de
`:where([data-prose] el)`, así que sus «partes» son elementos y la costura va
POR PARTE, como en `result`: `heading-*` (+ `h1-font-weight`, lo único que
distingue al h1 de sus hermanos), `link-*`, `strong-`/`dt-`/`th-font-weight`,
`mark-*`, `blockquote-*`, `code-*`, `pre-*`, `kbd-*`, `media-radius`, `hr-*`,
`table-*`, más `fg`, `fg-muted` y el eje `font-size-{sm,md,lg}`. Los valores
siguen siendo los del canon; lo que faltaba era el knob (PLAN §2-A).

LA ESCALA EDITORIAL NO ENTRA, Y ES DOCTRINA FIRMADA: los `2.25em`, `1.5em`,
`0.9em`, sus interlineados y su tracking son `em`-relativos a propósito —un
`font-size` raíz re-deriva encabezados y márgenes, por eso NO hay una escala
duplicada por talla como en tailwind-typography (su README: «No 5× duplicated
scale»)— y van anotados `/* literal: */`, la válvula de recipe-contract §3. Son
las 21 excepciones firmadas que el censo aprendió a leer esta misma sesión.

Dos literales se cambiaron por su paso de escala, con el mismo valor: el `3px`
del borde-acento del blockquote ES `--border-width-thick`, y los `1px` de kbd,
`hr` y la tabla son `--border-width`.

EL INSTRUMENTO ESTABA CIEGO DE DOS MANERAS, arregladas ANTES de medir:

1. **Sonda y centinela filtran por `data-{c}-*`** y aquí NINGUNA parte lo lleva:
   la sonda medía **1 nodo** y el guard daba **27 de 37** tokens por muertos.
   Los dos ganan un selector de nodos extra (`EXTRA_NODES` en la sonda,
   `extraNodes` en `COMPONENT_OVERRIDES`) — con él, 63 nodos y un gate que
   significa algo.
2. **El centinela no leía el color de un borde que no fuera el de arriba.** El
   blockquote tiñe su `border-inline-start` y la tabla sus bordes de bloque:
   tres tokens vivos leían muertos. Añadidos los cuatro lados lógicos y
   `borderBottomColor`.

OCHO ADJUDICACIONES, TODAS MEDIDAS: cinco las tapa el harness de la
documentación —`[data-uix-docs] code|pre` (0,2,0) gana a `:where(…)` (0,0,0),
que es la incidencia ya escrita para `code-block` un piso más arriba— y tres son
elementos que el HTML de ejemplo no usa (`<mark>`, `<dl>`), montados DENTRO del
contenedor real: un `<mark>` a pelo es exactamente lo que esta receta estiliza,
así que el nodo es fiel (la lección de `drag-drop` cumplida).

Gates: sonda antes/después **0 diffs en 11.910 valores** (63 nodos × 7 estados) ·
centinela 29/37 con las ocho adjudicadas · censo --only 83 % · `--names` 37/37
conformes · component-audit PASS · eidos-lint 0 invalid · suite eidos con el
único rojo conocido · rtl:check 0 · docs:check 0 · `check` con 72 errores
globales y 0 en los ficheros tocados. Componentes del ledger re-verificados tras
tocar el guard (text-gradient, chart, result, tree-view, textarea): sin
regresiones ni STALE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 46da0edd1a
commit 001b1cdbd0

@ -10,9 +10,9 @@
- **Medido**: 2026-08-22 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/`
- **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **59%** — 2727 de 4628 knobs pasan por un token público del componente
- **Reparto**: público 2727 · privado 338 · global 1064 · literal 499 · sistema transversal 509 · excepción firmada 54 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 17 · **alcance 100 %**: 17 · **con eje `size`**: 58
- **Alcance global**: **60%** — 2770 de 4628 knobs pasan por un token público del componente
- **Reparto**: público 2770 · privado 338 · global 1021 · literal 499 · sistema transversal 509 · excepción firmada 54 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 16 · **alcance 100 %**: 17 · **con eje `size`**: 58
## Cómo se lee
@ -77,22 +77,22 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma
| 2 | [chronos](./chronos.md) | 234 | 1% | 244 | 9 |
| 3 | [time-range-picker](./time-range-picker.md) | 67 | 20% | 88 | 22 |
| 4 | [proof-of-human](./proof-of-human.md) | 61 | 19% | 79 | 20 |
| 5 | [prose](./prose.md) | 52 | 2% | 53 | 2 |
| 6 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 |
| 7 | [color-picker](./color-picker.md) | 39 | 51% | 82 | 54 |
| 8 | [field-langs](./field-langs.md) | 37 | 0% | 39 | 0 |
| 9 | [date-range-picker](./date-range-picker.md) | 30 | 40% | 53 | 22 |
| 10 | [radio-cards](./radio-cards.md) | 28 | 60% | 72 | 39 |
| 11 | [drawer](./drawer.md) | 26 | 57% | 62 | 44 |
| 12 | [popover](./popover.md) | 26 | 46% | 51 | 36 |
| 13 | [natural-time-picker](./natural-time-picker.md) | 25 | 67% | 77 | 62 |
| 14 | [menubar](./menubar.md) | 22 | 15% | 27 | 5 |
| 15 | [nav-tree](./nav-tree.md) | 22 | 4% | 24 | 1 |
| 16 | [aura](./aura.md) | 20 | 43% | 35 | 24 |
| 17 | [float-panel](./float-panel.md) | 20 | 59% | 53 | 28 |
| 18 | [password-field](./password-field.md) | 20 | 41% | 35 | 23 |
| 19 | [navigation-menu](./navigation-menu.md) | 19 | 44% | 38 | 26 |
| 20 | [sidebar](./sidebar.md) | 19 | 55% | 45 | 50 |
| 5 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 |
| 6 | [color-picker](./color-picker.md) | 39 | 51% | 82 | 54 |
| 7 | [field-langs](./field-langs.md) | 37 | 0% | 39 | 0 |
| 8 | [date-range-picker](./date-range-picker.md) | 30 | 40% | 53 | 22 |
| 9 | [radio-cards](./radio-cards.md) | 28 | 60% | 72 | 39 |
| 10 | [drawer](./drawer.md) | 26 | 57% | 62 | 44 |
| 11 | [popover](./popover.md) | 26 | 46% | 51 | 36 |
| 12 | [natural-time-picker](./natural-time-picker.md) | 25 | 67% | 77 | 62 |
| 13 | [menubar](./menubar.md) | 22 | 15% | 27 | 5 |
| 14 | [nav-tree](./nav-tree.md) | 22 | 4% | 24 | 1 |
| 15 | [aura](./aura.md) | 20 | 43% | 35 | 24 |
| 16 | [float-panel](./float-panel.md) | 20 | 59% | 53 | 28 |
| 17 | [password-field](./password-field.md) | 20 | 41% | 35 | 23 |
| 18 | [navigation-menu](./navigation-menu.md) | 19 | 44% | 38 | 26 |
| 19 | [sidebar](./sidebar.md) | 19 | 55% | 45 | 50 |
| 20 | [field](./field.md) | 18 | 73% | 72 | 60 |
## Sin ningún token público propio (24)
@ -119,7 +119,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [motion](./motion.md) | 0% | 1 | 0 | 0 | 0 | 1 | 0 | 0 | – |
| [section](./section.md) | 0% | 1 | 0 | 0 | 0 | 1 | 0 | 4 | – |
| [chronos](./chronos.md) | 1% | 244 | 3 | 21 | 202 | 11 | 7 | 9 | y |
| [prose](./prose.md) | 2% | 53 | 1 | 0 | 43 | 9 | 0 | 2 | y |
| [nav-tree](./nav-tree.md) | 4% | 24 | 1 | 0 | 17 | 5 | 1 | 1 | – |
| [menubar](./menubar.md) | 15% | 27 | 4 | 6 | 16 | 0 | 1 | 5 | y |
| [time-picker](./time-picker.md) | 18% | 59 | 10 | 12 | 24 | 11 | 2 | 16 | y |
@ -207,6 +206,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [rating-group](./rating-group.md) | 80% | 16 | 12 | 0 | 1 | 2 | 1 | 25 | y |
| [code-block](./code-block.md) | 81% | 26 | 21 | 2 | 3 | 0 | 0 | 20 | – |
| [toggle](./toggle.md) | 81% | 28 | 22 | 1 | 1 | 3 | 1 | 119 | y |
| [prose](./prose.md) | 83% | 53 | 44 | 0 | 0 | 9 | 0 | 37 | y |
| [select](./select.md) | 83% | 72 | 54 | 5 | 3 | 3 | 7 | 101 | y |
| [calendar](./calendar.md) | 84% | 71 | 57 | 7 | 0 | 4 | 3 | 74 | – |
| [chat-message](./chat-message.md) | 84% | 84 | 65 | 0 | 6 | 6 | 7 | 75 | y |

@ -5,60 +5,16 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-22 · **Alcance**: **2%** — 1 de 53 knobs por token público
- **Knobs de apariencia**: 53 — público 1 · privado 0 · global 43 · literal 9 · sistema 0 · excepción 21 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 2 pública(s) — `measure`, `line-height`
- **Medido**: 2026-08-22 · **Alcance**: **83%** — 44 de 53 knobs por token público
- **Knobs de apariencia**: 53 — público 44 · privado 0 · global 0 · literal 9 · sistema 0 · excepción 21 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 37 pública(s) — `font-size-sm`, `font-size-md`, `font-size-lg`, `fg`, `fg-muted`, `heading-fg`, `heading-font-weight`, `h1-font-weight`, `link-fg`, `link-font-weight`, `strong-font-weight`, `dt-font-weight`, `th-font-weight`, `mark-bg`, `mark-radius`, `blockquote-border`, `blockquote-border-width`, `blockquote-fg`, `code-font-family`, `code-bg`, `code-radius`, `pre-bg`, `pre-radius`, `kbd-font-family`, `kbd-bg`, `kbd-border`, `kbd-border-width`, `kbd-radius`, `kbd-shadow`, `media-radius`, `hr-border`, `hr-border-width`, `table-header-border`, `table-cell-border`, `table-border-width`, `measure`, `line-height`
- **Eje `size`**: sí · **ficheros**: `prose.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (43)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `prose.css:24` | `[data-prose]` | `color` | `var(--color-content-primary)` |
| 2 | `prose.css:25` | `[data-prose]` | `font-size` | `var(--size-md-font-size)` |
| 3 | `prose.css:32` | `[data-prose][data-size='sm']` | `font-size` | `var(--size-sm-font-size)` |
| 4 | `prose.css:35` | `[data-prose][data-size='lg']` | `font-size` | `var(--size-lg-font-size)` |
| 5 | `prose.css:58` | `:where([data-prose] h1):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-bold)` |
| 6 | `prose.css:61` | `:where([data-prose] h1):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-primary)` |
| 7 | `prose.css:67` | `:where([data-prose] h2):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 8 | `prose.css:70` | `:where([data-prose] h2):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-primary)` |
| 9 | `prose.css:76` | `:where([data-prose] h3):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 10 | `prose.css:78` | `:where([data-prose] h3):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-primary)` |
| 11 | `prose.css:84` | `:where([data-prose] h4):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 12 | `prose.css:85` | `:where([data-prose] h4):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-primary)` |
| 13 | `prose.css:91` | `:where([data-prose] h5):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 14 | `prose.css:92` | `:where([data-prose] h5):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-primary)` |
| 15 | `prose.css:98` | `:where([data-prose] h6):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 16 | `prose.css:100` | `:where([data-prose] h6):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-muted)` |
| 17 | `prose.css:117` | `:where([data-prose] a):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-primary-text)` |
| 18 | `prose.css:118` | `:where([data-prose] a):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-medium)` |
| 19 | `prose.css:127` | `:where([data-prose] strong):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 20 | `prose.css:137` | `:where([data-prose] mark):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `background` | `var(--color-risk-track)` |
| 21 | `prose.css:140` | `:where([data-prose] mark):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-radius` | `var(--radius-sm)` |
| 22 | `prose.css:155` | `:where([data-prose] blockquote):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-inline-start` | `3px solid var(--color-border-strong)` |
| 23 | `prose.css:156` | `:where([data-prose] blockquote):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-secondary)` |
| 24 | `prose.css:170` | `:where([data-prose] ul > li)::marker` | `color` | `var(--color-content-muted)` |
| 25 | `prose.css:173` | `:where([data-prose] ol > li)::marker` | `color` | `var(--color-content-muted)` |
| 26 | `prose.css:187` | `:where([data-prose] dt):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 27 | `prose.css:200` | `:where([data-prose] code):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-family` | `var(--font-family-mono, monospace)` |
| 28 | `prose.css:203` | `:where([data-prose] code):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `background` | `var(--color-surface-muted)` |
| 29 | `prose.css:204` | `:where([data-prose] code):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-radius` | `var(--radius-sm)` |
| 30 | `prose.css:210` | `:where([data-prose] pre):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `background` | `var(--color-surface-muted)` |
| 31 | `prose.css:211` | `:where([data-prose] pre):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-radius` | `var(--radius-md)` |
| 32 | `prose.css:224` | `:where([data-prose] kbd):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-family` | `var(--font-family-mono, monospace)` |
| 33 | `prose.css:227` | `:where([data-prose] kbd):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `background` | `var(--color-surface-default)` |
| 34 | `prose.css:228` | `:where([data-prose] kbd):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border` | `1px solid var(--color-border-default)` |
| 35 | `prose.css:229` | `:where([data-prose] kbd):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-radius` | `var(--radius-sm)` |
| 36 | `prose.css:230` | `:where([data-prose] kbd):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `box-shadow` | `0 1px 0 1px var(--color-border-subtle)` |
| 37 | `prose.css:239` | `:where([data-prose] :is(img, video)):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-radius` | `var(--radius-md)` |
| 38 | `prose.css:253` | `:where([data-prose] figcaption):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `color` | `var(--color-content-muted)` |
| 39 | `prose.css:262` | `:where([data-prose] hr):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-block-start` | `1px solid var(--color-border-default)` |
| 40 | `prose.css:281` | `:where([data-prose] thead):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-block-end` | `1px solid var(--color-border-strong)` |
| 41 | `prose.css:285` | `:where([data-prose] th):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `font-weight` | `var(--font-weight-semibold)` |
| 42 | `prose.css:292` | `:where([data-prose] td):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-block-end` | `1px solid var(--color-border-subtle)` |
| 43 | `prose.css:300` | `:where([data-prose] tfoot):not(:where([data-prose-ignore], [data-prose-ignore] *))` | `border-block-start` | `1px solid var(--color-border-strong)` |
_Ninguno._
### 1.2 A través de un privado (0)
@ -122,7 +78,7 @@ _La receta no declara privados propios en su CSS._
- **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (13)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (5)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
@ -132,27 +88,15 @@ no distingue lo que debería — se marca `⚠`.
| token (`--prose-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `font-size` | `root` | ⚠ `var(--size-md-font-size)` / `2.25em` / `1.5em` / `1.25em` / `1em` / `0.9em` / `0.85em` / `0.75em` / `0.875em` / `0.8em` | 14 |
| `fg` | `root` | ⚠ `var(--color-content-primary)` / `var(--color-content-muted)` / `var(--color-primary-text)` / `var(--color-content-secondary)` | 12 |
| `font-weight` | `root` | ⚠ `var(--font-weight-bold)` / `var(--font-weight-semibold)` / `var(--font-weight-medium)` | 10 |
| `font-size` | `root` | ⚠ `2.25em` / `1.5em` / `1.25em` / `1em` / `0.9em` / `0.85em` / `0.75em` / `0.875em` / `0.8em` | 13 |
| `line-height` _(ya existe)_ | `root` | ⚠ `1.1` / `1.3` / `1.4` / `1.6` / `1.5` | 5 |
| `radius` | `root` | ⚠ `var(--radius-sm)` / `var(--radius-md)` | 5 |
| `bg` | `root` | ⚠ `var(--color-risk-track)` / `var(--color-surface-muted)` / `var(--color-surface-default)` | 4 |
| `letter-spacing` | `root` | ⚠ `-0.01em` / `-0.008em` / `0.02em` | 3 |
| `padding-inline` | `root` | ⚠ `1em` / `1.6em` | 2 |
| `font-family` | `root` | `var(--font-family-mono, monospace)` | 2 |
| `font-size-sm` | `root` | `var(--size-sm-font-size)` | 1 |
| `font-size-lg` | `root` | `var(--size-lg-font-size)` | 1 |
| `shadow` | `root` | `0 1px 0 1px var(--color-border-subtle)` | 1 |
| `width` | `root` | `max-content` | 1 |
### 4.2 Sin nombre mecánico (12)
### 4.2 Sin nombre mecánico (6)
- **⚠ decisión: `padding` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 6: `padding`.
- **⚠ decisión: `border-inline-start` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `border-inline-start`.
- **⚠ decisión: `border` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `border`.
- **⚠ decisión: `border-block-start` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 2: `border-block-start`.
- **⚠ decisión: `border-block-end` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 2: `border-block-end`.
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -166,6 +110,48 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-22 — 1 % → 83 %**, 35 claves de COSTURA. `global` a CERO.
Prose estiliza **HTML crudo** (`h1`, `code`, `table`…) a través de
`:where([data-prose] el)`, así que sus «partes» son elementos y la costura va
**por parte**, como en `result`. Los valores siguen siendo los del canon
—bundle de talla, ranuras de contenido, escalas de peso y radio, familia mono—
y ahora tienen su knob, que es lo que PLAN §2-A pide.
**Su escala editorial NO entra en el contrato, y es doctrina firmada**: los
`2.25em`, `1.5em`, `0.9em`, sus `line-height` y su tracking son
`em`-relativos a propósito —un `font-size` raíz re-deriva encabezados y
márgenes, por eso NO hay una escala duplicada por talla como en
tailwind-typography (README, «No 5× duplicated scale»)— y van anotados
`/* literal: */`, la válvula de recipe-contract §3. Son las **21 excepciones**
firmadas de §1.4; el censo las saca del ratio desde hoy.
Dos literales se cambiaron por su paso de escala, con el mismo valor: el
`3px` del borde-acento del blockquote es `--border-width-thick`, y los `1px`
de kbd, `hr` y la tabla son `--border-width`.
**El instrumento estaba ciego de dos maneras y las dos se arreglaron antes de
medir:**
1. **Sonda y centinela filtran por `data-{c}-*`**, y aquí NINGUNA parte lo
lleva: la sonda medía **1 nodo** y el guard daba 27 de 37 tokens por
muertos. Ambos ganan un selector de nodos extra
(`EXTRA_NODES` / `extraNodes`) — con él, **63 nodos** y el gate empieza a
significar algo.
2. **El centinela no leía el color de un borde que no fuera el de arriba.**
El blockquote tiñe su `border-inline-start` y la tabla sus bordes de
bloque: tres tokens vivos leían muertos. Añadidos los cuatro lados lógicos
(+ `borderBottomColor`).
**Ocho adjudicaciones, todas medidas**: cinco son el harness de la
documentación —`[data-uix-docs] code|pre` (0,2,0) gana a `:where(…)` (0,0,0),
que es la incidencia ya escrita para `code-block` un piso más arriba— y tres
son elementos que el HTML de ejemplo no usa (`<mark>`, `<dl>`), montados
DENTRO del contenedor real: un `<mark>` a pelo es exactamente lo que esta
receta estiliza, así que el nodo es fiel.
Gates: sonda **0 diffs en 11.910 valores** (63 nodos × 7 estados) · centinela
**29/37** con las ocho adjudicadas · censo 83 % · `--names` 37/37 conformes ·
audit PASS · eidos-lint 0 invalid.
<!-- veredicto:end -->

@ -7,14 +7,19 @@ y **+6 puntos al arreglar la MEDICIÓN**, no el código: ver abajo). Sin contrat
**24**; al 100 % **17**; alcance <20 % **17**. Últimos: `chart` 9 % → **89 %**
y `result` 14 % → **90 %**, los dos con `global` a CERO tras la COSTURA. El
censo cuenta 54 knobs menos: los literales ANOTADOS salen del ratio (clase
`exception`).
`exception`). Y `prose` 1 % → **83 %**.
⚠ **Sonda y centinela filtran por `data-{c}-*`**: un componente que estiliza
HTML crudo (prose) o cuyas partes no llevan atributo se mide sobre UN nodo y el
gate no significa nada. Los dos aceptan ya un selector extra
(`EXTRA_NODES` en la sonda, `extraNodes` en `COMPONENT_OVERRIDES`).
## LO PRIMERO AL ENTRAR (cinco minutos, en este orden)
1. **Comprueba que el suelo no se ha movido**:
```bash
node --import tsx/esm scripts/theming-census.ts # 162 · 5.137 · 2.727 (59 %) · 24 sin contrato
node --import tsx/esm scripts/theming-census.ts # 162 · 5.137 · 2.770 (60 %) · 24 sin contrato
node --import tsx/esm scripts/theming-census.ts --names # DESVIADAS 0 — la gramática se mantiene
```

@ -455,6 +455,35 @@ de commit. Sin artefacto, el paso no se ha hecho.
## 8. Registro
- 2026-08-22 — **`prose` 1 % → 83 %** (35 claves de costura, 2 → 37).
Global **59 % → 60 %**; `global` del catálogo 1.064 → 1.021.
Estiliza HTML CRUDO por `:where([data-prose] el)`, así que sus partes son
ELEMENTOS y la costura va por parte, como en `result`. Su escala editorial
`em`-relativa no entra: es decisión de diseño de su README («No 5×
duplicated scale») y va anotada `/* literal: */` — las 21 excepciones que el
censo aprendió a leer esta misma sesión. Dos literales se cambiaron por su
paso de escala con idéntico valor (`3px` → `--border-width-thick`, los `1px`
→ `--border-width`).
**El instrumento estaba ciego de dos maneras**, las dos arregladas antes de
medir: sonda y centinela filtran por `data-{c}-*` y aquí NINGUNA parte lo
lleva —la sonda medía **1 nodo** y el guard daba 27 de 37 por muertos—, así
que ambos ganan un selector de nodos extra (63 nodos con él); y el centinela
no leía el color de un borde que no fuera el de arriba, con lo que el
blockquote (`border-inline-start`) y la tabla (bordes de bloque) leían
muertos estando vivos.
Ocho adjudicaciones medidas: cinco las tapa el harness de la documentación
(`[data-uix-docs] code|pre` (0,2,0) gana a `:where(…)` (0,0,0) — la
incidencia ya escrita para `code-block`) y tres son elementos que el HTML de
ejemplo no usa, montados dentro del contenedor real.
Gates: sonda **0 diffs en 11.910 valores** (63 nodos × 7 estados) · centinela
29/37 · censo 83 % · `--names` 37/37 · audit PASS · lint 0 · suite con el
rojo conocido · rtl 0 · docs 0 · `check` sin errores propios. Los del ledger
re-verificados tras tocar el guard: sin regresiones.
- 2026-08-22 — **`listbox` ADJUDICADO: 68 % es su techo, y coserlo habría
mentido.** Sin cambios de código: su veredicto §5.

@ -63,6 +63,16 @@ const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'];
* text-gradient boots with `showBorder=false`, so its whole frame — three of
* its seven tokens — is absent from the default stage.
*/
/**
* Components whose PARTS are not tagged `data-{c}-*`: prose styles bare HTML
* (h1, code, table…) through `:where([data-prose] el)`, so the attribute filter
* below sees exactly ONE node and the gate proves nothing. The selector's
* matches join the measured set.
*/
const EXTRA_NODES: Record<string, string> = {
prose: '[data-prose] *'
};
const DEMO_VARIANTS: Record<string, string[]> = {
'text-gradient': [
'[data-uix-control]:has([data-uix-control-label]:text-is("showBorder")) [data-uix-chip]:text-is("true")'
@ -108,10 +118,12 @@ async function main() {
const measure = (c: string, props: string[], size: string | null) =>
page.evaluate(
([kebab, keys, forced]) => {
([kebab, keys, forced, extra]) => {
const root = document.querySelector('[data-uix-stage]') ?? document.body;
const nodes = [...root.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`))
const nodes = [...root.querySelectorAll<HTMLElement>('*')].filter(
(n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`)) ||
(extra ? n.matches(extra as string) : false)
);
if (forced)
for (const n of nodes)
@ -137,7 +149,7 @@ async function main() {
}
return rows;
},
[c, props, size] as const
[c, props, size, EXTRA_NODES[c] ?? null] as const
);
const result: Record<string, unknown> = {};
@ -193,26 +205,32 @@ async function main() {
await page.reload({ waitUntil: 'networkidle' });
await page.waitForTimeout(400);
await prepare();
const targets = await page.evaluate((kebab) => {
const targets = await page.evaluate(([kebab, extra]) => {
const root = document.querySelector('[data-uix-stage]') ?? document.body;
return [...root.querySelectorAll<HTMLElement>('*')]
.filter((n) => [...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`)))
.filter(
(n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`)) ||
(extra ? n.matches(extra) : false)
)
.map((n, i) => ({ i, tag: n.tagName.toLowerCase() }))
.filter((t) => ['button', 'a', 'input', 'li', 'div', 'tr'].includes(t.tag))
.slice(0, 24);
}, component);
}, [component, EXTRA_NODES[component] ?? null] as const);
const hovered: Record<string, Record<string, string>> = {};
for (const t of targets) {
const handle = await page.evaluateHandle(
([kebab, idx]) => {
([kebab, idx, extra]) => {
const root = document.querySelector('[data-uix-stage]') ?? document.body;
const all = [...root.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`))
const all = [...root.querySelectorAll<HTMLElement>('*')].filter(
(n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`)) ||
(extra ? n.matches(extra as string) : false)
);
return all[idx as number] ?? null;
},
[component, t.i] as const
[component, t.i, EXTRA_NODES[component] ?? null] as const
);
const el = handle.asElement();
if (!el) continue;

@ -314,6 +314,24 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'disabled-row-fg':
'no disabled row in the demo (0 mounted, counted); forced data-disabled → reaches (oklch(0.61 0 0) → rgb(1,2,3))'
},
prose: {
// Prose styles bare HTML at ZERO specificity on purpose (`:where([data-prose] el)`,
// so an embedded component keeps its own look). That is exactly what the docs
// harness beats: `[data-uix-docs] code|pre` (0,2,0) wins over (0,0,0). Removing
// the harness attribute in place 2026-08-22 -> the five reach. Same §13 entry as
// code-block's, one floor down.
'code-bg': 'the harness rule [data-uix-docs] code (0,2,0) beats :where(…) (0,0,0); without it -> reaches (oklch(0.9551 0 0) -> rgb(1,2,3))',
'code-radius': 'same harness rule; without it -> reaches (4px -> 1234px)',
'code-font-family': 'same harness rule; without it -> reaches (Azeret Mono -> Zapfino)',
'pre-bg': 'the harness rule [data-uix-docs] pre (0,2,0) beats :where(…); without it -> reaches (oklch(0.9551 0 0) -> rgb(1,2,3))',
'pre-radius': 'same harness rule; without it -> reaches (6px -> 1234px)',
// Two elements the demo's sample HTML does not use. Mounted INSIDE the real
// [data-prose] container 2026-08-22 — and a bare <mark>/<dt> IS what this
// recipe styles (it selects raw HTML), so the node is faithful.
'mark-bg': 'the sample HTML has no <mark>; mounted in place -> reaches (oklch(0.9923 0.0025 48.72) -> rgb(1,2,3))',
'mark-radius': 'same; mounted in place -> reaches (4px -> 1234px)',
'dt-font-weight': 'the sample HTML has no <dl>; mounted in place -> reaches (600 -> 123)'
},
result: {
// All five measured on the demo's REAL nodes (its own controls drive them),
// not on a node built by hand — the drag-drop lesson: a "forced -> reaches"

@ -52,6 +52,14 @@ const PROPS = [
'borderTopWidth',
'borderTopLeftRadius',
'borderInlineStartWidth',
// The colour of a border on any side but the TOP was invisible: prose tints
// its blockquote rule on the inline start and its table rules on the block
// edges, and all three read dead with a live token (measured 2026-08-22).
'borderInlineStartColor',
'borderInlineEndColor',
'borderBlockStartColor',
'borderBlockEndColor',
'borderBottomColor',
'paddingTop',
'paddingLeft',
'paddingRight',
@ -116,7 +124,14 @@ const PROPS = [
*/
const COMPONENT_OVERRIDES: Record<
string,
{ attrPrefix?: string; openWith?: string[]; openBy?: 'click' | 'hover'; urls?: string[] }
{
attrPrefix?: string;
openWith?: string[];
openBy?: 'click' | 'hover';
urls?: string[];
/** Nodes to measure that carry NO `data-{c}-*` attr (prose styles bare HTML). */
extraNodes?: string;
}
> = {
'picker-shell': {
attrPrefix: 'data-picker',
@ -141,6 +156,10 @@ const COMPONENT_OVERRIDES: Record<
// funnel, polar, heat, pie…) reads dead on it. Measured 2026-08-22: 58
// chart nodes on `/chart` against 302 in the whole page and the rest in
// sibling routes.
// Prose styles RAW HTML through `:where([data-prose] el)`, so its parts carry
// no attribute of its own: without this the guard measures ONE node (the root)
// and 27 of its 37 tokens read dead. Measured 2026-08-22.
prose: { extraNodes: '[data-prose] *' },
chart: {
// The tooltip and the crosshair only exist WHILE the pointer is over the
// plot — there is no trigger to click and no state to latch. Hovering the
@ -314,10 +333,12 @@ async function main() {
const staticPass = (key: string, value: string) =>
page.evaluate(
([kebab, token, val, props, prefix]) => {
([kebab, token, val, props, prefix, extra]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
[...document.querySelectorAll<HTMLElement>('*')].filter(
(n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix)) ||
(extra ? n.matches(extra as string) : false)
);
// Write on :root AND on every component node: resolved names are
// declared per part (root/host), and a portaled panel never sees the
@ -360,7 +381,7 @@ async function main() {
sized.forEach((n, i) => (original[i] ? n.setAttribute('data-size', original[i]!) : null));
return moved;
},
[component, key, value, PROPS, attrPrefix] as const
[component, key, value, PROPS, attrPrefix, override.extraNodes ?? null] as const
);
// Hover pass — a `hover-*` token can only move a computed value while some
@ -368,19 +389,23 @@ async function main() {
// hover-preset-border were false negatives without it).
const hoverPass = async (key: string, value: string) => {
const count = await page.evaluate(
(prefix) =>
[...document.querySelectorAll('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
([prefix, extra]) =>
[...document.querySelectorAll('*')].filter(
(n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix)) ||
(extra ? n.matches(extra) : false)
).length,
attrPrefix
[attrPrefix, override.extraNodes ?? null] as const
);
for (let i = 0; i < Math.min(count, 30); i++) {
const handle = await page.evaluateHandle(
([prefix, idx]) =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
([prefix, idx, extra]) =>
[...document.querySelectorAll<HTMLElement>('*')].filter(
(n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix)) ||
(extra ? n.matches(extra as string) : false)
)[idx as number] ?? null,
[attrPrefix, i] as const
[attrPrefix, i, override.extraNodes ?? null] as const
);
const el = handle.asElement();
if (!el) continue;
@ -390,10 +415,12 @@ async function main() {
continue;
}
const moved = await page.evaluate(
([prefix, token, val, props, idx, kebab]) => {
([prefix, token, val, props, idx, kebab, extra]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix))
[...document.querySelectorAll<HTMLElement>('*')].filter(
(n) =>
[...n.attributes].some((a) => a.name.startsWith(prefix)) ||
(extra ? n.matches(extra as string) : false)
);
const node = all()[idx as number];
if (!node) return false;
@ -409,7 +436,7 @@ async function main() {
h.style.removeProperty(`--${kebab}-${token}`);
return before !== after;
},
[attrPrefix, key, value, PROPS, i, component] as const
[attrPrefix, key, value, PROPS, i, component, override.extraNodes ?? null] as const
);
if (moved) return true;
}

@ -64,6 +64,25 @@ max-inline-size:100%; overflow-x:auto`): una tabla ancha scrollea dentro de
- **Trim de bordes a especificidad normal** (no `:where`): así trima también
un componente embebido que sea primer/último hijo (sin hueco líder/final).
## Talla y tema
Contrato en `lib/recipes/base.ts > prose` — **37 claves** (2 + 35 el
2026-08-22). Alcance del censo: **83 %** (era 1 %), con `global` a CERO.
**Guard R-5.4**: 29/37 en vivo; las ocho que callan están medidas (cinco las
tapa el harness de la documentación, tres son elementos que el HTML de ejemplo
no usa, montados a mano dentro del contenedor real).
La costura va **por parte**, porque aquí las partes son ELEMENTOS: `heading-*`
(+ `h1-font-weight`, que es lo único que distingue al h1 de sus hermanos),
`link-*`, `strong-`/`dt-`/`th-font-weight`, `mark-*`, `blockquote-*`,
`code-*`, `pre-*`, `kbd-*`, `media-radius`, `hr-*`, `table-*`, más `fg`,
`fg-muted` y el eje `font-size-{sm,md,lg}`.
**La escala editorial NO es contrato, y es la decisión de arriba**: los
`2.25em`, `0.9em`, sus interlineados y su tracking son `em`-relativos para que
un solo `font-size` raíz re-derive todo, y van anotados `/* literal: */` — la
válvula de recipe-contract §3. Son 21 excepciones firmadas, fuera del ratio.
## Passive justification
Prose declares 0 events because it is a pure typographic surface — it shapes

@ -21,18 +21,18 @@
/* ── Container ─────────────────────────────────────────────────────────── */
[data-prose] {
color: var(--color-content-primary);
font-size: var(--size-md-font-size);
color: var(--prose-fg);
font-size: var(--prose-font-size-md);
line-height: var(--prose-line-height);
max-inline-size: var(--prose-measure);
text-wrap: pretty;
}
[data-prose][data-size='sm'] {
font-size: var(--size-sm-font-size);
font-size: var(--prose-font-size-sm);
}
[data-prose][data-size='lg'] {
font-size: var(--size-lg-font-size);
font-size: var(--prose-font-size-lg);
}
/* Full-width surface (opt out of the reading measure). */
@ -55,49 +55,49 @@
:where([data-prose] h1):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 0 0.8em;
font-size: 2.25em; /* literal: em-relative editorial scale */
font-weight: var(--font-weight-bold);
font-weight: var(--prose-h1-font-weight);
line-height: 1.1; /* literal: em-relative editorial scale */
letter-spacing: -0.01em; /* literal: em-relative editorial scale */
color: var(--color-content-primary);
color: var(--prose-heading-fg);
text-wrap: balance;
}
:where([data-prose] h2):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.8em 0.9em;
font-size: 1.5em; /* literal: em-relative editorial scale */
font-weight: var(--font-weight-semibold);
font-weight: var(--prose-heading-font-weight);
line-height: 1.3; /* literal: em-relative editorial scale */
letter-spacing: -0.008em; /* literal: em-relative editorial scale */
color: var(--color-content-primary);
color: var(--prose-heading-fg);
text-wrap: balance;
}
:where([data-prose] h3):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.6em 0.6em;
font-size: 1.25em; /* literal: em-relative editorial scale */
font-weight: var(--font-weight-semibold);
font-weight: var(--prose-heading-font-weight);
line-height: 1.4; /* literal: em-relative editorial scale */
color: var(--color-content-primary);
color: var(--prose-heading-fg);
text-wrap: balance;
}
:where([data-prose] h4):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.4em 0.5em;
font-size: 1em; /* literal: em-relative editorial scale */
font-weight: var(--font-weight-semibold);
color: var(--color-content-primary);
font-weight: var(--prose-heading-font-weight);
color: var(--prose-heading-fg);
text-wrap: balance;
}
:where([data-prose] h5):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.4em 0.4em;
font-size: 0.9em; /* literal: em-relative editorial scale */
font-weight: var(--font-weight-semibold);
color: var(--color-content-primary);
font-weight: var(--prose-heading-font-weight);
color: var(--prose-heading-fg);
text-wrap: balance;
}
:where([data-prose] h6):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.4em 0.4em;
font-size: 0.85em; /* literal: em-relative editorial scale */
font-weight: var(--font-weight-semibold);
font-weight: var(--prose-heading-font-weight);
letter-spacing: 0.02em; /* literal: em-relative editorial scale */
color: var(--color-content-muted);
color: var(--prose-fg-muted);
text-wrap: balance;
}
/* Code inside a heading keeps the heading's weight/size context. */
@ -114,8 +114,8 @@
margin-block: 1.15em;
}
:where([data-prose] a):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
color: var(--color-primary-text);
font-weight: var(--font-weight-medium);
color: var(--prose-link-fg);
font-weight: var(--prose-link-font-weight);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
@ -124,7 +124,7 @@
text-decoration-thickness: 2px;
}
:where([data-prose] strong):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
font-weight: var(--font-weight-semibold);
font-weight: var(--prose-strong-font-weight);
color: inherit;
}
:where([data-prose] em):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
@ -134,10 +134,10 @@
text-decoration: line-through;
}
:where([data-prose] mark):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
background: var(--color-risk-track);
background: var(--prose-mark-bg);
color: inherit;
padding: 0.05em 0.15em;
border-radius: var(--radius-sm);
border-radius: var(--prose-mark-radius);
}
:where([data-prose] abbr):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
text-decoration: underline dotted;
@ -152,8 +152,8 @@
:where([data-prose] blockquote):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.5em;
padding-inline-start: 1em;
border-inline-start: 3px solid var(--color-border-strong);
color: var(--color-content-secondary);
border-inline-start: var(--prose-blockquote-border-width) solid var(--prose-blockquote-border);
color: var(--prose-blockquote-fg);
font-style: italic;
}
@ -167,10 +167,10 @@
margin-block: 0.4em;
}
:where([data-prose] ul > li)::marker {
color: var(--color-content-muted);
color: var(--prose-fg-muted);
}
:where([data-prose] ol > li)::marker {
color: var(--color-content-muted);
color: var(--prose-fg-muted);
font-variant-numeric: tabular-nums;
}
/* Nested lists + paragraphs in list items tighten up. */
@ -184,7 +184,7 @@
/* Definition lists. */
:where([data-prose] dt):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block-start: 1em;
font-weight: var(--font-weight-semibold);
font-weight: var(--prose-dt-font-weight);
}
:where([data-prose] dd):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block-end: 0.5em;
@ -197,18 +197,18 @@
double up on an embedded <Code> component and are a tailwind stylistic,
not editorial, choice; GitHub omits them too). */
:where([data-prose] code):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
font-family: var(--font-family-mono, monospace);
font-family: var(--prose-code-font-family);
font-size: 0.875em; /* literal: em-relative editorial scale */
padding: 0.15em 0.35em;
background: var(--color-surface-muted);
border-radius: var(--radius-sm);
background: var(--prose-code-bg);
border-radius: var(--prose-code-radius);
color: inherit;
}
:where([data-prose] pre):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.5em;
padding: 1em 1.15em;
background: var(--color-surface-muted);
border-radius: var(--radius-md);
background: var(--prose-pre-bg);
border-radius: var(--prose-pre-radius);
overflow-x: auto;
font-size: 0.875em; /* literal: em-relative editorial scale */
line-height: 1.6; /* literal: em-relative editorial scale */
@ -221,13 +221,13 @@
font-size: inherit;
}
:where([data-prose] kbd):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
font-family: var(--font-family-mono, monospace);
font-family: var(--prose-kbd-font-family);
font-size: 0.8em; /* literal: em-relative editorial scale */
padding: 0.15em 0.4em;
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
box-shadow: 0 1px 0 1px var(--color-border-subtle);
background: var(--prose-kbd-bg);
border: var(--prose-kbd-border-width) solid var(--prose-kbd-border);
border-radius: var(--prose-kbd-radius);
box-shadow: var(--prose-kbd-shadow);
}
/* ── Media ────────────────────────────────────────────────────────────── */
@ -236,7 +236,7 @@
max-inline-size: 100%;
block-size: auto;
margin-block: 1.5em;
border-radius: var(--radius-md);
border-radius: var(--prose-media-radius);
}
:where([data-prose] figure):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 1.5em;
@ -250,7 +250,7 @@
:where([data-prose] figcaption):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block-start: 0.5em;
font-size: 0.85em; /* literal: em-relative editorial scale */
color: var(--color-content-muted);
color: var(--prose-fg-muted);
text-align: center;
}
@ -259,7 +259,7 @@
:where([data-prose] hr):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
margin-block: 2.5em;
border: 0;
border-block-start: 1px solid var(--color-border-default);
border-block-start: var(--prose-hr-border-width) solid var(--prose-hr-border);
}
/* ── Table ────────────────────────────────────────────────────────────── */
@ -278,18 +278,18 @@
line-height: 1.5; /* literal: em-relative editorial scale */
}
:where([data-prose] thead):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
border-block-end: 1px solid var(--color-border-strong);
border-block-end: var(--prose-table-border-width) solid var(--prose-table-header-border);
}
:where([data-prose] th):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
padding: 0.5em 0.75em;
font-weight: var(--font-weight-semibold);
font-weight: var(--prose-th-font-weight);
text-align: start;
vertical-align: bottom;
}
:where([data-prose] td):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
padding: 0.5em 0.75em;
vertical-align: top;
border-block-end: 1px solid var(--color-border-subtle);
border-block-end: var(--prose-table-border-width) solid var(--prose-table-cell-border);
}
:where([data-prose] tbody tr:last-child td):not(
:where([data-prose-ignore], [data-prose-ignore] *)
@ -297,7 +297,7 @@
border-block-end: 0;
}
:where([data-prose] tfoot):not(:where([data-prose-ignore], [data-prose-ignore] *)) {
border-block-start: 1px solid var(--color-border-strong);
border-block-start: var(--prose-table-border-width) solid var(--prose-table-header-border);
}
/* Trim the outer cell padding so columns align to the text edge. */
:where([data-prose] :is(th, td):first-child):not(

@ -3929,6 +3929,41 @@
--skip-link-bg: var(--color-primary-solid);
--skip-link-fg: var(--color-primary-contrast);
--skip-link-shadow: var(--shadow-overlay);
--prose-font-size-sm: var(--size-sm-font-size);
--prose-font-size-md: var(--size-md-font-size);
--prose-font-size-lg: var(--size-lg-font-size);
--prose-fg: var(--color-content-primary);
--prose-fg-muted: var(--color-content-muted);
--prose-heading-fg: var(--color-content-primary);
--prose-heading-font-weight: var(--font-weight-semibold);
--prose-h1-font-weight: var(--font-weight-bold);
--prose-link-fg: var(--color-primary-text);
--prose-link-font-weight: var(--font-weight-medium);
--prose-strong-font-weight: var(--font-weight-semibold);
--prose-dt-font-weight: var(--font-weight-semibold);
--prose-th-font-weight: var(--font-weight-semibold);
--prose-mark-bg: var(--color-risk-track);
--prose-mark-radius: var(--radius-sm);
--prose-blockquote-border: var(--color-border-strong);
--prose-blockquote-border-width: var(--border-width-thick);
--prose-blockquote-fg: var(--color-content-secondary);
--prose-code-font-family: var(--font-family-mono);
--prose-code-bg: var(--color-surface-muted);
--prose-code-radius: var(--radius-sm);
--prose-pre-bg: var(--color-surface-muted);
--prose-pre-radius: var(--radius-md);
--prose-kbd-font-family: var(--font-family-mono);
--prose-kbd-bg: var(--color-surface-default);
--prose-kbd-border: var(--color-border-default);
--prose-kbd-border-width: var(--border-width);
--prose-kbd-radius: var(--radius-sm);
--prose-kbd-shadow: 0 1px 0 1px var(--color-border-subtle);
--prose-media-radius: var(--radius-md);
--prose-hr-border: var(--color-border-default);
--prose-hr-border-width: var(--border-width);
--prose-table-header-border: var(--color-border-strong);
--prose-table-cell-border: var(--color-border-subtle);
--prose-table-border-width: var(--border-width);
--prose-measure: 65ch;
--prose-line-height: 1.75;
--anchor-nav-indent: var(--space-3);

@ -6175,6 +6175,55 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// line-height are public; the em-relative element scale is recipe-internal.
// ─────────────────────────────────────────────────────────────────────
prose: {
// ── La costura (2026-08-22) ──────────────────────────────────────────
// Prose estiliza HTML CRUDO (h1, code, table…) por `:where([data-prose] el)`,
// así que sus "partes" son elementos. Los valores siguen siendo del canon
// —bundle de talla, ranuras de contenido, escalas de peso y radio— y aquí
// están sus knobs, que es lo que PLAN §2-A pide.
//
// Su ESCALA editorial (2.25em, 1.5em, 0.9em…) NO entra: es `em`-relativa a
// propósito —un `font-size` raíz re-deriva encabezados y márgenes, y por
// eso no hay una escala duplicada por talla como en tailwind-typography— y
// va anotada `/* literal: */` en el CSS, la válvula de recipe-contract §3.
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
fg: 'var(--color-content-primary)',
// La tinta apagada: h6, los marcadores de lista y el pie de figura.
'fg-muted': 'var(--color-content-muted)',
'heading-fg': 'var(--color-content-primary)',
'heading-font-weight': 'var(--font-weight-semibold)',
// h1 pesa más que sus hermanos, y es lo único que lo distingue aquí.
'h1-font-weight': 'var(--font-weight-bold)',
'link-fg': 'var(--color-primary-text)',
'link-font-weight': 'var(--font-weight-medium)',
'strong-font-weight': 'var(--font-weight-semibold)',
'dt-font-weight': 'var(--font-weight-semibold)',
'th-font-weight': 'var(--font-weight-semibold)',
'mark-bg': 'var(--color-risk-track)',
'mark-radius': 'var(--radius-sm)',
// El borde-acento del blockquote: 3px ES el paso `thick` de la escala.
'blockquote-border': 'var(--color-border-strong)',
'blockquote-border-width': 'var(--border-width-thick)',
'blockquote-fg': 'var(--color-content-secondary)',
'code-font-family': 'var(--font-family-mono)',
'code-bg': 'var(--color-surface-muted)',
'code-radius': 'var(--radius-sm)',
'pre-bg': 'var(--color-surface-muted)',
'pre-radius': 'var(--radius-md)',
'kbd-font-family': 'var(--font-family-mono)',
'kbd-bg': 'var(--color-surface-default)',
'kbd-border': 'var(--color-border-default)',
'kbd-border-width': 'var(--border-width)',
'kbd-radius': 'var(--radius-sm)',
'kbd-shadow': '0 1px 0 1px var(--color-border-subtle)',
'media-radius': 'var(--radius-md)',
'hr-border': 'var(--color-border-default)',
'hr-border-width': 'var(--border-width)',
// La tabla: la cabecera y el pie marcan más fuerte que la fila.
'table-header-border': 'var(--color-border-strong)',
'table-cell-border': 'var(--color-border-subtle)',
'table-border-width': 'var(--border-width)',
measure: '65ch',
'line-height': '1.75'
},

Loading…
Cancel
Save

Powered by TurnKey Linux.