# CONTINUE — `Affix`, la capa de colocación, y el eje que abrió: los knobs visuales
> **Kickoff**: _«Lee `docs/process/CONTINUE-affix.md` y sigue por §3.»_
> **Fecha**: 2026-08-15 (cierre de jornada) · Rama `alpha-0.1-dir-prefs`,
> **pusheada en `65536ae6c`** — limpia contra el remoto, 0 por delante.
> ⚠️ **RAMA COMPARTIDA, y hoy se notó**: otra sesión trabaja en `blocks` y su
> commit `948f7cf7e` es la punta actual. En el árbol quedan **23 ficheros suyos
> sin commitear** (`src/uix/blocks/*`, `docs/process/*blocks*`,
> `scripts/blocks-check.ts`). Stagear **por lista explícita**, nunca `git add`
> de un directorio: hoy un `git add docs/` se llevó cinco ficheros ajenos.
> **Doctrina viva**: [`architecture/morfo.md`](../architecture/morfo.md)
> §_What morfo does NOT contain_ + §Backlog `undeclaredState` ·
> [`architecture/eidos.md`](../architecture/eidos.md) ·
> [`theming/reference.md`](../theming/reference.md) §39 ·
> [`canon/vocabularies.md`](../canon/vocabularies.md) §_Placement grids (2 × 9)_.
> El relato completo del eje `Affix` está en
> [`PLAN-affix.md`](PLAN-affix.md) §6 y §7 — **este documento no lo repite**.
---
## 0 · Dónde estás
Catorce commits publicados en la jornada; **trece son de este eje** (el
catorceavo, `948f7cf7e`, es de la sesión de `blocks` — se publicó con el push
porque la rama es una sola).
| Commit | Qué |
| ------------- | ---------------------------------------------------------------------------- |
| `92c8f6466` | **`Affix`** — la pieza que `Sticky` no puede ser, y la capa que `Fab` y `MenuDial` ya habían copiado dos veces |
| `1fc0a7a96` | `Fab` deja su copia privada de la colocación y lee la capa |
| `c945be23b` | **breaking** — un eje de capa = un token público + una ranura; `--fab-offset` / `--fab-z` retirados sin shim |
| `acb7f0b41` | la capa deja de no tener guard: uno de texto y uno de valor computado |
| `4b39b718c` | el aviso al pie se lee donde se ve |
| `c53efc602` | `MenuDial` lee la colocación de la capa — la última copia privada, borrada |
| `ee41b691d` | los 20px del `MenuDial` son una decisión, no una deriva |
| `7ea913ab1` | **la rejilla lógica se canoniza** — cinco uniones a mano pasan a una; cierra EID-3 |
| `a11805d43` | la doctrina alcanza a la jornada |
| `de0615caf` | `affix` deja de declarar lo que sólo lee una capa; el gancho pasa a nombre de CAPA |
| `b72a56491` | la capa sale de `components/` — tenerla ahí acoplaba `Fab` y `MenuDial` a `Affix` |
| `998d72191` | **los 25 knobs visuales salen del contrato** — comentados, no borrados |
| `65536ae6c` | el enum que salió del morfo vuelve, leído de la unión de TypeScript (§2), y este handoff |
---
## 1 · El eje `Affix`: CERRADO
Está entero en [`PLAN-affix.md`](PLAN-affix.md) §6 (lo construido vs lo planeado)
y §7 (lo que vino después: el peldaño `--z-index-affix: 150`, el patrón de dos
tokens por eje, la canonización de la rejilla, los dos guards, y las dos
correcciones del autor). **Lo único que sigue abierto ahí**:
- **Test de geometría de la capa** — diferido al tercer consumidor. El tercero ya
existe (`Affix`, `Fab`, `MenuDial`), así que la condición se cumplió. El hueco
y por qué ninguna aserción a nivel de propiedad lo cubre está escrito en la
cabecera de [`scripts/layer-check.ts`](../../scripts/layer-check.ts):
`getComputedStyle` da el valor USADO, y un `inset: auto` se lee como píxeles
(medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo).
Discriminarlo exige **geometría** — el borde contra su bloque contenedor —, y
eso exige recorrer los ancestros buscando `transform` / `filter` / `contain` /
`will-change`.
---
## 2 · El eje que abrió: los knobs visuales fuera del contrato
No estaba previsto. Nació de la corrección de `de0615caf` (§7 del plan): `affix`
declaraba dos attrs que **lee una sola capa**. Al aplicarle la misma vara al
resto del árbol salieron **25 declaraciones más**, en seis morfos.
### La sentencia del autor, literal
> _«la doctrina prevalece, porque no tiene sentido declarar atributos que sólo se
> usan en una capa, morfo funciona como una capa declarativa entre capas, eso es
> el diseño.»_
Y el criterio de retirada:
> _«no los retires, déjalos comentados y con la explicación del porqué.»_
### Lo aplicado (`998d72191`)
25 entradas comentadas en sitio — avatar 9 · s-text 6 · image 5 · avatar-group 2
· skeleton 2 · qr-code 1 —, cada bloque citando **tres** fuentes de doctrina
(`morfo.md` §_What morfo does NOT contain_, `morfo.md` §Backlog `undeclaredState`,
`theming/reference.md` §39). Comentadas y no borradas porque son el registro de
lo que el morfo prometía **y** de los enums que un guard podría volver a leer.
De todo lo que declaraban esos seis morfos, sobrevive sólo `data-status`
(avatar ×3, image ×4): lo mueve `ImageProvider` desde `$soma/layers`, así que
**sí** cruza la frontera de capa. Ese es el criterio, y se sostiene solo.
### Lo que se perdió al comentarlos, y se ha devuelto hoy
Un `values: [...]` en el morfo le daba a `eidos-lint` con qué rechazar
`[data-size='huge']`. Al salir, esa validación se quedó sin fuente. **Se ha
devuelto leyendo la unión de TypeScript**, que es donde el enum vive de verdad.
No en `eidos-lint`: en `recipe-css-contract.test.ts`, donde el mecanismo **ya
existía** para `variant` desde antes (línea ~790, _«Variant canon enforcement»_).
Lo hecho es extenderlo, no inventarlo:
- el colector de valores CSS pasa a tomar el attr por parámetro;
- un extractor nuevo entiende `Extract` — donde viven cinco de
los ejes (`AvatarSize`, `ImageSize`, `SkeletonSize`, `QrCodeSize`,
`AvatarBadgePosition`) — y el miembro `boolean` de `AvatarRing`;
- **11 ejes** en la tabla `KNOB_AXES`, inspeccionando **44 valores CSS reales**.
Dos cosas que importan más que el conteo:
- **`qr-code.cell-shape` quedó FUERA, con motivo escrito.** Estampa
`data-cell-shape` en el `