/* ============================================================================
   CUSTODE · 06-motion — LA CAPA DE MOVIMIENTO DEFINITIVA
   ----------------------------------------------------------------------------
   Capa ADITIVA. Se carga DESPUES de 05-pages.css. No toca ni un archivo del
   child theme, no toca layout, no toca color de marca, no toca tipografia.

   PROCEDENCIA. Esta capa es la sintesis de tres direcciones que compitieron y
   de tres jueces que las midieron en navegador. La columna vertebral es
   "CONTENCION ABSOLUTA" (restraint), ganadora por rendimiento y por
   accesibilidad. Sobre ella se injertan las piezas que los jueces senalaron
   por nombre. Cada injerto lleva marcado su origen y la CONDICION MEDIDA con
   la que se acepto:

   AUDITORIA DE RESOLUCION (posterior). Tres bloques de este archivo —§5.1,
   §5.3 y §6— se habian calibrado contra "375 fuentes de 979x734 y un master
   remoto capado en 1280x960". Las dos cifras eran falsas: se habia medido
   data/images/, que son las MINIATURAS. El archivo real es data/images-extra/
   (390 originales, mediana 1320 px, maximo 4160x3120). §6 dice que se corrigio
   y a costa de que; §2.2 explica por que el hero NO cambio.

     [restraint] cabecera con histeresis · indice con aria-current · filete que
                 se traza en ocho contextos · la presion (scaleY 2) · foco
                 instantaneo · paridad teclado/raton · retirada en movil
     [cinema]    grano estatico sobre lamina — SIN mix-blend-mode (§5.1) ·
                 asentamiento de escala del hero — RESOLVIENDO A 1.000 (§2.2) ·
                 la disciplina @supports (animation-timeline: view()) (§9) ·
                 el velo de barrio que se abre en hover (§6)
     [document]  la cifra que cuenta (§8) · el vigilante que no depende de que
                 nadie haga scroll (motion.js) · @media print (§12) · matar
                 bajo reduced-motion por `animation: none`, no por duracion

   LO QUE SE DEJO FUERA, Y POR QUE (todo medido, nada de gusto):
     · El velo marfil al 7% en reposo sobre las laminas. Los TRES jueces lo
       atacaron: aclara la fotografia +7,12 niveles de luminancia sobre el 81%
       de los pixeles, es decir degrada el activo principal el 95% del tiempo
       para poder descubrirlo el 5% restante. Lo sustituye el grano (§5.1),
       que anade materia sin restar luz.
     · Los cuatro bucles `infinite` del hero (deriva, respiracion x2, grano
       animado). Medido por el juez de rendimiento: +245,5 ms de hilo principal
       y 7,8-11,7% de un nucleo QUEMANDOSE EN REPOSO mientras el visitante solo
       lee. Y la amplitud era humanamente indetectable (5,01 px/s de deriva,
       5% de opacidad en 46 s). Coste enorme, lectura nula.
     · `mix-blend-mode: overlay` sobre el grano. Es exactamente lo que sacaba
       las animaciones de cinema del compositor (A/B medido: matar los 4 bucles
       con blend hizo caer 252 recalculos de estilo a CERO). Aqui el grano es
       estatico Y sin blend: coste por fotograma cero por partida doble.
     · La manga de papel de "document". Medida estirando la duracion a 6000 ms:
       recorre el 92% del viaje en el 27% del tiempo. A 700 ms reales es un
       chasquido de ~190 ms. Y era lo que obligaba a esa direccion a poner una
       compuerta de opacidad sobre contenido, que es lo que la descalifico.

   TRES REGLAS DE CONSTRUCCION, AUDITABLES CON UN grep:
   1. NINGUNA declaracion `opacity: 0` sobre CONTENIDO en todo el archivo. Las
      unicas de la capa estan sobre velos y pseudoelementos decorativos, cuyo
      estado DECLARADO es su estado FINAL. El contenido nace visible y no
      depende de JS, de un observador, de un temporizador ni de que el
      navegador soporte scroll-driven animations.
   2. Solo se animan `transform`, `translate`, `scale`, `opacity` y `filter`.
      Dos excepciones documentadas y MEDIDAS en §7 y §8.
   3. Toda duracion y toda curva salen de 00-tokens.css. Los seis tokens
      propios de esta capa se declaran en §0 y cada uno dice por que existe.
      De los seis, UNO SOLO es una duracion nueva, y §0 la justifica.
   ========================================================================== */


/* ── §0 · TOKENS PROPIOS DE LA CAPA ────────────────────────────────────────
   Tres amplitudes, un suelo, una cadencia, UNA duracion y el tile del grano.
   Todo lo demas sale de 00-tokens.css. Se declaran juntos para que se auditen
   de un vistazo, y el unico que hay que defender de verdad —la duracion— lleva
   su defensa pegada. */
:root {

  /* Escala de arranque del hero. El asentamiento va de aqui a 1.000.
     NO se queda en 1.04 como hacia cinema: la direccion de la que viene el
     gesto dejaba la foto permanentemente ampliada para reservarse holgura
     para una deriva que aqui no existe, y eso costaba un 4% de nitidez
     PERMANENTE en la unica foto de 2560 px del sitio (muestreo efectivo
     0,855 frente a 0,889 @DPR2). Al no haber deriva no hace falta holgura,
     asi que el gesto RESUELVE HACIA LA NITIDEZ MAXIMA y el estado de reposo
     es el fotograma mas definido que la fuente permite. Ver §2.2.

     ESTE NUMERO SOBREVIVIO A LA AUDITORIA DE RESOLUCION. La revision que
     corrigio §5.1, §5.3 y §6 —donde se habia tomado la carpeta de miniaturas
     por el archivo fotografico— no toco este token, y hay que decir por que:
     sus cifras nunca salieron del inventario. Salen del propio
     hero-punta-roca.jpg, que se ha vuelto a medir y sigue siendo 2560x1403.
     El razonamiento estaba bien apoyado desde el principio. */
  --c-m-strike:        1.045;

  /* Duracion del asentamiento del hero. UNICA duracion nueva del sistema y
     por eso la unica que hay que justificar. La escala mas larga de
     00-tokens.css es --c-dur-enter (900 ms) y esta calibrada para ENTRADAS
     DE BLOQUE: a 900 ms un cambio de escala del 4,5% se lee como un zoom de
     interfaz. Por debajo de ~1,4 s el gesto es un zoom; por encima de ~2,2 s
     el visitante ya scrolleo y se lo pierde. 1800 ms es el centro de esa
     ventana. Se apaga entero bajo reduced-motion (§13). */
  --c-m-dur-strike:    1800ms;

  /* Cuanto se queda atras la lamina del hero al salir del encuadre.
     La foto viaja al 93% de la velocidad de la pagina sobre ~684 px de
     recorrido. NO consume presupuesto de recorte: la lamina se estira hacia
     arriba exactamente lo que va a bajar (§9.1). */
  --c-m-lag:           48px;

  /* Travelling lateral de la lamina de barrio.
     El presupuesto ya NO se calcula: se GARANTIZA. §9.3 ensancha la imagen
     exactamente 2x este valor y la desplaza medio ensanchamiento hacia la
     izquierda, igual que §9.1 estira la lamina del hero justo lo que va a
     bajar. Antes el margen se deducia de la relacion de aspecto —la lamina es
     3:4 y la fuente era 4:3, luego sobraban +/-117 px— y esa deduccion se cayo
     en cuanto la portada empezo a servir originales: el original de la lamina
     de Portal Genoves es 1200x1600, es decir 3:4 EXACTO, la misma proporcion
     que la caja, y `cover` no descarta ni un pixel. Con la formula vieja ese
     azulejo habria descubierto papel en el primer fotograma del travelling.
     Ver §9.3. */
  --c-m-truck:         14px;

  /* Suelo del contador (§8). El 0 absoluto deja la pagina diciendo
     "0 inmuebles en custodia" durante ~900 ms, que para una marca que vende
     respaldo es un transitorio que trabaja en contra. Arrancar en el 40% del
     valor conserva el gesto y borra el problema. Lo lee motion.js. */
  --c-m-count-floor:   .40;

  /* Cadencia de escritura del contador, en ms. ~22 fps. A 60 fps es una
     tragaperras; a 22 suena a prensa de sellar, y ademas 4 contadores cuestan
     ~80 escrituras de texto en vez de 216. Lo lee motion.js. */
  --c-m-count-step:    45;

  /* GRANO — turbulencia fractal binarizada, 528 B, sin peticion de red.

     LA `feComponentTransfer type='discrete'` NO ES ADORNO: ES LA PIEZA QUE
     HACE QUE EL GRANO EXISTA, y llegar a ella costo una tarde de medicion que
     merece quedar escrita porque el error es facil de repetir.

     El tile que arrastraban las direcciones anteriores era feTurbulence +
     feColorMatrix saturate 0, sin mas. Medido dibujandolo en un <canvas> de
     160x160 y sacando media y desviacion tipica de verdad:
         tile SIN transfer .......... media 186,7 · sigma 15,3
         tile CON discrete 0/1 ...... media 127,5 · sigma 127,5
     Es decir: el tile de siempre no es ruido neutro, es un gris CLARO con
     casi nada de varianza. A opacidad .03 sobre fotografia eso no anade
     textura: anade un velo. Medido sobre la lamina de una ficha a DPR2,
     recorte de 742x494 px, contra la misma lamina sin grano:
         tile viejo, sin blend ...... media +0,74 niveles · RMSE 1,25
         tile viejo, overlay ........ media +0,53 niveles · RMSE 0,73
         TILE BINARIO, sin blend .... media -0,12 niveles · RMSE 2,18
     El tile binario es el unico que anade textura de alta frecuencia SIN
     mover la media: la fotografia recibe materia y no pierde luz. Y esa es
     exactamente la diferencia con el velo marfil del 7% que se descarto, que
     costaba +7,12 niveles de media — sesenta veces mas desplazamiento de luz
     por menos textura.

     PARA PROPAGAR A DESIGN-SYSTEM.md §5.4: `--c-grain-opacity: .06` esta
     calibrado para un tile de ruido NEUTRO Y DE ALTA VARIANZA. Con el tile
     que se venia usando, ese 6% no se ve en pantalla. El numero no estaba
     mal: le faltaba decir contra que tile. */
  --c-m-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='discrete' tableValues='0 1'/%3E%3CfeFuncG type='discrete' tableValues='0 1'/%3E%3CfeFuncB type='discrete' tableValues='0 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
}


