Reescritura completa de la versión A (`bf12b8a57`), que el autor valoró como
duplicación. La medición le daba la razón: `fit()` idéntico en dos ficheros,
`hold()` y `stop()` en otros dos, y `MediaPlayerProvider.require()` en los
cuatro. Eran cuatro componentes compartiendo carpeta, no un skin.
## La forma
Un componente cablea, los aparatos sólo pintan.
- `skin-media-player.svelte` — EL punto de cableado: un `require()`, una
derivación de todo, y las teclas generadas recorriendo un mapa.
- `controls.ts` — datos por aparato: lienzo, teclas `[{id,x,y,w,h,action}]` y la
ranura del volumen.
- `actions.ts` — el vocabulario único (`play·pause·toggle·stop·seek-*·rpm·none`),
con `stop()` y el veto por `preventDefault()` en un solo sitio.
- `type-fit.svelte.ts` — el ajuste de `<text>`, una vez.
- Los cuatro cuerpos: props entran, SVG sale. Cero provider, cero `$effect`,
cero contexto. Añadir un quinto aparato es un SVG y una fila de datos.
`<SkinMediaPlayer>` deja de montar player: es una PIEZA dentro de uno. No monta
`Media`, no monta barra y no decide la forma — `inline/bar/row/card` es el eje
`variant` de `AudioPlayer`, y la piel viaja dentro como hija.
## Movimiento: el del sistema, nada a mano
La versión A traía tres `@keyframes` locales y siete transiciones con números
crudos, ignorando `docs/theming/motion.md` (leída ahora, entera).
- Todo giro continuo es el preset **`spin`**; el respirar de la cinta, **`pulse`**.
El periodo va en la ranura del propio loop (`--motion-loop-spin`: 1.333s a 45
rpm, 1.8s a 33).
- Las transiciones consumen `--duration-*` / `--ease-*` por una ranura `--_smp-*`
por eje.
- DEFECTO FUNCIONAL corregido: el generador apaga los loops por
`[data-motion='reduce']` **y** por la media query; el bloque a mano sólo tenía
la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba
el plato. Medido en navegador: ahora `animation-name: none`.
- Queda un `@keyframes` local, la aguja del vúmetro, anotado `functional:` y con
el periodo en token: ningún preset representa un instrumento analógico.
## Tres defectos que sólo se ven en movimiento
Los tres pasaban toda comprobación de atributos y sólo aparecieron mirando la
imagen moverse:
1. **La sombra rotaba con el disco.** El `feDropShadow` colgaba del grupo que
gira, así que su desplazamiento orbitaba (se leía como la sombra subiendo y
bajando) y la penumbra de la región del filtro barría las marcas del plato,
que parecían parpadear. El filtro pasa a un padre quieto — un disco es un
círculo, su silueta no cambia. Igual en el brazo.
2. **Las marcas del plato salían disparadas en arco.** Al rehacer se perdió la
regla del estrobo; sin `transform-origin` el preset gira alrededor del centro
del viewBox (500, 440) en vez del centro del plato (406, 468.5).
3. **El tamaño no llegaba.** `max-inline-size` sólo encoge, así que `lg` y `xl`
eran inertes en cuanto la columna era más estrecha (medido: md/lg/xl los tres
a 720px). Ahora `inline-size: min(100%, …)`, base por objeto y factor por
tamaño, una sola fuente en el recipe — la base estaba declarada dos veces,
inline y en CSS, y ganaba la inline.
## Volumen
Primero se compuso el `Knob` del catálogo y era inmanejable: mapea el valor al
ángulo ABSOLUTO del puntero alrededor del centro (`Gesture.rotate`), o sea
orbitar el ratón en torno a un dial de 40px. Sustituido por el `VolumeSlider`
del propio player corriendo en una guía pintada por la máquina. El re-tinte va
sobre el MISMO descendiente que retinta el player (`media-player.css:420`) o su
regla gana por estar más cerca del thumb.
La banda inferior de la pletina se reordenó para alojarlo: vúmetro a media
anchura con el dial recentrado (aguja intacta), deslizador a 42 unidades del
bisel (20px a `md`), contador y piloto +20 a la derecha, la etiqueta REPROD del
piloto fuera, y «MODELO CR-77» al mismo ancho que «Levante» por `textLength` +
`lengthAdjust="spacing"`.
## Prop `labels` y la decisión de idioma
`labels` (default `true`) apaga la serigrafía entera de la máquina. Los rótulos
NO se traducen, y es decisión firmada: son parte del OBJETO, como la marca del
badge. Los nombres accesibles sí siguen el idioma, porque salen del morfo de las
partes compuestas.
## Demo canónica
`web/routes/uix/components/skin-media-player/+page.svelte` con la plantilla v2
de 9 pestañas + su entrada en el sidebar. El banco provisional de
`/otros/players` se borra: duplicaba la demo con DOS players independientes, que
es lo que produjo dos lecturas falsas («no está sincronizado», «fondo negro»).
Los cuatro bocetos del autor se quedan en la carpeta, ya sin ruta.
`preload="none"` en la demo a propósito: un preload de metadata abre una
petición de rango que el navegador aborta, y `smoke` cuenta ese aborto como
fallo same-origin.
## Auditoría — el hueco que queda
`component-guide.md` §5 dice que incluso un átomo pasivo es morfo-first, con un
morfo mínimo `scope: ['eidos']` y `## Passive justification`. Este componente no
lo tiene porque el alcance acordado excluía `src/uix/morfo/`, y la consecuencia
no es cosmética: **tres máquinas no lo ven** — `component:audit` enumera por
ficheros de morfo y no le da fila, `eidos-lint` lo salta («no morfo file») y
`morfo:check` no visita su demo. Los guards transversales sí pasan; los
específicos de componente no se han ejecutado sobre él ni una vez.
Pendiente además: `data-perm-step` en la demo (A37), sin el cual `perm:check` la
salta en silencio.
Verificado: `npm run check` sin errores nuevos (base 72/59) · 77 tests de eidos
(recipe-css-contract · component-api-contract · component-visual-attrs · motion)
· `rtl:check` 0/179 · `docs:check` 0/0 · `layer:check` 0 · `smoke` PASS ·
`morfo:check` 159/159. Y en Chrome, con el ratón: transporte en los dos
sentidos, brazo siguiendo el surco, bobinas por conservación de área, contador
de cinta, y el volumen moviendo `audio.volume` de verdad.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>