/* ── §1 · RED DE SEGURIDAD: el contenido deja de depender del JS ───────────
   01-base.css §13 y 05-pages.css declaran los dos `.js .c-reveal{opacity:0}`
   y confian en que un IntersectionObserver —o un setTimeout de 1200 ms—
   devuelva la opacidad. Esta capa no revela nada por scroll: su asentamiento
   (§9.4) es `translate` puro y no toca opacidad, asi que puede RETIRAR esa
   dependencia entera. A partir de aqui el contenido es visible por CSS, pase
   lo que pase con el JavaScript. El script en linea del home sigue anadiendo
   `.is-in` sobre elementos ya visibles: queda como no-operacion inofensiva.

   Dos reglas separadas y no una lista de selectores A PROPOSITO: 01-base.css
   documenta que Chrome tomaba la especificidad del selector mas alto de la
   lista y la regla de reposo dejaba de aplicarse. Con reglas sueltas no hay
   ambiguedad posible: 0,2,0 declarado despues gana a 0,2,0 declarado antes. */
.js .c-reveal        { opacity: 1; transform: none; transition: none; }
.js .c-reveal.is-in  { opacity: 1; transform: none; transition: none; }


/* ── §2 · LA APERTURA DEL FOLD — dos acontecimientos, una sola vez ─────────
   El fold es lo unico de lo que el cliente se quejo, y es donde las dos
   direcciones perdedoras no hacian nada (0,00% y 0,24% de pixeles distintos
   contra el home base, medido por el juez de gusto). Esta capa si lo toca,
   pero con los dos unicos gestos que no pueden esconder nada por
   construccion: uno vive en un pseudoelemento y el otro solo escala.

   Ninguno de los dos es un bucle. Ocurren al cargar, terminan, y no vuelven.
   El compositor queda en reposo absoluto: 0% de CPU con la pagina quieta. */

/* 2.1 · SUBEN LAS LUCES  [restraint]
   SEGURIDAD: `.c-hero::after` se DECLARA en opacity 0, que es su estado
   final. El velo solo existe dentro del @keyframes. Sin animaciones, sin JS
   o bajo reduced-motion, el resultado es exactamente la foto limpia. */
@keyframes c-m-lights-up { from { opacity: .24; } to { opacity: 0; } }

.c-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--c-z-scrim);         /* sobre el scrim, bajo el titular */
  background-color: var(--c-ink);
  opacity: 0;                        /* ESTADO DECLARADO = ESTADO FINAL */
  pointer-events: none;
  animation: c-m-lights-up var(--c-dur-enter) var(--c-ease-plate) both;
}

/* 2.2 · LA CAMARA SE POSA  [cinema, corregido]
   Unico gesto de la capa que amplia, y va en el sentido correcto: de blando a
   nitido. Termina en scale 1.000 — el fotograma mas definido que da la fuente
   de 2560x1403. No puede revelar pixelado porque el estado de reposo es el
   estado sin ampliar; lo unico blando es el transitorio, y dura 1,8 s.

   PRESUPUESTO MEDIDO (s = max(cajaW/fuenteW, cajaH/fuenteH)):
     caja 1440x761,4 · fuente 2560x1403 · s = 0,5625
     reposo   scale 1.000 -> 0,889 px de fuente por px de pantalla @DPR2
     arranque scale 1.045 -> 0,851  (4,4% mas blando, 1,8 s, resolviendo)

   POR QUE NO HAY KEN BURNS CONTINUO, Y QUE HARIA FALTA PARA QUE LO HUBIERA.
   La pregunta se ha vuelto a hacer con la resolucion real encima de la mesa,
   porque la respuesta anterior podia estar contaminada por la premisa falsa
   de §5.1. No lo estaba —los numeros de este gesto salen del propio
   hero-punta-roca.jpg, no del inventario— pero la comprobacion cambio el
   argumento de sitio, y a mejor.

   El dato duro es ese 0,889: el hero YA esta submuestreado en reposo, a
   scale 1.000, en una pantalla @DPR2. No hay presupuesto que gastar. Una
   deriva continua no seria "un poco de zoom": seria dejar la unica fotografia
   a pantalla completa del sitio permanentemente por debajo de un reposo que
   ya es deficitario. Medido en navegador con la foto real (energia de alta
   frecuencia sobre el mismo recorte, @DPR2), contra un corte del ORIGINAL de
   4160 px que enmarca lo mismo:

     hero 2560 @1.000 ... 81-86% del detalle que el original pondria ahi
     hero 2560 @1.090 ... 80-86%, y ademas -3% a -7% respecto a su propio reposo
     hires 4160 @1.090 ... +13% a +16% SOBRE el hero actual QUIETO

   Esa ultima linea es la conclusion entera: un Ken Burns cortado del original
   seria mas nitido en su fotograma MAS AMPLIADO que el hero de hoy parado.
   El obstaculo no es la fisica de ampliar, es que el JPEG del hero se corto a
   2560 px cuando el original da 4160.

   CONDICION EXACTA PARA REABRIR ESTO — medida, no estimada. El hero es
   `crop 4160x2281+0+559` de data/images-extra/10014220/10014220-01.jpg
   (4160x3120), reducido despues a 2560. Cortandolo otra vez SIN reducir:
     caja 1440x761,4 · fuente 4160x2281 · s = 0,3462 -> 1,444 en reposo
   Con 1,444 de muestreo hay sitio para una deriva de hasta 1,44 sin bajar de
   1,0, y entonces el Ken Burns lento se defiende solo. Ese archivo, sin
   embargo, vive en wp-content/themes/custode/assets/img/, que esta capa NO
   posee. Queda escrito con su geometria exacta para que sea una tarea de diez
   minutos y no una investigacion. Hasta entonces, el asentamiento unico no es
   prudencia: es la unica opcion que resuelve HACIA la nitidez. */
@keyframes c-m-strike { from { scale: var(--c-m-strike); } to { scale: 1; } }

.c-hero__plate img {
  animation: c-m-strike var(--c-m-dur-strike) var(--c-ease-plate) both;
}

/* 2.3 · EL TITULAR SE ASIENTA — `translate`, NUNCA opacidad.
   La direccion de la que viene este gesto lo hacia con opacity 0 -> 1 y
   `fill: both`, lo que deja el titular invisible durante los primeros 160 ms.
   Todos los jueces lo dieron por bueno; aun asi aqui se hace con `translate`,
   porque asi el archivo entero puede afirmar CERO opacidad sobre contenido y
   eso es una garantia auditable en vez de una ventana corta.

   Usa `translate` y no `transform` a proposito: el contramovimiento de salida
   (§9.2) usa `transform`, y son propiedades distintas que se COMPONEN. Asi el
   gesto de carga y el de scroll no se pisan sin necesitar
   `animation-composition: add`. */
@keyframes c-m-settle-y {
  from { translate: 0 var(--c-enter-y); }
  to   { translate: 0 0; }
}
.c-hero__inner {
  animation: c-m-settle-y var(--c-dur-enter) calc(var(--c-stagger) * 2)
             var(--c-ease-plate) both;
}


/* ── §3 · CABECERA: el negro reservado como encuadernacion  [restraint] ────
   BRAND-DIRECTION: "El negro no desaparece: se gana. Cuando aparece,
   significa algo." La cabecera solida es TINTA, no marfil. Tres consecuencias:
     a) el texto de la nav NUNCA cambia de color -> cero repintado de texto,
        la transicion es una unica opacidad compuesta en GPU;
     b) la pagina queda encuadernada en tinta arriba y abajo (footer);
     c) el marfil sigue siendo el fondo del producto, sin competencia.

   RED DE SEGURIDAD DEL ESTADO: el defecto de la cabecera fija es SOLIDA
   (legible sobre cualquier fondo). El JS solo RETIRA la solidez mientras
   estamos arriba. Si el JS muere a mitad de sesion la cabecera se queda
   solida y legible — nunca texto marfil sobre marfil. Y si no corre nunca,
   no existe `.js-motion` y la cabecera se queda absoluta como en la base.

   POR QUE HAY UNA CLASE `.is-armed`: motion.js va al final del <body> (no en
   el <head>), asi que existe una ventana en la que el navegador podria pintar
   la cabecera solida antes de que el script fije el estado inicial. Las
   transiciones se arman UN FOTOGRAMA DESPUES de fijarlo, de modo que esa
   correccion —si llega a ocurrir— es instantanea e invisible en vez de un
   fundido de 700 ms. Medido: CLS 0,00. */
.js-motion .c-nav       { position: fixed; isolation: isolate; }

.js-motion .c-nav::before {              /* la lamina de tinta */
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--c-ink);
  opacity: 1;                            /* DEFECTO = SOLIDA = LEGIBLE */
}
.js-motion.is-top .c-nav::before { opacity: 0; }

/* El unico filete ceremonial de la pagina: se traza a lo ancho de la cabecera
   cuando esta se asienta y se retira POR EL LADO CONTRARIO al volver arriba.
   El volteo de transform-origin no es invento nuestro: MOTION-FORENSICS lo
   midio en `nav .navigation__link::before` de la propia referencia. */
.js-motion .c-nav::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: var(--c-hair-w);
  background-color: var(--c-brass);
  transform: scaleX(1);
  transform-origin: 0% 50%;
}
.js-motion.is-top .c-nav::after { transform: scaleX(0); transform-origin: 100% 50%; }

.js-motion.is-armed .c-nav::before { transition: opacity  var(--c-dur-plate) var(--c-ease-plate); }
.js-motion.is-armed .c-nav::after  { transition: transform var(--c-dur-rule)  var(--c-ease-rule); }

/* Anclas internas: la cabecera fija no puede tapar el destino de un salto.
   Usabilidad, no movimiento. (01-base.css ya anula scroll-behavior:smooth
   bajo reduced-motion — la referencia falla justo eso; nosotros no.) */
.js-motion :target,
.js-motion #contenido,
.js-motion #inventario,
.js-motion #territorio,
.js-motion #protocolo {
  scroll-margin-block-start: calc(var(--c-nav-h) + var(--c-sp-6));
}

/* Foco de teclado sobre la lamina de tinta: el anillo por defecto es
   --c-brass-hair (6,03:1 sobre papel) y sobre tinta se pierde. */
.js-motion .c-nav :focus-visible { outline-color: var(--c-focus-color-inv); }


/* ── §4 · ENLACES DE NAV: el filete, con volteo de origen  [restraint] ─────
   Sustituye la transicion de `border-color` de la base (propiedad de pintado)
   por un scaleX compuesto. Especificidad 0,3,1 > 0,2,1 de origen: gana sin
   !important. */
.js-motion .c-nav__links a {
  position: relative;
  border-block-end-color: transparent;
  transition: none;
}
.js-motion .c-nav__links a::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: calc(var(--c-hair-w) * -1);
  block-size: var(--c-hair-w);
  background-color: var(--c-brass);
  transform: scaleX(0);
  transform-origin: 100% 50%;
  transition: transform var(--c-dur-hover) var(--c-ease-rule);
}
.js-motion .c-nav__links a:hover,
.js-motion .c-nav__links a:focus-visible { border-block-end-color: transparent; }
.js-motion .c-nav__links a:hover::after,
.js-motion .c-nav__links a:focus-visible::after {
  transform: scaleX(1);
  transform-origin: 0% 50%;
}

/* §4b · EL INDICE — el unico estado, ademas de la solidez de la cabecera, que
   cambia el scroll. NO es decoracion: es wayfinding, y en un documento de
   3.900 px el lector tiene derecho a saber en que capitulo va.

   Se declara sobre `aria-current="location"`, que escribe el JS. No hay una
   clase visual con una etiqueta ARIA pegada al lado: el atributo semantico ES
   el selector, asi que la marca que ve el ojo y la que oye un lector de
   pantalla no pueden divergir nunca. --c-brass-hi da 9,06:1 sobre tinta. */
.js-motion .c-nav__links a[aria-current] {
  color: var(--c-brass-hi);
  transition: color var(--c-dur-tick) var(--c-ease-ink);
}
.js-motion .c-nav__links a[aria-current]::after {
  transform: scaleX(1);
  transform-origin: 0% 50%;
}


/* ── §5 · LA LAMINA: materia en reposo, filete al mirarla ──────────────────

   5.1 · GRANO ESTATICO  [cinema, sin blend]
   El injerto que los tres jueces pidieron por nombre. El grano enmascara el
   bloqueo JPEG y la distorsion de gran angular de unas tomas de agente
   inmobiliario. No es decoracion: es reparacion, y su coste por fotograma es
   CERO porque no se mueve.

   CORRECCION DE HECHOS (auditoria de resolucion). Este parrafo decia que "las
   375 fuentes del inventario son 979x734 y el master remoto esta capado en
   1280x960". Las dos afirmaciones eran falsas, y la segunda ademas cerraba la
   puerta a comprobarlo. Lo que se habia medido era data/images/, que son las
   MINIATURAS —y si, esas 375 son 979x734 exactas, sin una excepcion—. El
   archivo fotografico vive en data/images-extra/: 390 originales, mediana
   1320 px, maximo 4160x3120, y 176 de ellos por encima de 2000 px. En
   WordPress ya hay 336 adjuntos importados con mediana 1320 y maximo 2560.

   El grano SE QUEDA, y ahora por un motivo mas estrecho y mas honesto: la
   mediana real del inventario es 1320 px, no 4160. La mitad larga del
   catalogo son fotos de telefono de 1280x960 con compresion agresiva, y sobre
   esas el grano sigue haciendo exactamente lo que se midio. Lo que ya no se
   puede decir es que el archivo entero sea blando.

   DOS CONDICIONES MEDIDAS, las dos innegociables:
     · ESTATICO. El grano animado de cinema (10 pasos, 9,1 fps) costo
       +245,5 ms de hilo principal y mantenia el compositor despierto.
     · SIN `mix-blend-mode`. El blend es exactamente lo que sacaba aquellas
       animaciones del compositor (A/B: matarlas hizo caer 252 recalculos de
       estilo a CERO) y ademas anadia una capa compuesta por lamina — 10 en
       el home, 330 MB de textura @DPR3 en el peor caso de aquella direccion.
       Sin blend el grano es un unico rectangulo pintado, sin contexto de
       apilado y sin sorpresas en Safari movil, que es donde el propio
       cinema admitia no haber probado.

   COSTE OPTICO MEDIDO (lamina de ficha, recorte 742x494 a DPR2, contra la
   misma lamina con la regla desactivada):
       media ....... 137,794 -> 137,677  =  -0,12 niveles de luminancia
       sigma ....... 55,783  -> 54,993
       RMSE ........ 2,18 niveles/255 de textura de alta frecuencia anadida
   El grano NO aclara la fotografia: la media se mueve una decima de nivel.
   El velo marfil del 7% que esto sustituye la aclaraba +7,12 niveles. Ese es
   el argumento entero, y es un numero, no una opinion. Ver la justificacion
   del tile binario en §0 y las capturas en docs/MOTION.md §3. */
/* NO cuelga de `.js-motion`, y es la unica regla de la capa que no lo hace:
   el grano no puede ocultar nada, no depende de ningun estado y es la pieza
   que repara la fotografia. Un visitante con JavaScript apagado tiene el
   mismo derecho a que el inventario no se vea plastico. */
.c-plate::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--c-z-scrim);       /* sobre la foto, bajo el filete interior */
  pointer-events: none;
  background-image: var(--c-m-grain);
  background-size: 160px 160px;
  opacity: calc(var(--c-grain-opacity) / 2);   /* .03 */
}

/* 5.2 · EL FILETE DE LA LAMINA MONTADA  [restraint]
   Una linea de laton bajo la foto, dentro del passe-partout: el filete que
   lleva una copia montada en carton. Se traza con scaleX — transform puro,
   sin reflow y sin tocar un pixel de fotografia. */
.js-motion .c-listing__plate { position: relative; }
.js-motion .c-listing__plate::after {
  content: "";
  position: absolute;
  inset-inline: var(--c-plate-mat);
  inset-block-end: calc(var(--c-plate-mat) * .5);
  block-size: var(--c-hair-w);
  background-color: var(--c-brass);
  transform: scaleX(0);
  transform-origin: 0% 50%;
  transition: transform var(--c-dur-hover) var(--c-ease-rule);
}
.js-motion .c-listing:hover  .c-listing__plate::after,
.js-motion .c-listing:focus-within .c-listing__plate::after { transform: scaleX(1); }

/* 5.3 · EL ZOOM DE LA FICHA SE CONSERVA EN 1,03 — y ahora hay un numero que
   lo obliga, no solo una doctrina que lo permite.
   `restraint` apagaba el zoom del sistema en TODAS las laminas por doctrina
   ("un expediente no se agranda"). En la ficha eso nunca fue correcto: quitar
   el zoom deja la superficie principal del producto respondiendo solo con un
   filete de 1 px.

   PRESUPUESTO RECALCULADO. La cifra vieja —"techo 1,32"— se habia medido en
   UN solo viewport, el de 1440 px, donde la lamina de ficha mide 371x247. Se
   ha barrido el rango entero de 700 a 1920 px en pasos de 20: la ficha no es
   mas ancha a 1440, es mas ancha a 1040, donde la rejilla `auto-fill` la lleva
   a 448x298,7 antes de admitir otra columna. Ese es el caso que manda:

     caja 448x298,7 · fuente 979x734 · s = 0,4576
     reposo  ......... 1,093 px de fuente por px de pantalla @DPR2
     con zoom 1,03 ... 1,061

   Es decir: el techo real no es 1,32, es 1,093, y el 1,03 que usa el sistema
   gasta el 94% del presupuesto. NO se sube. La conclusion no cambia respecto
   a la version anterior de este comentario, pero el motivo si: antes se
   conservaba 1,03 porque sobraba margen y mandaba la doctrina de
   DESIGN-SYSTEM §6.1; ahora se conserva porque, sobre la miniatura que la
   ficha sirve, el margen NO sobra. La doctrina y la fisica coinciden, que es
   la mejor situacion posible para un numero.

   POR QUE LA FICHA NO PASA A ORIGINALES COMO SI HIZO LA LAMINA DE BARRIO
   (§6): porque no lo necesita. Servir el original subiria el techo de 1,093 a
   1,429 —presupuesto que no se va a gastar, porque §6.1 fija 1,03— a cambio
   de 989 KB y 1,7 MB en dos de las seis portadas. La regla es servir la
   resolucion que el presupuesto optico PIDE, no la maxima que hay en disco. */
.js-motion .c-listing__folio {
  transition: color var(--c-dur-tick) var(--c-ease-ink);
}
.js-motion .c-listing:hover .c-listing__folio,
.js-motion .c-listing:focus-within .c-listing__folio { color: var(--c-brass-text); }


/* ── §6 · BARRIOS: la lamina VUELVE a responder, y el velo se sigue abriendo ─

   AQUI ESTABA EL DANO. Este bloque apagaba el zoom del sistema en la lamina
   de barrio con un argumento que parecia de fisica y era de contabilidad mal
   llevada. Decia: "caja 302x403 sobre fuente 979x734, s = 0,549 -> techo 0,91
   @DPR2; la lamina YA esta submuestreada EN REPOSO; cualquier zoom ahi es
   pixelado garantizado". La aritmetica era correcta. La fuente no. 979x734 no
   es la resolucion de la fotografia: es la resolucion de la MINIATURA que la
   portada estaba sirviendo. Ver §5.1.

   Y ese es el matiz que importa, porque no fue un error de medicion sino de
   diagnostico: el sintoma observado —la lamina de barrio esta submuestreada—
   era REAL. Lo que estaba mal era la causa. No era que la foto no existiera a
   mas resolucion; era que la portada pedia el archivo equivocado. Del
   diagnostico falso salio una conclusion irreversible ("no se puede ampliar")
   en vez de la reparable ("hay que servir el otro archivo").

   LO QUE SE HIZO. scripts/build-home.py sirve ahora la lamina de barrio desde
   una derivada del ORIGINAL, con el lado largo topado en 1280 px — la
   resolucion que el presupuesto de abajo pide, no la maxima en disco (hay
   originales de 4032x3024 que pesan 1,7 MB para pintar 302 px).

   PRESUPUESTO. No se calcula en el viewport comodo: se barre el rango entero
   de 700 a 1920 px en pasos de 20 —62 anchos x 4 azulejos— y se toma el peor,
   con la caja YA ensanchada por el travelling de §9.3. El peor de todos es el
   viewport de 700 px, donde la rejilla `auto-fit` deja una sola columna y el
   azulejo se va a 318x424:

                              @DPR2, reposo    con el zoom 1,03 puesto
     antes (miniatura 979x734)      0,866            0,841
     ahora (derivada del original)  1,127            1,095

   Antes NO era una opinion prudente: la lamina de barrio estaba de verdad
   submuestreada, un 13% por debajo del umbral, y ampliarla habria pixelado.
   Ahora hay un 13% por encima, con el zoom puesto y en el peor viewport que
   existe. El techo llega a 1,127 y se gasta 1,03.

   En el viewport de referencia de 1440 px los cuatro azulejos miden asi:
     Punta Roca ..... 1280x960 -> 1,192 reposo · 1,157 con zoom
     Portal Genoves .. 960x1280 -> 1,455 reposo · 1,412 con zoom
     Alto Prado ..... 1280x956 -> 1,187 reposo · 1,153 con zoom
     Villa Campestre  1280x960 -> 1,192 reposo · 1,157 con zoom

   POR QUE 1,03 Y NO EL TECHO. Porque la lamina de barrio es NAVEGACION y la
   ficha es PRODUCTO. Darle a los barrios una amplitud mayor que a los
   inmuebles pondria la fila de territorio a moverse mas que la rejilla de
   inventario, que es exactamente al reves de lo que la pagina quiere decir.
   Se recupera el gesto que la premisa falsa habia prohibido; no se inventa
   uno nuevo. --c-zoom-plate manda en las dos superficies, igual.

   SE CONSERVA el injerto de [cinema]: el velo que ya existe —y que existe
   porque el nombre del barrio tiene que leerse— se ABRE un punto al mirarlo.
   El gesto solo puede hacer la foto MAS visible, nunca menos: es la
   diferencia exacta con el velo del 7% que se descarto.

   PARIDAD TECLADO/RATON: 05-pages.css declara el zoom solo en `:hover`. Se
   anade aqui el `:focus-within` que faltaba, en las dos superficies, porque
   esta capa se comprometio a que raton y teclado vean lo mismo y ese
   compromiso lo cumplia todo menos la propia lamina. */
.js-motion .c-zone:hover .c-plate img,
.js-motion .c-zone:focus-within .c-plate img,
.js-motion .c-listing:focus-within .c-plate img {
  transform: scale(var(--c-zoom-plate));
}

.js-motion .c-zone__scrim {
  transition: opacity var(--c-dur-plate) var(--c-ease-ink);
}
.js-motion .c-zone:hover .c-zone__scrim,
.js-motion .c-zone:focus-within .c-zone__scrim {
  opacity: .78;
  transition-duration: var(--c-dur-hover);
}

/* El rotulo se levanta 4 px: el barrio da un paso al frente.  [document] */
.js-motion .c-zone__meta {
  transition: transform var(--c-dur-plate) var(--c-ease-plate);
}
.js-motion .c-zone:hover .c-zone__meta,
.js-motion .c-zone:focus-within .c-zone__meta {
  transform: translate3d(0, -4px, 0);
  transition-duration: var(--c-dur-hover);
}

/* Y el filete, el mismo verbo de siempre, por encima del velo. */
.js-motion .c-zone::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--c-z-meta);
  block-size: calc(var(--c-hair-w) * 2);
  background-color: var(--c-brass);
  transform: scaleX(0);
  transform-origin: 0% 100%;        /* crece hacia arriba al apretar (§7b) */
  transition: transform var(--c-dur-hover) var(--c-ease-rule);
  pointer-events: none;
}
.js-motion .c-zone:hover::after,
.js-motion .c-zone:focus-within::after { transform: scaleX(1); }

/* La lamina de barrio va sobre fotografia: el anillo de foco necesita
   contraste. --c-brass-hair mide 1,62:1 contra un gris medio, por debajo del
   3:1 que exige WCAG 1.4.11 para un indicador de foco; --c-brass-hi mide
   9,73:1 contra negro. Lo levanto el juez de accesibilidad y aqui se cierra. */
.js-motion .c-zone:focus-visible,
.js-motion .c-hero :focus-visible {
  outline-color: var(--c-brass-hi);
  box-shadow: 0 0 0 calc(var(--c-focus-w) + var(--c-focus-offset)) var(--c-veil-86);
}


/* ── §7 · CONTROLES: acuse de recibo, no animacion  [restraint] ────────────
   Un boton que tarda 400 ms en reconocer el puntero se siente lento. La base
   los transiciona con --c-dur-hover; aqui bajan a --c-dur-tick (180 ms).
   Contencion no es lentitud: es no moverse. Cuando hay que responder, se
   responde de inmediato.

   EXCEPCION nº1 A LA REGLA "solo transform, opacity y filter", MEDIDA:
   estos dos controles transicionan `background-color`, que es pintado.
   Superficies medidas a 1440x900 con getBoundingClientRect, no estimadas:
       .c-search__submit   153x71 = 10.845 px
       .c-nav__cta         211x45 =  9.469 px
   Peor caso con los dos repintando a la vez: 20.314 px = 1,57% de un viewport
   de 1440x900, y nunca coinciden porque el boton Buscar solo existe en el
   fold. Promoverlos a capa compuesta costaria mas memoria de la que ahorran
   en pintado. */
.js-motion .c-search__submit { transition-duration: var(--c-dur-tick); }
.js-motion .c-nav__cta       { transition-duration: var(--c-dur-tick); }

/* Campos del buscador: mismo verbo. El filete marca donde esta el cursor. */
.js-motion .c-search__field { position: relative; }
.js-motion .c-search__field::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: calc(var(--c-hair-w) * 2);
  background-color: var(--c-brass);
  transform: scaleX(0);
  transform-origin: 0% 50%;
  transition: transform var(--c-dur-hover) var(--c-ease-rule);
  pointer-events: none;
}
.js-motion .c-search__field:focus-within::after,
.js-motion .c-search__field:hover::after { transform: scaleX(1); }

/* Enlace lateral de cabecera de seccion ("Ver los 20 registros"). */
.js-motion .c-sectionhead__aside {
  position: relative;
  transition: color var(--c-dur-tick) var(--c-ease-ink);
}
.js-motion .c-sectionhead__aside::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: calc(var(--c-sp-1) * -1);
  block-size: var(--c-hair-w);
  background-color: var(--c-brass);
  transform: scaleX(0);
  transform-origin: 100% 50%;
  transition: transform var(--c-dur-hover) var(--c-ease-rule);
}
.js-motion .c-sectionhead__aside:hover::after,
.js-motion .c-sectionhead__aside:focus-visible::after {
  transform: scaleX(1);
  transform-origin: 0% 50%;
}

/* §7b · LA PRESION — el unico gesto del sistema que usa --c-ease-seal.
   Nada acusaba recibo de una PULSACION, solo del puntero. Un instrumento
   bien hecho responde tambien cuando lo aprietas. La respuesta no es un
   desplazamiento ni un rebote: el filete DOBLA su grosor mientras el dedo
   esta abajo. Es la misma linea de siempre, apretando mas fuerte — como una
   pluma que carga la mano. scaleY sobre un pseudoelemento de 1 px: transform
   puro, sin reflow, sin repintar la fotografia. */
.js-motion .c-listing:active .c-listing__plate::after,
.js-motion .c-zone:active::after {
  transform: scaleX(1) scaleY(2);
  transition-duration: var(--c-dur-tick);
  transition-timing-function: var(--c-ease-seal);
}
/* Los dos botones acusan la pulsacion volviendo a su color de reposo: el
   hover los aclara, la pulsacion los devuelve a la tinta. Es "el sello baja".
   CONTRASTE VERIFICADO, y no es opcional: `:active` se dispara TAMBIEN con
   Enter desde el teclado, cuando `:hover` NO esta puesto y el color de texto
   sigue siendo el de reposo.
     .c-search__submit  marfil #FAF8F5 sobre tinta #0C0C0C ......... 18,45:1
     .c-nav__cta        se fija color Y borde ademas del fondo, para que la
                        combinacion de teclado (activo sin hover) de a
                        tinta #0C0C0C sobre marfil #FAF8F5 .......... 18,45:1
   Un #8A7658 de laton oscuro habria dado 4,13:1 con texto de 11 px: por
   debajo de AA. Medido, no estimado. */
.js-motion .c-search__submit:active { background-color: var(--c-ink); }
.js-motion .c-nav__cta:active       { background-color: var(--c-paper);
                                      color: var(--c-ink);
                                      border-color: var(--c-paper); }


/* ── §8 · LA CIFRA QUE CUENTA  [document] ──────────────────────────────────
   El unico gesto de toda la competencia que ANADE INFORMACION en vez de
   decoracion, y el mas barato medido: escribe cada 45 ms (~22 fps) en vez de
   cada fotograma, y en la traza del juez de rendimiento produjo 0 eventos de
   Layout. La columna vertebral de esta capa lo rechazaba por doctrina ("un
   dato que sube es un dato actuando"); dos de los tres jueces lo injertaron
   por nombre. Se acepta la enmienda y se dice aqui, no en un comentario
   escondido.

   TRES SALVAGUARDAS, porque es lo UNICO de esta capa que muta el DOM:
     1. La cifra esta en el HTML y nunca desaparece: se SOBREESCRIBE. El valor
        original se guarda en `data-motion-value` antes de tocar nada.
     2. Un vigilante restituye el valor exacto pase lo que pase, aunque el
        contador muera a mitad (motion.js §6).
     3. Bajo prefers-reduced-motion el contador NI SIQUIERA SE INSTALA.

   EXCEPCION nº2 A LA REGLA "solo transform, opacity y filter", MEDIDA: la
   cifra se tinta de laton mientras cuenta y vuelve a tinta al detenerse —
   mientras cuenta es un calculo, cuando se para es un dato. Eso es `color`,
   propiedad de pintado. Superficie medida: los cuatro .c-stat__n suman
   4 x 296x48,4 = 57.306 px = 4,42% de un viewport de 1440x900, y solo se
   repintan durante 900 ms una vez en la vida de la pagina. --c-brass-hair
   mide 6,03:1 sobre papel: la cifra pasa AA incluso a mitad de conteo.

   La superficie se mide sobre la CAJA del elemento y no sobre los glifos,
   que es lo que repinta de verdad: `.c-stat__n` es `display:block` dentro de
   una columna de la grilla de cifras, asi que cada una ocupa 296x48,4 px
   aunque el numero sean tres caracteres. */
.js-motion .c-stat__n {
  transition: color var(--c-dur-plate) var(--c-ease-ink);
}
.js-motion .c-stat.is-counting .c-stat__n { color: var(--c-brass-hair); }


/* ── §9 · MOVIMIENTO LIGADO AL SCROLL ──────────────────────────────────────
   TODO este bloque vive dentro de `@supports (animation-timeline: view())`.
   El patron es el injerto que el juez de rendimiento y el de accesibilidad
   pidieron por nombre, y los motivos son, en orden de importancia:

   a) SEGURIDAD. Fuera del bloque no hay una sola declaracion. Un navegador
      sin scroll-driven animations recibe la pagina estatica y COMPLETA. No
      existe el estado "esperando a que algo dispare", que es exactamente lo
      que descalifico a la tercera direccion. Y dentro del bloque, en progreso
      0 —lo que ve un rastreador que no hace scroll— todo esta en su sitio.
   b) COSTE. En Chromium estas animaciones corren en el COMPOSITOR. Cero
      listeners de scroll, cero requestAnimationFrame, cero JS en el camino
      del gesto. La referencia gasta un setInterval a 4 Hz que corre aunque
      nadie scrollee.
   c) HONESTIDAD. Cero librerias. La referencia carga GSAP 3.11.5 (24,7 KB)
      para ejecutar 0 tweens, medido.

   SON CUATRO GESTOS Y NI UNO MAS. Se dejaron fuera a proposito el dolly de
   ficha (una capa compuesta por imagen durante todo el scroll, a cambio de
   una escala que el asentamiento ya insinua) y el filete de cabecera de
   seccion trazado por scroll (a scrollY=0 queda a medio trazar y cualquier
   captura del fold parece un bug; el juez de gusto lo levanto).

   TODOS son `translate` puro. CERO opacidad sobre contenido, tambien aqui. */
@supports (animation-timeline: view()) {

  /* 9.0 · LINEAS DE TIEMPO CON NOMBRE — trampa MEDIDA, no teoria, y hay que
     propagarla a DESIGN-SYSTEM.md: `animation-timeline: view()` se ancla al
     CONTENEDOR DE SCROLL mas cercano, y `overflow: hidden` YA ES un contenedor
     de scroll. Nuestro sistema monta toda la fotografia dentro de cajas
     recortadas —`.c-hero`, `.c-plate` y `.c-zone` llevan `overflow: hidden`—
     asi que un `view()` puesto sobre la imagen se ata a una caja que no
     scrollea nunca y se queda congelado en progreso 0 (medido:
     ViewTimeline.currentTime = -0,001% con scrollY 900). La solucion es
     declarar la linea de tiempo en un elemento que SI vive en el scroll del
     documento y referenciarla por nombre desde dentro del recorte.
     Nota: gracias a la regla nº1 el sintoma de ese fallo fue una animacion
     QUIETA, no una pagina en blanco. */
  .c-hero    { view-timeline-name: --c-motion-hero; }
  .c-zone    { view-timeline-name: --c-motion-zone; }
  .c-listing { view-timeline-name: --c-motion-card; }

  /* 9.1 · EL HERO SE HUNDE — parallax de SALIDA, 48 px sobre ~684 px de
     recorrido: la foto viaja al 93% de la velocidad de la pagina. Es
     deliberadamente poco. La referencia no tiene parallax NINGUNO (medido:
     desplazamiento 1:1 exacto en 17 posiciones y 0 llamadas a rAF durante el
     scroll) y aun asi se siente viva; lo que se busca aqui no es un efecto,
     es que el hero no se despegue como una calcomania.

     COBERTURA GARANTIZADA POR CONSTRUCCION: la lamina se estira hacia ARRIBA
     exactamente lo que va a bajar. Asi el parallax no le roba ni un pixel de
     holgura al recorte de `cover` —holgura que en un viewport alto no
     existe— y no puede descubrir papel por arriba en NINGUN viewport. Es la
     diferencia entre un parallax que funciona y uno que funciona en el
     portatil del que lo programo.

     Va sobre la LAMINA y no sobre la imagen, asi que no se pelea con el
     asentamiento de escala de §2.2, que vive en el <img>. */
  .c-hero__plate { inset-block-start: calc(var(--c-m-lag) * -1); }

  @keyframes c-m-lag { from { translate: 0 0; } to { translate: 0 var(--c-m-lag); } }

  .c-hero__plate {
    animation-name:            c-m-lag;
    animation-duration:        auto;
    animation-timing-function: linear;
    animation-fill-mode:       both;
    animation-timeline:        --c-motion-hero;
    animation-range:           exit 0% exit 100%;
  }

  /* 9.2 · EL TITULAR SE VA ANTES QUE LA FOTO — contramovimiento de salida.
     `transform`, no `translate`: asi compone con el asentamiento de carga de
     §2.3 sin necesitar `animation-composition: add`. Y sin opacidad: la
     direccion de la que viene este gesto disolvia el titular hasta 0,12 y
     todos los jueces lo dieron por bueno porque solo ocurria en rango `exit`,
     pero aqui la separacion la produce el diferencial de velocidad y la
     opacidad no compra nada que valga romper la garantia del archivo. */
  @keyframes c-m-hero-rise {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(0, -24px, 0); }
  }
  .c-hero__inner {
    animation-name:            c-m-hero-rise;
    animation-duration:        auto;
    animation-timing-function: linear;
    animation-fill-mode:       both;
    animation-timeline:        --c-motion-hero;
    animation-range:           exit 0% exit 90%;
  }

  /* 9.3 · TRAVELLING DE LAS LAMINAS DE BARRIO — sigue siendo el mejor negocio
     del archivo, pero ahora el margen se GARANTIZA en vez de deducirse.

     La version anterior razonaba asi: "la lamina es 3:4 vertical y la fuente
     4:3 apaisada, luego `cover` renderiza 537 px para una caja de 302 y
     descarta 235; mover la imagen +/-14 px ahi dentro cuesta cero". El
     razonamiento era correcto MIENTRAS todas las fuentes fueran 4:3 — que es
     lo que parecia cuando todas eran la misma miniatura de 979x734. En cuanto
     §6 empezo a servir originales dejo de serlo: el original de Portal
     Genoves es 1200x1600, 3:4 EXACTO, la MISMA proporcion que la caja.
     `cover` no descarta nada, el margen es CERO, y el travelling habria
     sacado papel por el borde en el primer fotograma.

     LA CORRECCION ES LA MISMA TECNICA QUE §9.1 usa en el hero: no se calcula
     el margen, se fabrica. La imagen se ensancha exactamente 2x --c-m-truck y
     se desplaza la mitad hacia la izquierda, asi que el recorrido lateral
     tiene su holgura por construccion, con cualquier fuente y en cualquier
     viewport. Deja de existir la clase de fallo entera.

     COSTE:
       · fuentes 4:3 — manda el ALTO en `cover`, asi que ensanchar la caja no
         cambia la escala: coste EXACTAMENTE CERO, igual que antes.
       · fuente 3:4 (Portal Genoves) — manda el ancho: a 1440 px el muestreo
         baja de 1,589 a 1,455. Se gasta el 8% de un superavit del 59%.
     A cambio, la garantia deja de depender de que nadie suba nunca una foto
     vertical.

     VERIFICADO POR BARRIDO, no por deduccion: 62 anchos de viewport (700 a
     1920, paso 20) x 4 azulejos, midiendo en cada uno cuanto cubre la imagen
     en el extremo peor del recorrido. Minimo de las 248 medidas: +1,00 px.
     Nunca negativo, es decir nunca asoma papel.

     La direccion alterna por posicion: la fila entera se lee como un plano
     lateral por delante de cuatro ventanas, no como cuatro efectos iguales.

     `max-inline-size: none` NO ES OPCIONAL Y COSTO UNA MEDICION: 01-base.css
     lleva el `img { max-inline-size: 100% }` que todo reset sensato tiene, y
     ese tope volvia a recortar la imagen a 302 px en silencio. El margen
     negativo si se aplicaba, asi que la lamina quedaba DESCENTRADA 14 px y sin
     una sola holgura ganada — un fallo que a ojo parece un encuadre raro y no
     una regla anulada. Se anota aqui porque es la trampa exacta que va a
     repetir quien copie esta tecnica.

     EL PIXEL DE MAS TAMBIEN ESTA MEDIDO. Ensanchando 2x --c-m-truck exacto, en
     el extremo del recorrido el borde de la imagen cae EXACTAMENTE sobre el
     borde de la placa: cobertura 0,00 px (comprobado en el azulejo 3:4, que es
     el unico sin holgura propia). Cero no es un margen, es un empate, y la
     caja de la lamina no siempre tiene ancho entero —el barrido de viewports
     la deja en 302, 318, 268 y valores fraccionarios entremedias—. Se anade
     1 px por lado. Coste de resolucion: NINGUNO en las fuentes 4:3, donde
     manda el alto; 0,6% en la 3:4. */
  .c-zone .c-plate img {
    inline-size:         calc(100% + var(--c-m-truck) * 2 + 2px);
    max-inline-size:     none;
    margin-inline-start: calc(var(--c-m-truck) * -1 - 1px);
  }

  @keyframes c-m-truck-l {
    from { translate: calc(var(--c-m-truck) * -1) 0; }
    to   { translate: var(--c-m-truck) 0; }
  }
  @keyframes c-m-truck-r {
    from { translate: var(--c-m-truck) 0; }
    to   { translate: calc(var(--c-m-truck) * -1) 0; }
  }
  .c-zone .c-plate img {
    animation-name:            c-m-truck-l;
    animation-duration:        auto;
    animation-timing-function: linear;
    animation-fill-mode:       both;
    animation-timeline:        --c-motion-zone;
    animation-range:           cover 0% cover 100%;
  }
  .c-zone:nth-child(2n) .c-plate img { animation-name: c-m-truck-r; }

  /* 9.4 · ASENTAMIENTO — el bloque se posa --c-enter-y (14 px), la distancia
     que 00-tokens.css fija para "un documento no se desliza 100%: se asienta".
     `translate` puro, OPACIDAD INTACTA. Esto sustituye al `fadeInUp` de la
     referencia y al `.c-reveal` del sistema: mismo efecto de llegada, cero
     riesgo, cero JavaScript y cero observador. El escalonado no lo hace
     ningun temporizador — cada elemento tiene su propia linea de tiempo y
     basta desplazar el `animation-range` por columna, asi que responde a la
     posicion de scroll y no al reloj. */
  @keyframes c-m-settle {
    from { translate: 0 var(--c-enter-y); }
    to   { translate: 0 0; }
  }
  .c-listing,
  .c-step,
  .c-stat {
    animation-name:            c-m-settle;
    animation-duration:        auto;
    animation-timing-function: linear;
    animation-fill-mode:       both;
    animation-timeline:        view();
    animation-range:           entry 0% cover 38%;
  }
  .c-listing { animation-timeline: --c-motion-card; }

  .c-listing:nth-child(3n+2),
  .c-step:nth-child(2n),
  .c-stat:nth-child(4n+2) { animation-range: entry 5%  cover 43%; }
  .c-listing:nth-child(3n+3),
  .c-stat:nth-child(4n+3) { animation-range: entry 10% cover 48%; }
  .c-stat:nth-child(4n+4) { animation-range: entry 15% cover 53%; }
}


/* ── §10 · FOCO: el unico sitio donde CERO movimiento es accesibilidad ─────
   Un usuario de teclado tabulando rapido no puede esperar 180 ms a saber
   donde esta. Instantaneo no es descuido: es el requisito. El anillo no
   espera; la afordancia (los pseudo-filetes de §4, §5, §6 y §7) si se traza,
   porque eso es informacion y no el indicador. */
.js-motion :focus-visible          { transition-property: none; animation: none; }
.js-motion :focus-visible::after   { transition-property: transform; }


/* ── §11 · CONTENCION TAMBIEN EN MOVIL ─────────────────────────────────────
   La cabecera fija se RETIRA por debajo de 1024 px, donde 05-pages.css ya
   esconde los enlaces de nav. Medido a 390 px de ancho: la cabecera mide
   111,8 px de alto — el 12,5% del viewport. Fijarla significa gastar ese
   octavo de pantalla, permanentemente, en un wordmark y un boton. En movil el
   recurso escaso es el alto. Contencion tambien es saber retirarse. */
@media (max-width: 1023px) {
  .js-motion .c-nav          { position: absolute; }
  .js-motion .c-nav::before,
  .js-motion .c-nav::after   { display: none; }
  .js-motion :target,
  .js-motion #contenido,
  .js-motion #inventario,
  .js-motion #territorio,
  .js-motion #protocolo      { scroll-margin-block-start: 0; }
}

/* En una pantalla tactil no hay puntero: los estados de hover no existen y no
   hay nada que retirar, porque esta capa NO vela nada en reposo. Lo unico que
   se ajusta es la latencia del travelling, que en un viewport bajo recorre su
   rango mucho mas rapido. */
@media (hover: none) {
  :root { --c-m-truck: 8px; }
}


/* ── §12 · IMPRESION  [document] ───────────────────────────────────────────
   Barato, correcto, y ninguna de las tres direcciones lo tenia salvo una.
   `:not(.c-skip)` para no imprimir el enlace de salto, que vive fuera de
   pantalla con un translate. */
@media print {
  .c-hero::after,
  .c-plate::before            { display: none !important; }

  /* En papel no hay travelling, luego tampoco hace falta la holgura de §9.3.
     Sin ella la lamina se imprime con el encuadre centrado exacto. */
  .c-zone .c-plate img        { inline-size: 100% !important;
                                max-inline-size: 100% !important;
                                margin-inline-start: 0 !important; }

  *:not(.c-skip),
  *:not(.c-skip)::before,
  *:not(.c-skip)::after {
    animation: none !important;
    transition: none !important;
    translate: none !important;
    scale: none !important;
    opacity: 1 !important;
  }
  .js-motion .c-nav { position: absolute !important; }
}


/* ── §13 · prefers-reduced-motion — LA CAPA ENTERA SE APAGA ────────────────
   NO ES OPCIONAL, y no basta con heredar los tokens. 00-tokens.css lleva las
   duraciones a 1 ms y 01-base.css fuerza ademas `animation-iteration-count:1`,
   pero hay que decir alto el bug que hay debajo y que hay que propagar a
   00-tokens.css: UNA DURACION DE 1 ms NO PARA UNA ANIMACION `infinite`, LA
   VUELVE ESTROBOSCOPICA — que es lo contrario de lo que pide un usuario
   fotosensible. Esta capa no tiene ni una animacion infinita, pero se mata
   por `animation: none`, no por duracion, para que la promesa no dependa de
   una herencia de tokens. Es literalmente el fallo que MOTION-FORENSICS midio
   en la referencia: su guarda solo cubria `.animated` y se le escapaban 11
   elementos de cabecera que seguian animando 1 s.

   El PRIMER cinturon no es este bloque: es motion.js, que bajo esta
   preferencia no instala el contador en absoluto (doctrina de `document`,
   la mejor pieza de accesibilidad de las tres propuestas). Este bloque es el
   SEGUNDO cinturon.

   LO QUE SOBREVIVE, porque es legibilidad y textura y no movimiento:
     · la cabecera se solidifica igual, solo que instantaneamente;
     · el indice sigue nombrando el capitulo en curso;
     · el grano estatico de la lamina sigue ahi — es materia, no movimiento;
     · los filetes aparecen puestos o quitados, sin trazo.
   Objetivo verificable: document.getAnimations().length === 0. */
@media (prefers-reduced-motion: reduce) {

  .c-hero::after,
  .c-hero__plate,
  .c-hero__plate img,
  .c-hero__inner,
  .c-listing,
  .c-step,
  .c-stat,
  .c-listing .c-plate img,
  .c-zone .c-plate img {
    animation: none !important;
    animation-play-state: paused !important;
    translate: none !important;
    scale: none !important;
    transform: none !important;
  }

  /* El ensanchamiento de §9.3 solo existe para dar holgura a un travelling que
     bajo esta preferencia no ocurre. Se retira: sin el, `cover` vuelve a la
     escala mas pequena que cubre la caja, y el fotograma en reposo es el mas
     definido que da la fuente. Se apaga la garantia porque se apago el riesgo,
     y por doctrina de §13 se dice por `inline-size`, no por confiar en que
     --c-m-truck valga 0 en algun sitio. */
  .c-zone .c-plate img {
    inline-size:         100% !important;
    max-inline-size:     100% !important;
    margin-inline-start: 0    !important;
  }

  .js-motion .c-nav::before,
  .js-motion .c-nav::after,
  .js-motion .c-nav__links a,
  .js-motion .c-nav__links a::after,
  .js-motion .c-nav__cta,
  .js-motion .c-listing__plate::after,
  .js-motion .c-listing__folio,
  .js-motion .c-zone::after,
  .js-motion .c-zone__scrim,
  .js-motion .c-zone__meta,
  .js-motion .c-search__field::after,
  .js-motion .c-search__submit,
  .js-motion .c-sectionhead__aside,
  .js-motion .c-sectionhead__aside::after,
  .js-motion .c-stat__n,
  .c-plate img {
    transition: none !important;
  }
}


/* ── §14 · CENSO DE ESTA CAPA ──────────────────────────────────────────────
   Las cifras medidas y su metodo estan en docs/MOTION.md. Aqui va lo que se
   puede contar leyendo el propio archivo.

   Declaraciones `opacity: 0` sobre CONTENIDO ................................ 0
   Elementos que declaran su estado oculto en CSS ............................ 0
   Bucles `infinite` en toda la capa ......................................... 0
   Bytes de libreria externa ................................................. 0
   Peticiones de red que anade esta capa ..................................... 0
   Imagenes que esta capa amplia en REPOSO ................................... 0
       (el zoom de §5.3 y §6 es solo :hover / :focus-within; el asentamiento
        del hero §2.2 RESUELVE a 1.000. Ninguna foto queda ampliada quieta.)
   Superficies fotograficas que responden al puntero ......................... 2
       ficha .c-listing (1,03) · lamina .c-zone (1,03, restituida: §6)
   Peor muestreo en reposo tras la auditoria de resolucion @DPR2 ......... 0,889
       el hero, y esta acotado por su propio JPEG (§2.2), no por el inventario
   Peor muestreo de una lamina del inventario, con el zoom puesto ....... 1,061
       ficha a 448x298,7, el viewport de 1040 px (§5.3)
   Propiedades animadas ...................................... transform, translate,
                                                  scale, opacity + 2 excepciones
   Excepciones documentadas y medidas ........................................ 2
       §7 background-color en 2 controles (1,57% del viewport, peor caso)
       §8 color en 4 cifras (4,42% del viewport, una vez, 900 ms)
   Acontecimientos autonomos en toda la vida de la pagina .................... 3
       §2.1 el velo del hero · §2.2 la camara se posa · §2.3 el titular se asienta
   Gestos ligados al scroll (todos dentro de @supports, todos translate) ..... 4
       §9.1 hero se hunde · §9.2 titular sube · §9.3 travelling · §9.4 asentamiento
   Estados que cambia el scroll fuera de esos cuatro ......................... 2
       §3 la cabecera se vuelve legible · §4b el indice nombra el capitulo
   Elementos enfocables con respuesta explicita ......................... 22 de 23
   Superficies que ademas acusan la PULSACION ............................... 12
   Latencia del anillo de foco ............................................. 0 ms
   Tokens propios declarados ................................................. 6
   Duraciones nuevas que hubo que justificar ................................. 1
   ========================================================================== */
