/* =========================================================
   Solved Design System — ESCENA DE PRODUCTO
   SceneCard + SceneGrid. Sustituye a cualquier rejilla de "features" con
   icono y párrafo: aquí se enseña producto funcionando, no una promesa.

   Requiere ds/tokens.css y ds/tint.css.

   API — atributos, no clases sueltas:
     .scene-grid                        rejilla de 12 columnas, gutter 20px
       article.scene[data-module]       incidencias · acciones · checklists
                                        activos · kpis · ia   → DICTA EL TINTE
                [data-span]             4 · 6 · 8 · 12
                [data-ratio]            4:3 · 1:1 · 21:9
         .scene__title                  título, arriba a la izquierda
         .scene__media                  el fragmento de producto
         .scene__expand                 sólo si expande de verdad

   NO HAY FORMA DE PASAR UN COLOR. No existe --scene-tint, ni una clase de
   fondo, ni un modificador cromático: el único selector que pinta lienzo es
   .scene[data-module="…"] con la lista cerrada de seis valores. Un data-module
   que no esté en la lista no pinta nada y el validador de desarrollo lo canta.
   Esa restricción es el componente: sin ella, en tres meses hay lienzos
   elegidos por gusto y el color deja de significar módulo.
   ========================================================= */

/* ---------------------------------------------------------
   REJILLA
   Doce columnas dentro del encuadre existente. No trae padding ni ancho
   propios: vive dentro de .frame, como todo lo demás del sitio.
   --------------------------------------------------------- */
.scene-grid{
  display:grid;
  grid-template-columns:repeat(12,1fr);
  gap:20px;
}

/* Una pieza que no es escena pero comparte fila: la banda de IA de
   /incidencias/ entra en la rejilla como la tarjeta ancha de la primera fila.
   Ocupa las doce y toma el radio de la escena —16 y no 12—, porque dentro de
   esta rejilla lo que manda es la ventana, no la ficha. */
.scene-grid > .aiband{ grid-column:1 / -1; border-radius:16px }

/* ---------------------------------------------------------
   MARCO
   Radio 16 —mayor que --ds-radius-card (12px), que es el de las tarjetas de
   texto—: una escena es una ventana, no una ficha, y el radio la separa.
   Sin sombra: la única sombra del componente es la del fragmento.
   --------------------------------------------------------- */
.scene{
  position:relative;
  grid-column:span 6;
  border-radius:16px;
  overflow:hidden;
  border:1px solid var(--color-hairline);
  background:var(--ds-surface);
  transition:border-color var(--ds-motion);
}
.scene:hover{ border-color:var(--color-hairline-input) }

/* La tarjeta crece hacia fuera al pasar por encima. NUNCA con transform:scale:
   dentro hay una pantalla con texto de 10px y escalarla la deja borrosa, que
   es justo lo que no puede pasar en el sitio donde se gana la credibilidad.

   Crece un anillo por fuera del marco, en el color del módulo y casi
   transparente. Va en outline y no en una capa propia porque el outline se
   dibuja fuera del borde y NO lo recorta el overflow:hidden de la escena —una
   capa hija sí quedaría cortada, y ese overflow es el que recorta el
   fragmento, así que no se puede tocar. */
.scene{
  outline:0 solid transparent;
  transition:border-color var(--ds-motion),
             outline-width 320ms cubic-bezier(.16,1,.3,1),
             outline-color 320ms linear;
}
.scene:hover{
  outline-width:5px;
  outline-color:color-mix(in srgb, var(--scene-glow, var(--color-primary)) 12%, transparent);
}
/* El foco de teclado manda sobre el hover: ahí el anillo es el del sistema. */
button.scene:focus-visible,
a.scene:focus-visible{ outline:2px solid var(--color-primary); outline-offset:3px }

@media (prefers-reduced-motion:reduce){
  .scene{ transition:border-color var(--ds-motion) }
  .scene:hover{ outline-width:0 }
}

/* Una escena puede ser un enlace al módulo que enseña. Sigue sin llevar texto
   de llamada dentro: la tarjeta entera es el destino, y el cambio de borde más
   el cursor son la afordancia. Si hace falta un "Ver módulo →" para que se
   entienda que se puede pulsar, el patrón elegido es otro. */
a.scene{ display:block; text-decoration:none; color:inherit }
a.scene:focus-visible,
button.scene:focus-visible{ outline:2px solid var(--color-primary); outline-offset:3px }

/* Cuando la escena abre un diálogo en vez de navegar, es un <button> y no un
   <a>. No es formalismo: un enlace que no lleva a ninguna URL rompe el clic
   con rueda, el "abrir en pestaña nueva" y el anuncio del lector de pantalla.
   El destino del módulo no se pierde — vive dentro del diálogo. */
button.scene{
  display:block; width:100%;
  padding:0;
  font:inherit; color:inherit; text-align:left;
  -webkit-appearance:none; appearance:none;
  cursor:pointer;
}
/* Hover: sólo el borde. Ni elevación, ni escala, ni movimiento del fragmento —
   si la escena se mueve, el ojo va al gesto y no a lo que la tarjeta enseña. */

.scene[data-span="4"]{ grid-column:span 4 }
.scene[data-span="6"]{ grid-column:span 6 }
.scene[data-span="8"]{ grid-column:span 8 }
.scene[data-span="12"]{ grid-column:span 12 }

/* La altura la fija la proporción, nunca el contenido: dos escenas de la misma
   fila con distinta cantidad de fragmento tienen que medir lo mismo. */
.scene[data-ratio="4:3"]{ aspect-ratio:4/3 }
.scene[data-ratio="1:1"]{ aspect-ratio:1/1 }
.scene[data-ratio="21:9"]{ aspect-ratio:21/9 }

/* FILAS MIXTAS — data-ratio="fila"
   Los repartos 8+4 y 4+8 estaban permitidos pero no se podían montar: a doce
   columnas y 20px de calle, un 8 en 4:3 mide 571 de alto y un 4 en 1:1 mide
   371, y la proporción gana a `stretch` —comprobado: la corta NO se estira—,
   así que la fila quedaba con un agujero de 200px debajo de la estrecha.

   La salida no es igualar las proporciones (a distinto ancho, distinto alto
   igual) sino declarar quién manda: la ancha lleva su proporción y la estrecha
   lleva `fila`, que renuncia a la suya y toma el alto de la fila. Una fila, una
   proporción declarada; el resto la obedece.

   No es un tercer formato libre: sin `aspect-ratio` la caja depende de la
   vecina, así que `fila` sólo tiene sentido acompañando a una escena que sí la
   declare. Sola en su fila deja la altura al contenido, que es justo lo que el
   componente no quiere. */
.scene[data-ratio="fila"]{ aspect-ratio:auto; align-self:stretch }

/* ---------------------------------------------------------
   CUÁNDO SE MUEVE
   Dos variables, y todo lo que se anima dentro de una escena —aquí, en
   ds/app.css, ds/plantilla.css, ds/documentos.css, ds/celdas.css— las obedece
   con `animation-play-state:var(--play,running)` y
   `animation-iteration-count:var(--iter,infinite)`. Sin JS ninguna de las dos
   se toca, así que todo sigue en bucle desde el fotograma 0, que es el estado
   seguro.

   --play LO PONE ds/scene.js. Antes sólo pausaba lo que colgaba de `.app`
   (la regla vivía en ds/app.css); una escena sin `.app` dentro —un `.res`, un
   `.qrscan`, la pila de `.montaje`— seguía corriendo fuera de pantalla porque
   la regla no llegaba. Puesta aquí, en la escena entera, alcanza cualquier
   pieza que cuelgue de un `.scene`, la cargue o no `ds/app.css` la página —
   `/dashboard/` y `/casos-de-exito/` cargan `ds/scene.css` sin `ds/app.css`, y
   antes de esto no pausaban nada—. Y `ds/scene.js` reinicia el fotograma a 0
   al VOLVER a pantalla —`getAnimations().currentTime=0`, no sólo quita
   `data-offscreen`—, así que una escena vista a medias, perdida de vista y
   recuperada empieza otra vez desde el principio y no desde donde se quedó.

   --iter LO PONE ds/rotador.js, no esta hoja ni ds/scene.js. Una capacidad
   del rotador es un acordeón: se elige, cuenta su gesto una vez y se queda
   ahí — no lo repite mientras siga elegida. Puesto en JavaScript y no en CSS:
   sin script el rotador no oculta ningún panel (se ven las cuatro capacidades
   abiertas y en bucle, que es el estado seguro sin JS) y capar la vuelta a
   una sola en CSS puro las habría dejado corriendo una vez y quietas para
   siempre en su fotograma de bucle —que casi nunca es el resuelto, ver más
   abajo—. `animation-fill-mode:forwards` sostiene ESE fotograma final —el
   100 % del `@keyframes`—, y ahí está el matiz que hizo falta resolver aparte:
   muchas de estas piezas fabrican su 100 % IGUAL al 0 % a propósito, para que
   el bucle no dé un salto en la costura —el check de «leído» se apaga otra
   vez, el acto A del cruce vuelve a hacerse visible—, así que congelar ahí
   enseñaría el reinicio del bucle, no la historia contada. El estado que de
   verdad hay que sostener es el que ya describe cada
   `@media (prefers-reduced-motion:reduce)` de esta hoja y de ds/app.css,
   ds/plantilla.css, ds/documentos.css y ds/celdas.css —«el estado resuelto,
   el que cuenta la historia entera»—, duplicado tal cual bajo
   `.scene[data-terminado] …` más abajo. `ds/rotador.js` pone ese atributo en
   el instante exacto en que el reloj de la capacidad (`data-dur`) se cumple,
   y lo quita al reelegirla —reiniciando antes los fotogramas a 0 con
   `getAnimations().currentTime`, el mismo mecanismo que usa ds/scene.js—, así
   que vuelve a contarse entera. En cualquier escena que NO sea del rotador,
   `--iter` no se fija en ningún sitio y el `infinite` de siempre sigue
   mandando: nada de esto cambia una escena de una rejilla. */
.scene[data-offscreen]{ --play:paused }

/* ---------------------------------------------------------
   LIENZO TEÑIDO
   Capa propia bajo el contenido: así el mismo tinte sirve sobre blanco y sobre
   grafito sin tocar el marco. El tinte SÓLO se asigna aquí.
   --------------------------------------------------------- */
.scene::before{
  content:"";
  position:absolute;
  inset:0;
  pointer-events:none;
}
.scene[data-module="incidencias"]::before{ background-image:var(--tint-incidencias) }
.scene[data-module="acciones"]::before{    background-image:var(--tint-acciones) }
.scene[data-module="checklists"]::before{  background-image:var(--tint-checklists) }
.scene[data-module="activos"]::before{     background-image:var(--tint-activos) }
.scene[data-module="kpis"]::before{        background-image:var(--tint-kpis) }
.scene[data-module="ia"]::before{          background-image:var(--tint-ia) }

/* El mismo atributo dicta el color del halo del borde. Un módulo, un color:
   si el tinte y el halo pudieran ir por separado, en dos meses no coincidirían. */
.scene[data-module="incidencias"]{ --scene-glow:var(--module-incidencias) }
.scene[data-module="acciones"]   { --scene-glow:var(--module-acciones) }
.scene[data-module="checklists"] { --scene-glow:var(--module-registros) }
.scene[data-module="activos"]    { --scene-glow:var(--module-activos) }
.scene[data-module="kpis"]       { --scene-glow:var(--module-kpis) }
.scene[data-module="ia"]         { --scene-glow:var(--module-ia) }

/* La capa de IA es la única que va sobre grafito, y va sola: el token del
   tinte y el fondo oscuro se activan con el mismo atributo, así que no se
   pueden separar por descuido. */
.scene[data-module="ia"]{
  background:var(--color-graphite-900);
  border-color:#2A2E34;
}
.scene[data-module="ia"]:hover{ border-color:#3C424A }

/* ---------------------------------------------------------
   FONDO DECORATIVO DE LA HOME — .scene__deco
   Segunda vuelta (22 sep 2026): el shader de la hero no se adaptaba al cuadro
   —se recortaba en vez de rellenarlo— y pintaba un solo tono por módulo. Sale
   entero. En su lugar, seis piezas de SVG hechas a medida (olas, burbujas,
   puntos), con toda la paleta de Solved en cada una y un `viewBox` medido a
   la proporción real de su cuadro — el marcado y los degradados compartidos
   están en `index.html`, junto a la rejilla, no aquí: es contenido, no
   sistema, y sólo lo usan los seis `index.html`.

   `preserveAspectRatio="none"` en cada SVG es la pieza que resuelve "no se
   adapta al espacio": estira sin recortar sobre un viewBox ya proporcionado
   al cuadro, así que rellena el hueco exacto en cualquier ancho de ventana.
   Vive donde vivía el tinte —mismo hueco, z-index:0— y ENCIMA de él, así que
   el tinte plano del módulo sigue por debajo como base.
   --------------------------------------------------------- */
.scene-grid--carrusel .scene__deco{
  position:absolute;
  inset:0;
  z-index:0;
  width:100%;
  height:100%;
  pointer-events:none;
}

/* ---------------------------------------------------------
   TÍTULO
   Arriba a la izquierda, sobre el lienzo limpio. Dos líneas como máximo: si
   hacen falta tres, el título está explicando en vez de nombrar.
   Sin descripción dentro de la tarjeta — el párrafo va una vez, en el
   encabezado de la sección.
   --------------------------------------------------------- */
.scene__title{
  position:absolute;
  top:20px; left:20px; right:52px;
  z-index:2;
  margin:0;
  font:var(--type-body-md);
  letter-spacing:var(--tracking-body);
  color:var(--color-ink);
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}
.scene[data-module="ia"] .scene__title{ color:#fff }

/* ---------------------------------------------------------
   ENLACE BAJO EL TÍTULO — excepción declarada, no API del componente
   (10 sep 2026). El componente no trae hueco para enlace, a propósito («Sin
   descripción dentro de la tarjeta» arriba): esto es un `.scene__link` suelto
   que sólo se pinta donde el marcado lo añade, para las escenas que la propia
   documentación del sitio señaló como recuperables — ver la nota del 9 de
   septiembre sobre el rotador de capacidades retirado de `/incidencias/`.

   Se posiciona debajo del título asumiendo sus dos líneas (20px + 2×21px de
   `--type-body-md` + aire): si el título de una escena concreta cabe en una
   sola línea, el enlace sube más de lo necesario y no de menos, que es el
   error que no se ve. Mismo idioma visual que `.cab__link` (ds/aiband.css) —
   subrayado fino, sin negrita— pero en tinta clara: esa variante vive sobre
   grafito y aquí el lienzo es el tinte plano de `.scene[data-module]`.
   --------------------------------------------------------- */
.scene__link{
  position:absolute;
  top:70px; left:20px; right:52px;
  z-index:2;
  width:fit-content;
  font:var(--type-caption);
  letter-spacing:var(--tracking-caption);
  color:var(--color-ink-secondary);
  text-decoration:none;
  border-bottom:1px solid rgba(17,18,20,.24);
  padding-bottom:1px;
}
.scene__link:hover{ color:var(--color-ink); border-bottom-color:rgba(17,18,20,.5) }

/* ---------------------------------------------------------
   ESCENA ANCHA: el título en columna
   A 12 columnas el título arriba a la izquierda deja un tercio de lienzo
   muerto —la línea de texto mide lo que mide y el resto de la banda queda
   vacío—. Así que en la ancha el título baja al centro del tercio izquierdo y
   el fragmento ocupa los dos tercios de la derecha, sangrando por ese lado.
   Sigue sin haber descripción dentro: es el mismo título, sólo colocado donde
   la tarjeta lo necesita.
   --------------------------------------------------------- */
.scene[data-span="12"] .scene__title{
  top:50%; translate:0 -50%;
  left:6%; right:auto;
  width:26%;
  font:var(--type-display-md);
  letter-spacing:var(--tracking-display-md);
  /* Cuatro y no tres: el titular de la escena de causa raíz de /incidencias/
     —«Analiza la causa raíz y programa registros para asegurarte de que no se
     repite»— son 76 caracteres, y a tres líneas se cortaba con puntos
     suspensivos justo antes de la promesa. El titular va centrado en vertical,
     así que la cuarta línea no empuja nada. */
  -webkit-line-clamp:4;
}
.scene[data-span="12"] .scene__media[data-crop="br"],
.scene[data-span="12"] .scene__media[data-crop="r"]{ left:38%; top:14%; right:-10%; bottom:-12% }
/* `b` a doce columnas: como `br` pero SIN sangrado por la derecha. Es para el
   fragmento que no puede continuar fuera —una conversación cuya pregunta va
   pegada al margen derecho: si sangra, lo que se corta es justo la pregunta—. */
.scene[data-span="12"] .scene__media[data-crop="b"]{ left:38%; top:14%; right:3%; bottom:-12% }
.scene[data-span="12"] .scene__media[data-crop="dev-br"]{ left:38%; top:12%; width:72% }

/* ---------------------------------------------------------
   LA ESTRECHA DE UNA FILA MIXTA — data-span="4" + data-ratio="fila"
   Los recortes están calculados sobre una tarjeta de 6 columnas. A 4 columnas
   —y con el alto de la fila, o sea, más alta que ancha— los mismos porcentajes
   dejan el aparato en 108px de ancho, y dentro no hay texto que llegue a 10px
   reales. La regla de los 10px manda sobre la de la proporción, así que aquí
   el fragmento ocupa más porcentaje de tarjeta: en píxeles acaba midiendo lo
   mismo que en una de 6.

   Va atado a `fila` y no sólo a `data-span="4"`: una de 4 columnas en `1:1` es
   un cuadrado pequeño, no una tarjeta alta, y ahí el aparato al 50 % se sale.
   Lo que pide el ajuste es el alto de la fila mixta, no el ancho.
   --------------------------------------------------------- */
.scene[data-span="4"][data-ratio="fila"] .scene__media[data-crop="dev-movil-c"]{ top:26%; width:50% }
.scene[data-span="4"][data-ratio="fila"] .scene__media[data-crop="dev-movil"]{ left:8%; top:26%; width:48% }
/* Un aparato apaisado —tablet o ventana— en la estrecha de una fila mixta: con
   el 104 % de la regla general se queda a media altura y deja medio lienzo
   vacío debajo, porque la caja aquí es alta y estrecha. Se agranda y se baja:
   entra más aparato y se sale más por la derecha, que es lo que el encuadre
   dice de todas formas. */
.scene[data-span="4"][data-ratio="fila"] .scene__media[data-crop="dev-br"]{ top:30%; width:132% }
/* Un aparato apaisado en la ancha de una fila mixta (8 de 12). Con el 104 % de
   la regla general se sale por abajo y se lleva por delante el pie de la
   pantalla; cuando ahí abajo está lo que la tarjeta promete —la firma y el
   informe que sale solo—, el fragmento tiene que caber. Entra entero de ancho y
   sólo sangra por el borde inferior. */
.scene[data-span="8"] .scene__media[data-crop="dev-br"]{ left:11%; top:15%; width:88% }

/* Los recortes planos (br, bl, b, r) NO se tocan aquí, y se probó: sangrar más
   por el lado sólo ensancha el fragmento, y una rejilla como la de
   Integraciones —cuatro columnas centradas— se queda con dos logotipos y medio
   lienzo vacío. Con el recorte de siempre entran los mismos que en una tarjeta
   de 6 columnas, y lo que se pierde por el borde es la última columna. */

@media (max-width:900px){
  /* Ya no es ancha: el título vuelve arriba. */
  .scene[data-span="12"] .scene__title{
    top:20px; translate:none; left:20px; right:52px; width:auto;
    font:var(--type-body-md); letter-spacing:var(--tracking-body); -webkit-line-clamp:2;
  }
  .scene[data-span="12"] .scene__media[data-crop="br"],
  .scene[data-span="12"] .scene__media[data-crop="r"]{ left:11%; top:26%; right:-14%; bottom:-10% }
  .scene[data-span="12"] .scene__media[data-crop="dev-br"]{ left:13%; top:25%; width:104% }
  /* FALTABA "b" EN ESTE RESET (14 sep 2026, tercera vuelta), y es lo que
     dejaba la escena de IA de la home con el fragmento subido encima del
     título. `b` no estaba en la lista de arriba, así que por debajo de
     900px se quedaba con la regla de la ancha —`top:14%`, pensada para un
     título en columna a la izquierda que en móvil ya no existe—: la
     ventana del chat quedaba a 14% de una tarjeta cuyo título había vuelto
     arriba, y una tapaba a la otra. Mismo valor que la `b` de cualquier
     escena normal: sin sangrado por la derecha, que es lo que pide esta
     forma —la pregunta va pegada a ese margen y sangrar la cortaría—. */
  .scene[data-span="12"] .scene__media[data-crop="b"]{ left:11%; right:11%; top:26%; bottom:-10% }
  /* Y FALTABAN LOS `dev-br` DE LA FILA MIXTA (15 sep 2026). Las reglas de
     arriba para la ancha de 8 (`top:15%`) y la estrecha de 4 (`top:30%;
     width:132%`) tienen más especificidad que la general y no se reseteaban:
     por debajo de 900px ya no hay fila mixta, pero el iPad de Registros y la
     ventana de Informes de la home seguían arrancando al 15% de una tarjeta
     cuyo título ocupa hasta los 63px — el aparato tapaba la segunda línea.
     Vuelven al encuadre general del aparato apaisado. */
  .scene[data-span="8"] .scene__media[data-crop="dev-br"],
  .scene[data-span="4"][data-ratio="fila"] .scene__media[data-crop="dev-br"]{ left:13%; top:26%; width:104% }
}

/* ---------------------------------------------------------
   FRAGMENTO DE PRODUCTO
   Captura real, recortada por al menos un borde. El recorte es el gesto que
   dice "esto continúa"; un fragmento entero y centrado parece un icono grande.
   Se posiciona desde el HTML de cada escena (top/left/width), porque el
   encuadre depende de qué parte de la pantalla se está enseñando.
   --------------------------------------------------------- */
.scene__media{
  position:absolute;
  z-index:1;
  border-radius:10px;
  overflow:hidden;
  /* Contenedor de medida para los marcos de dispositivo (ds/device.css): bisel
     y radio del aparato se calculan sobre este ancho. */
  container-type:inline-size;
  /* La única sombra del componente: despega el fragmento del lienzo. */
  box-shadow:rgba(12,32,64,.10) 0 8px 24px;
}
/* EL FRAGMENTO SIN PANEL — .scene__media--suelto
   Quita la sombra, y con ella el rectángulo redondeado que la dibuja. Nació por
   el puente de Integraciones: ahí dentro no hay UNA captura que despegar del
   lienzo, hay una composición —ventana, ficha del ERP y cable— que ya trae su
   propia elevación en la ficha. Con la sombra del contenedor, la parte del
   encuadre que queda a la izquierda de la ventana se ve vacía y se lee como
   una segunda pantalla blanca detrás de la escena, que es justo lo que no hay.

   **LA REGLA, escrita el 9 de septiembre de 2026 a petición del cliente: en una
   animación no va una pantalla de fondo sin forma.** Si lo que hay dentro del
   encuadre no llena el encuadre —un panel más pequeño que la caja, un dibujo
   sobre el lienzo, una composición de piezas sueltas—, el rectángulo del
   contenedor asoma por los lados y se lee como una pantalla que no existe. Lo
   llevan ya el puente de la home y los dos cuadros de `/incidencias/` que van
   sin aparato: el selector de responsable y el Ishikawa con su espina.

   Sigue sin ser la norma: la sombra de `.scene__media` es la única del
   componente, y en una escena con captura, con marco de dispositivo o con
   fragmento que llena la caja sigue haciendo falta. La excepción es para lo que
   ya se sostiene solo. */
.scene__media--suelto{ box-shadow:none }

.scene__media img,
.scene__media picture,
.scene__media video{ display:block; width:100%; height:auto }

/* Encuadres de uso corriente. Todos se salen por al menos un lado. */
.scene__media[data-crop="br"]{ left:11%; top:26%; right:-14%; bottom:-10% }
.scene__media[data-crop="bl"]{ right:11%; top:26%; left:-14%; bottom:-10% }
.scene__media[data-crop="b"] { left:11%; right:11%; top:26%; bottom:-10% }
.scene__media[data-crop="r"] { left:11%; right:-14%; top:26%; bottom:12% }

/* EXCEPCIÓN: el panel SUELTO con crop `b`, en un móvil estrecho de verdad
   (30 sep 2026). `.app__asigna` (Incidencias, No-conformidades, Activos),
   `.app__conv`, `.app__raiz` y `.app__comparte` van todos sobre
   `data-crop="b"`, que deja 11 % de margen a cada lado — pensado para un
   fragmento con marco de dispositivo, no para un panel `--suelto` que ya trae
   su propio borde y su propia sombra. A 320px ese margen deja el campo de
   descripción de `.app__asigna` en ~180px de texto disponible, y «Ajustar el
   cierre del tapón en la embotelladora» (214px) y «Recalibrar el formador de
   pallets de PAL-004» (207px) se cortaban por la derecha dentro de
   `.app__campov` (`overflow:hidden`) — comprobado con `scrollWidth` contra el
   ancho real del campo, no a ojo. Pasa igual dentro de un rotador (sin
   `data-span`/`data-ratio`, ver `/no-conformidades/`) y en la fila mixta (con
   ellos, ver `/incidencias/` y `/gestion-de-activos/`): el panel no necesita
   el margen de un aparato en ninguno de los dos sitios. Se estrecha a 4 % sólo
   para el `--suelto` de este crop y sólo en el móvil más estrecho, y gana los
   ~40px que faltaban sin tocar el resto de escenas que comparten `b` con marco
   de dispositivo. Va DESPUÉS de la regla base a propósito: misma especificidad
   (0-2-0), y sin el orden correcto la base gana pase lo que diga la media
   query. */
@media (max-width:400px){
  .scene__media--suelto[data-crop="b"]{ left:4%; right:4% }
}

/* Encuadres para marco de dispositivo (ds/device.css). Aquí no se fija el alto:
   lo pone la proporción del propio aparato, que es la que lo hace reconocible.
   Se fija la esquina y el ancho, y el recorte lo hace el borde de la escena. */
.scene__media[data-crop^="dev-"]{ right:auto; bottom:auto }
.scene__media[data-crop="dev-br"]{ left:13%; top:25%; width:104% }
.scene__media[data-crop="dev-b"] { left:50%; translate:-50% 0; top:27%; width:58% }
.scene__media[data-crop="dev-bl"]{ left:-17%; top:25%; width:104% }
/* Un móvil es estrecho y alto: se planta más abajo y ocupa menos ancho, para
   que se salga por abajo y no por el costado. */
.scene__media[data-crop="dev-movil"]{ left:11%; top:30%; width:30% }
.scene__media[data-crop="dev-movil-c"]{ left:50%; translate:-50% 0; top:30%; width:31% }

/* — `.scene__media--fijo`: cuando lo de dentro necesita saber cuánto sobra —
   El párrafo de arriba lo dice: el alto de un `dev-*` no se fija, lo pone la
   proporción del aparato, y lo que no cabe lo recorta el borde de LA ESCENA,
   no el de `.scene__media`. Eso es invisible mientras nada de dentro necesite
   medir «cuánto queda» — todas las escenas de antes sólo maquetaban una
   pantalla ya resuelta—. El informe de `/ia/` (30 sep 2026) es la primera que
   necesita saberlo de verdad: `.app__iascroll` (`ds/app.css`) calcula cuánto
   desplazarse con `cqh`, y `cqh` mide el contenedor de tamaño más cercano —que,
   sin este modificador, es el propio `.app__iascroll`, que por `flex:1` mide
   el alto ENTERO del aparato, no el que de verdad se ve—. Con
   `.app` recortando a los 433-454px del aparato y `.scene` recortando a los
   340-398px que de verdad se enseñan, el informe se creía con 80px más de
   sitio de los que tiene, y el cierre —compartir/descargar— se salía por el
   borde de la escena sin que `.app__iascroll` lo supiera (comprobado con
   Puppeteer: `.app` medía más que `.scene` en las nueve anchuras de prueba).

   El arreglo no toca la regla de arriba —160 escenas dependen de que el
   aparato no fije su alto—: es un modificador que sólo se pone donde hace
   falta. `height:75%` es el `100% - top:25%` de `dev-br` sin `data-span`,
   que es el único crop que usa esta escena y no cambia con el ancho —no hay
   ninguna media query que toque `top:25%` para una escena sin `data-span`—;
   si esta pieza se copia a una escena con otro `data-crop` o `data-span`, el
   75% hay que rehacerlo con el `top` de esa regla. Con el contenedor ya del
   alto real, `.device` (hijo directo, `ds/device.css`) se limita a esos
   mismos `100cqh`: dentro de esa escena, el aparato deja de fingir que mide
   lo que su proporción diría y pasa a medir lo que la escena de verdad
   enseña — mismo `overflow:hidden` que ya tenía, sólo que ahora recorta lo
   que sobra en el sitio correcto. */
.scene__media--fijo{ height:75%; container-type:size }
/* `max-height` no bastaba: el alto de `.device` es `auto` —lo pone su
   contenido, `.device__screen`—, y un `auto` capado con `max-height` no
   cuenta como «definido» para que un hijo resuelva un `height:100%` contra
   él —es la misma regla por la que un `%` de alto no funciona dentro de un
   contenedor sin alto propio—. `height:100cqh` a secas sí es un alto fijo
   de verdad, y con uno fijo el `100%` de `.device__screen` ya tiene contra
   qué resolverse. */
.scene__media--fijo > .device{ height:100cqh }
/* Y hace falta en los dos niveles: `.device__screen` (`ds/device.css`) mide
   su propio alto por `aspect-ratio`, a partir de SU ancho —no del de
   `.device`—, así que sin esto seguía saliendo a los 433-454px de siempre
   por dentro, aunque `.device` ya midiera menos por fuera. `height:100%`
   sobre un alto ya definido gana a `aspect-ratio` —CSS sólo usa la
   proporción para la dimensión que falta—, así que `.app` (que llena
   `.device__screen`) pasa a medir de verdad los mismos `100cqh`. */
.scene__media--fijo .device__screen{ height:100% }

/* ---------------------------------------------------------
   FRAGMENTO COMPUESTO
   Dos fragmentos en la misma escena, uno delante del otro: la ventana de
   escritorio detrás y el móvil delante. Es el patrón de la home de Stripe, y
   sirve para lo que un fragmento solo no puede decir — que la misma cosa se
   hace desde el puesto y desde la planta.

   Sigue siendo UNA escena: un tinte, un título, un recorte. Lo que cambia es
   que hay dos capas. Tres ya no; con tres no se lee ninguna.

   El de delante lleva más sombra que el de detrás. Sin esa diferencia las dos
   capas se leen como una sola imagen pegada y el gesto se pierde.

     <div class="scene__media" data-crop="comp-back">  …ventana… </div>
     <div class="scene__media" data-crop="comp-front"> …móvil…   </div>
   --------------------------------------------------------- */
.scene__media[data-crop^="comp-"]{ right:auto; bottom:auto }
.scene__media[data-crop="comp-back"]{
  left:26%; top:20%; width:92%;
  z-index:1;
}
.scene__media[data-crop="comp-front"]{
  left:5%; top:36%; width:25%;
  z-index:2;
  box-shadow:rgba(12,32,64,.16) 0 16px 40px;
}

/* En la escena ancha el título baja al centro del tercio izquierdo, así que
   las dos capas se corren a la derecha para no pisarlo. Es el mismo ajuste que
   ya tienen br, r y dev-br unas líneas más arriba. */
.scene[data-span="12"] .scene__media[data-crop="comp-back"]{ left:52%; top:12%; width:62% }
.scene[data-span="12"] .scene__media[data-crop="comp-front"]{ left:37%; top:26%; width:15% }

@media (max-width:900px){
  /* A una columna la composición se estrecha: el móvil se sale por la
     izquierda y la ventana pierde el arranque, que es lo prescindible.
     La ancha deja de serlo aquí —el título vuelve arriba—, así que sus dos
     capas se reescriben con los mismos valores en vez de "resetearse": una
     regla con data-span="12" gana a la suelta y dejaría el encuadre a medias. */
  .scene__media[data-crop="comp-back"],
  .scene[data-span="12"] .scene__media[data-crop="comp-back"]{ left:30%; top:22%; width:100% }
  .scene__media[data-crop="comp-front"],
  .scene[data-span="12"] .scene__media[data-crop="comp-front"]{ left:2%; top:38%; width:30% }
}
@media (max-width:600px){
  /* En teléfono no caben dos capas legibles. Se queda la de delante, que es la
     que argumenta, y la de detrás desaparece: enseñar menos antes que enseñar
     ruido. */
  .scene__media[data-crop="comp-back"]{ display:none }
  .scene__media[data-crop="comp-front"],
  .scene[data-span="12"] .scene__media[data-crop="comp-front"]{ left:8%; top:32%; width:52% }
}

/* ---------------------------------------------------------
   HALO DEL BORDE
   Un círculo de color que sigue al cursor por el borde de la tarjeta. Es el
   único movimiento del componente y es el de Stripe, medido en su home: un
   radial-gradient de 1,4 veces el ancho, al 50 % de opacidad, que persigue al
   ratón con transform y un easing largo (1s, easeOutExpo). El retraso es lo
   que lo hace elegante: si va pegado al cursor parece un puntero, no una luz.

   Nada se mueve solo. En la home de Stripe no hay una sola animación en bucle
   dentro de estas tarjetas — 76 elementos con transición y cero keyframes—:
   el movimiento RESPONDE, no entretiene. Es la diferencia entre un producto
   serio y una landing.

   El color sale del módulo, como el tinte: lo pone --scene-glow desde
   ds/tint.css. No hay forma de darle un color a mano.
   --------------------------------------------------------- */
.scene__glow{
  position:absolute;
  inset:0;
  z-index:0;
  border-radius:inherit;
  pointer-events:none;
  opacity:0;
  transition:opacity .25s linear;
  /* Sólo se ve en el perímetro: dos máscaras, una recortada a la caja de
     contenido y otra a la caja entera, y se restan. El compuesto va en su
     propia propiedad — dentro del atajo `mask` no es válido y la máscara se
     ignora entera, con lo que el halo llena la tarjeta en vez de quedarse en
     el canto. */
  padding:1px;
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;          /* el nombre viejo del mismo compuesto */
          mask-composite:exclude;
  overflow:hidden;
}
.scene__glow::before{
  content:"";
  position:absolute;
  top:0; left:0;
  width:140%; aspect-ratio:1;
  translate:calc(var(--glow-x,50%) - 50%) calc(var(--glow-y,50%) - 50%);
  background:radial-gradient(circle,
    var(--scene-glow,var(--color-primary)) 0%,
    rgba(211,79,157,.55) 33%,
    var(--frame) 66%,
    transparent 70%);
  transition:translate 1s cubic-bezier(.16,1,.3,1);
}
.scene:hover .scene__glow,
.scene:focus-visible .scene__glow{ opacity:.55 }

@media (prefers-reduced-motion:reduce){
  .scene__glow::before{ transition:none }
}

/* ---------------------------------------------------------
   EXPANDIR
   Sólo se pinta si expande de verdad. Una afordancia que no hace nada cuesta
   más confianza de la que gana, así que el marcado exige el destino:
   .scene__expand sin [data-expand-target] lo canta el validador y no se pinta.
   --------------------------------------------------------- */
.scene__expand{
  position:absolute;
  top:14px; right:14px;
  z-index:3;
  width:32px; height:32px;
  display:none;                     /* por defecto no existe */
  place-items:center;
  padding:0;
  border:1px solid var(--color-hairline);
  border-radius:var(--ds-radius-control);
  background:rgba(255,255,255,.72);
  backdrop-filter:blur(6px);
  color:var(--ds-text-2);
  cursor:pointer;
  transition:border-color var(--ds-motion), background var(--ds-motion);
}
.scene__expand[data-expand-target]{ display:grid }
.scene__expand:hover,
.scene:hover .scene__expand{ border-color:var(--color-hairline-input); background:#fff }
/* Con la tarjeta entera pulsable, la afordancia se adelanta un pelo al pasar
   por encima: es lo que dice "esto se abre" antes de que nadie lo pruebe. */
.scene:hover .scene__expand{ translate:0 -1px }
.scene[data-module="ia"] .scene__expand{
  border-color:#3C424A; background:rgba(17,18,20,.6); color:#C9CFD6;
}
.scene[data-module="ia"] .scene__expand:hover{ border-color:#555C65; background:#1B2026 }
.scene__expand svg{ width:16px; height:16px; display:block }

/* ---------------------------------------------------------
   RESPONSIVE
   Por debajo de tablet la rejilla de 12 no aporta nada: una escena por fila,
   y la proporción se relaja para que el fragmento siga siendo legible (la
   regla de los 10px reales manda sobre la de la proporción).
   --------------------------------------------------------- */
@media (max-width:900px){
  .scene-grid{ grid-template-columns:repeat(6,1fr) }
  .scene[data-span]{ grid-column:span 6 }
  .scene[data-ratio="21:9"]{ aspect-ratio:16/9 }
  /* Aquí ya no hay filas mixtas —una escena por fila—, así que `fila` se queda
     sin vecina de la que tomar el alto y la caja colapsa a dos píxeles. A lo
     ancho de la columna, la proporción que toca es la misma a la que se relaja
     todo lo demás en teléfono. */
  .scene[data-ratio="fila"]{ aspect-ratio:4/3 }
}
@media (max-width:600px){
  .scene-grid{ grid-template-columns:1fr; gap:16px }
  .scene[data-span]{ grid-column:auto }
  /* 4:3 SE QUEDABA CORTO (14 sep 2026, segunda vuelta). A una columna la
     escena mide el ancho de la pantalla —358px en un iPhone de gama media—
     y a 4:3 eso da 268px de alto. El título ocupa hasta el 63 (dos líneas,
     `top:20px` fijo) y el primer encuadre del fragmento (`top:25-26%`)
     empezaba a los 67-70px: cuatro píxeles de margen, y con un título de una
     palabra más en otro idioma, ninguno — el cliente lo vio solapado. El
     cuadrado da un tercio más de alto y separa las dos cosas de verdad; de
     paso el fragmento recortado se ve más entero, que era la otra queja
     («se corta»). */
  .scene[data-ratio="4:3"],
  .scene[data-ratio="1:1"],
  .scene[data-ratio="21:9"],
  .scene[data-ratio="fila"]{ aspect-ratio:1/1 }
}

/* ---------------------------------------------------------
   CARRUSEL EN MÓVIL — modificador `.scene-grid--carrusel`
   Sólo lo lleva la rejilla de escenas de la home (petición del cliente, 14
   sep 2026): ahí las seis tarjetas en columna hacían la página kilométrica.
   En carril se llegan igual —deslizando— y cada una se ve entera en vez de
   a media pantalla al hacer scroll. El resto de páginas con `.scene-grid`
   (incidencias, activos, las de industria…) NO lo llevan: para ellas basta
   el cuadrado de arriba.

   Sin JS: scroll nativo con scroll-snap, como el carril recortado de
   ds/sections.js pero sin flechas —aquí no hacen falta, es táctil, y el
   propio componente ya dice «en táctil se arrastra»—. No se reutiliza
   `.rail` tal cual porque ésa es carril también en escritorio; aquí la
   rejilla de la home tiene que seguir siendo rejilla por encima de 600px.
   --------------------------------------------------------- */
@media (max-width:600px){
  .scene-grid--carrusel{
    display:grid;
    /* `.scene-grid` de base declara `grid-template-columns:1fr` a este ancho
       (la regla de arriba, para cuando NO es carrusel). Dejarla puesta aquí
       competía con las columnas implícitas de abajo: un `1fr` explícito se
       reparte lo que quede DESPUÉS de las `86%` fijas, y cinco columnas al
       86% ya suman más que el contenedor — al primer hijo no le tocaba casi
       nada (2px de ancho). `none` deja las seis columnas por igual, todas
       implícitas. */
    grid-template-columns:none;
    grid-auto-flow:column;
    grid-auto-columns:86%;
    gap:14px;
    overflow-x:auto;
    overscroll-behavior-x:contain;
    scroll-snap-type:x mandatory;
    scrollbar-width:none;
    padding-bottom:6px;
  }
  .scene-grid--carrusel::-webkit-scrollbar{ display:none }
  .scene-grid--carrusel > .scene[data-span]{ grid-column:auto; scroll-snap-align:start }
}

/* El móvil es el encuadre que peor aguanta el estrechamiento: su ancho es un
   porcentaje de la tarjeta, así que en un teléfono el aparato acaba midiendo
   cien píxeles y dentro no cabe nada legible. Se le va dando más porcentaje a
   medida que la tarjeta encoge, para que la pantalla siga midiendo parecido en
   píxeles reales — que es lo que manda la regla de los 10px. */
@media (max-width:900px){
  .scene__media[data-crop="dev-movil"]{ width:34% }
}
@media (max-width:600px){
  .scene__media[data-crop="dev-movil"]{ left:8%; width:44% }
}
@media (max-width:430px){
  .scene__media[data-crop="dev-movil"]{ width:56% }
}

@media (prefers-reduced-motion:reduce){
  .scene,.scene__expand{ transition:none }
}

/* =========================================================
   LIENZO DE FOTO — .scene[data-canvas="foto"]
   =========================================================
   Variante de lienzo, no un componente nuevo: la escena sigue siendo el mismo
   marco, el mismo título y el mismo fragmento cortado por el borde. Lo único
   que cambia es el suelo — en vez de una superficie teñida, una fotografía de
   alguien usando móvil, tablet, portátil o el panel de una máquina en planta o
   en almacén.

   Por qué existe: el lienzo plano dice "esto es software". La foto dice quién
   lo usa y dónde, que es la mitad del argumento en un sitio donde el rival no
   es otro software sino el Excel, el papel y el grupo de WhatsApp. Encima de
   la foto siguen flotando las piezas de producto (ds/app.css) o, cuando la
   sección no enseña pantalla, un recurso: la tarjeta que comunica la acción
   —parte creado, acción asignada, registro firmado, respuesta de la IA—.

   Lo que NO cambia:
   · El tinte lo sigue dictando data-module, con los mismos seis colores. Sobre
     foto usan --tint-foto-* (ds/tint.css), que es el mismo color con más
     cuerpo, porque al 10% sobre una fotografía el color no significa nada.
   · Sigue sin haber forma de pasar un color a mano.
   · El fragmento sigue saliéndose por al menos un borde.

   Lo que sí cambia: el título pasa a blanco, porque debajo hay una foto y
   ninguna foto garantiza contraste para tinta oscura. El velo que lo permite
   es neutro, no de módulo: si el velo tuviera color, se sumaría al tinte y
   cada escena tendría un color distinto del que le toca.

   Las fotos viven en assets/planta/ y su procedencia está en
   assets/planta/FUENTES.md. Criterio para añadir una: una persona usando un
   dispositivo en un entorno industrial. Una nave vacía no vale.

   Marcado:

     <article class="scene" data-module="incidencias" data-span="6" data-ratio="1:1"
              data-canvas="foto">
       <img class="scene__foto" src="/assets/planta/…webp" alt="" width="1100"
            height="1100" loading="lazy" decoding="async">
       <span class="scene__velo" aria-hidden="true"></span>
       <h3 class="scene__title">…</h3>
       <div class="scene__media" data-crop="br"> …pantalla o recurso… </div>
     </article>
   --------------------------------------------------------- */

.scene[data-canvas="foto"]{
  /* Suelo bajo la foto: si la imagen tarda o falla, la escena sigue siendo una
     tarjeta oscura legible y no un rectángulo blanco con texto blanco. */
  background:var(--color-graphite-900, #111214);
}

.scene__foto{
  position:absolute;
  inset:0;
  z-index:0;
  width:100%;
  height:100%;
  object-fit:cover;
  display:block;
}

/* Zum lento y continuo —"Ken Burns"—, para que la foto se lea como algo
   filmado y no como una imagen pegada. Nunca pasa de un 6% de escala, que es
   lo que cabe sin desencuadrar el fragmento de producto que flota encima;
   obedece `--play` como el resto de animaciones de esta hoja —se para fuera
   de pantalla— y se apaga con movimiento reducido, que es cuando una foto
   quieta es lo correcto. */
.scene__foto{
  animation:scene-foto-zoom 22s ease-in-out infinite alternate;
  animation-play-state:var(--play,running);
  will-change:transform;
}
@keyframes scene-foto-zoom{
  from{ transform:scale(1) }
  to{ transform:scale(1.06) }
}
@media (prefers-reduced-motion:reduce){
  .scene__foto{ animation:none }
}

/* Velo neutro. Dos gradientes: uno de arriba, que es donde vive el título, y
   otro general que baja el contraste de la foto para que el fragmento de
   producto —blanco, con texto de 10px— se despegue de ella. */
.scene__velo{
  position:absolute;
  inset:0;
  z-index:1;
  pointer-events:none;
  background:
    linear-gradient(180deg, rgba(11,18,32,.74) 0%, rgba(11,18,32,.30) 30%, rgba(11,18,32,.16) 60%, rgba(11,18,32,.46) 100%);
}

/* El tinte del módulo pasa por delante de la foto y por delante del velo: es
   la señal, y una señal debajo de un velo neutro deja de serlo. */
.scene[data-canvas="foto"]::before{ z-index:1 }
.scene[data-canvas="foto"][data-module="incidencias"]::before{ background-image:var(--tint-foto-incidencias) }
.scene[data-canvas="foto"][data-module="acciones"]::before{    background-image:var(--tint-foto-acciones) }
.scene[data-canvas="foto"][data-module="checklists"]::before{  background-image:var(--tint-foto-checklists) }
.scene[data-canvas="foto"][data-module="activos"]::before{     background-image:var(--tint-foto-activos) }
.scene[data-canvas="foto"][data-module="kpis"]::before{        background-image:var(--tint-foto-kpis) }
.scene[data-canvas="foto"][data-module="ia"]::before{          background-image:var(--tint-foto-ia) }

/* Reparto de capas dentro de la variante. Mismo orden de siempre, subido uno
   para dejar sitio a la foto y al velo. */
.scene[data-canvas="foto"] .scene__media{ z-index:2 }
.scene[data-canvas="foto"] .scene__title{ z-index:3; color:#fff }
.scene[data-canvas="foto"] .scene__expand{
  z-index:4;
  border-color:rgba(255,255,255,.28);
  background:rgba(11,18,32,.44);
  color:#fff;
}
.scene[data-canvas="foto"]:hover .scene__expand,
.scene[data-canvas="foto"] .scene__expand:hover{
  border-color:rgba(255,255,255,.5);
  background:rgba(11,18,32,.62);
}

/* Variante "todavía no hay foto" para este lienzo — mismo criterio que
   .stage--hueco (ds/sections.css): el hueco de imagen (patrón 6) ocupa el
   sitio de `.scene__foto`, SIN `.scene__velo` en el marcado mientras no hay
   foto —ese velo existe para que el fragmento se despegue de una fotografía
   real, y sobre el hueco a rayas sólo ensucia el prompt—. A diferencia de
   donde vive `.img-slot` normalmente —sobre papel—, aquí el lienzo es
   oscuro (`.scene[data-canvas="foto"]` ya lo declara de reserva para cuando
   la foto tarda o falla), así que el hueco invierte sus tintas: borde y
   texto claros, para seguir siendo legible. El tinte del módulo sigue
   pintándose encima igual que con una foto real. */
.scene[data-canvas="foto"] .img-slot{
  position:absolute; inset:0; z-index:0;
  aspect-ratio:auto; border-radius:0;
  border-color:rgba(255,255,255,.22);
  background:
    repeating-linear-gradient(45deg,
      rgba(255,255,255,.05) 0 10px,
      rgba(255,255,255,.1) 10px 20px);
}
.scene[data-canvas="foto"] .img-slot__tag{
  color:rgba(255,255,255,.75);
  border-color:rgba(255,255,255,.3);
  background:rgba(11,18,32,.5);
}
.scene[data-canvas="foto"] .img-slot__prompt{ color:rgba(255,255,255,.82) }
.scene[data-canvas="foto"] .img-slot__prompt b{ color:#fff }

/* =========================================================
   RECURSO — .res
   =========================================================
   La pieza que comunica la acción cuando la escena no enseña una pantalla
   entera: el parte que se acaba de crear, la acción con su plazo, el registro
   firmado, la respuesta de la IA citando el parte del que sale.

   Va dentro de un .scene__media, así que hereda su recorte y su sombra. Es
   producto, no adorno: los datos son los del tour de /incidencias/, y un
   identificador que sale en dos sitios es la misma avería.

   El color del icono lo pone la escena, no el recurso: --res-acento cae por
   herencia desde .scene[data-module], igual que el tinte.
   --------------------------------------------------------- */

.scene[data-module="incidencias"]{ --res-acento:var(--module-incidencias) }
.scene[data-module="acciones"]   { --res-acento:var(--module-acciones) }
.scene[data-module="checklists"] { --res-acento:var(--module-registros) }
.scene[data-module="activos"]    { --res-acento:var(--module-activos) }
.scene[data-module="kpis"]       { --res-acento:var(--module-kpis) }
.scene[data-module="ia"]         { --res-acento:var(--module-ia) }

.res{
  background:#fff;
  border-radius:var(--ds-radius-card, 12px);
  padding:16px 18px;
  color:var(--color-ink);
  text-align:left;
}
.res__hd{ display:flex; align-items:center; gap:10px }
.res__ico{
  flex:none;
  width:30px; height:30px;
  border-radius:9px;
  display:grid; place-items:center;
  background:var(--res-acento, var(--color-primary));
  color:#fff;
}
.res__ico svg{ width:16px; height:16px; display:block }
.res__t{
  margin:0;
  font:var(--font-weight-medium, 500) 14px/1.25 var(--font-sans);
  letter-spacing:-.01em;
}
.res__s{
  margin:2px 0 0;
  font:var(--type-body-sm, 400 13px/1.4 var(--font-sans));
  color:var(--color-ink-mute);
}
.res__rows{ margin-top:12px }
.res__kv{
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:8px 0;
  border-top:1px solid var(--color-hairline);
  font-size:13px;
}
.res__kv dt{ margin:0; color:var(--color-ink-mute) }
.res__kv dd{ margin:0; color:var(--color-ink) }

/* =========================================================
   .res SOBRE FOTO — vidrio, nunca blanco sólido
   =========================================================
   Regla del sistema (1 oct 2026, petición del cliente: «no quiero elementos
   blancos planos; o bocadillos con fondo cristal o interfaz realista»):
   dentro de un lienzo de foto (.scene[data-canvas="foto"]) un `.res` opaco se
   lee como una pegatina pegada encima de la fotografía, no como parte de
   ella. Mismo diagnóstico que ya resolvió `.ntf` más abajo en este fichero
   («sin copiar su tarjeta blanca y plana... el fondo es el ESTILO HIELO»):
   el recurso pasa a vidrio esmerilado, el mismo `.compo--hielo` de
   /gestion-de-activos/. Fuera de un lienzo de foto —sobre el tinte plano de
   módulo— el `.res` sigue blanco sólido, que es donde sí lee como tarjeta.
   --------------------------------------------------------- */
.scene[data-canvas="foto"] .res{
  background:rgba(255,255,255,.46);
  border:1px solid rgba(255,255,255,.5);
  -webkit-backdrop-filter:blur(22px) saturate(1.3);
          backdrop-filter:blur(22px) saturate(1.3);
  box-shadow:0 20px 44px rgba(12,16,24,.28), inset 0 1px 0 rgba(255,255,255,.65);
}
.scene[data-canvas="foto"] .res__kv{ border-top-color:rgba(255,255,255,.4) }
.scene[data-canvas="foto"] .res__s{ color:var(--color-ink) }

/* =========================================================
   LAS FILAS DE UN .res RELLENÁNDOSE — .res__rows--fill
   Reutilizable: la escena de la incidencia (3 filas, detrás de la lectura
   del QR) y la de la acción (4 filas, detrás de la creación de la tarea) la
   comparten. Cada fila sale escondida DE VERDAD —opacity:0 puesto en la
   propia regla, no sólo en el fotograma 0%, que es el fallo de siempre
   cuando se usa animation-delay (ver ds/app.css, la ronda de OK/KO)— y
   entra, se queda y se apaga, una detrás de otra sobre un reloj de 8 s.
   Hasta seis filas (.res__rows--fila añade la cuarta para las tarjetas de
   cuatro).
   ========================================================= */
.res__rows--fill .res__kv{
  opacity:0;
  animation:res-row-in 8s ease-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
.res__rows--fill .res__kv:nth-child(1){ animation-delay:0s }
.res__rows--fill .res__kv:nth-child(2){ animation-delay:.5s }
.res__rows--fill .res__kv:nth-child(3){ animation-delay:1s }
.res__rows--fill.res__rows--fila .res__kv:nth-child(4){ animation-delay:1.5s }
@keyframes res-row-in{
  0%,4%  { opacity:0; translate:0 4px }
  10%,88%{ opacity:1; translate:0 0 }
  96%,100%{ opacity:0; translate:0 4px }
}

/* =========================================================
   QR ESCANEADO → LA FICHA EN EL MÓVIL — .qrscan / .qrficha
   Segunda vuelta del cliente, 29 sep 2026: el visor de cámara de la tanda
   anterior (`.qrscan-phone`, retirado) «no quedaba bien». Lo que pedía era
   más simple y más elocuente a la vez —un QR que se marca como leído y
   ENTONCES la ficha real, en el móvil, con la foto del activo, su código y
   sus incidencias relacionadas—, así que la pieza pasa a ser dos actos con
   el mismo cruce de opacidad que `.planalt`, más abajo:

     Acto A — el QR con la mira (.qrscan, se conserva igual) y, cuando la
     mira termina su barrida, el aviso «leído» (.qrscan__ok, un check).
     Acto B — la ficha del activo, en `.app--movil` + `.app--ficha` —las
     mismas piezas de la composición de portada de esta página, mismo
     aviso: la foto y el listado son ficha simplificada, a petición
     expresa del cliente, ver el aviso de CLAUDE.md—.

   El QR es el mismo SVG de la ficha de arriba (/assets/activos/qr-activo.svg):
   mismo activo, mismo código, no un dibujo nuevo con pinta de QR. La mira
   barre una vez por reloj de 8 s y se apaga; no es un escáner de
   supermercado en bucle continuo, es UNA lectura seguida de UNA ficha.

   El color de la mira lo pone la escena (--res-acento), igual que el icono
   del recurso: no es un adorno suelto, es la misma regla de tinte por
   módulo que gobierna el resto de la pieza.

   TERCERA VUELTA, 29 sep 2026: el `.device` de iPhone se retira de esta
   escena, a petición del cliente —«no hace falta el borde del móvil, lo
   importante es que se vea bien la pantalla»—. Sin bisel, la pantalla
   entra en `.qrficha__screen`, un panel suelto (mismo criterio que
   `.scene__media--suelto`: sombra propia, sin marco de aparato) que ya no
   paga los ~50px de bisel+isla del `.device`, así que crece y llena mejor
   la tarjeta. Dentro se reutilizan `.app`/`.app--movil`/`.app--ficha` tal
   cual —nada de marcado nuevo para la app, sólo lo que la envuelve—.
   ========================================================= */
.qrscan{ position:relative; width:132px; height:132px; flex:0 0 auto }
.qrscan img{ width:100%; height:100%; display:block; border-radius:12px; background:#fff; padding:10px; box-sizing:border-box }
.qrscan__corner{ position:absolute; width:26px; height:26px; border:3px solid var(--res-acento, var(--color-primary)) }
.qrscan__corner--tl{ top:-8px; left:-8px; border-right:0; border-bottom:0; border-radius:6px 0 0 0 }
.qrscan__corner--tr{ top:-8px; right:-8px; border-left:0; border-bottom:0; border-radius:0 6px 0 0 }
.qrscan__corner--bl{ bottom:-8px; left:-8px; border-right:0; border-top:0; border-radius:0 0 0 6px }
.qrscan__corner--br{ bottom:-8px; right:-8px; border-left:0; border-top:0; border-radius:0 0 6px 0 }
.qrscan__line{
  position:absolute; left:10px; right:10px; top:10px; height:3px; border-radius:1.5px;
  background:var(--res-acento, var(--color-primary));
  box-shadow:0 0 10px 1px var(--res-acento, var(--color-primary));
  opacity:0;
  animation:qrscan-sweep 8s ease-in-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes qrscan-sweep{
  0%     { top:10px; opacity:1 }
  30%    { top:calc(100% - 13px); opacity:1 }
  38%,100%{ opacity:0 }
}
/* El check de «leído»: nace cuando la mira ya ha barrido y se apaga, y se
   queda encendido el resto del acto A —el acto entero se apaga con
   `.qrficha__act--a`, no hace falta que el check se apague antes—. */
.qrscan__ok{
  position:absolute; right:-9px; bottom:-9px;
  width:34px; height:34px; border-radius:50%;
  background:var(--ok,#0E9F6E); color:#fff;
  display:grid; place-items:center;
  box-shadow:0 0 0 4px #fff;
  opacity:0; scale:.6;
  animation:qrscan-ok 8s ease-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
.qrscan__ok svg{ width:18px; height:18px }
@keyframes qrscan-ok{
  0%,32%  { opacity:0; scale:.6 }
  40%,94% { opacity:1; scale:1 }
  100%    { opacity:0; scale:.6 }
}

/* Los dos actos: MISMO cruce que `.planalt` (mismo reloj de 8 s, sin
   `animation-delay`, cada uno con su fotograma complementario del otro),
   pero en su propio espacio de nombres porque su caja mide distinto.
   Los dos van centrados en vertical sobre el mismo alto fijo —mismo
   arreglo que `.planalt`, más arriba en esta hoja—, así ninguno de los
   dos deja hueco vacío ni se sale por el borde que recorta
   `overflow:hidden`. **Si se toca el contenido de cualquiera de los dos
   actos, hay que volver a medir `min-height` y el alto de
   `.qrficha__screen` aquí abajo.** */
.qrficha{ position:relative; min-height:440px }
.qrficha__act--a,
.qrficha__act--b{
  position:absolute; inset:0;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:12px;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
.qrficha__act--a{ animation:planalt-a 8s ease-in-out infinite }
.qrficha__act--b{ opacity:0; animation:planalt-b 8s ease-in-out infinite }

/* La pantalla suelta: sin bisel y sin sombra —petición del cliente, 29 sep
   2026, tercera vuelta—, se apoya en el propio lienzo teñido en vez de
   despegarse de él. El alto es fijo (400px) para que `.app{height:100%}`
   tenga contra qué resolverse: sin marco de dispositivo nadie más se lo
   da. */
.qrficha__screen{
  position:relative;
  width:236px; height:400px;
  border-radius:16px;
  overflow:hidden;
  background:#fff;
}
.qrficha__screen .app{ border-radius:0 }

/* El scroll: `.app--ficha` (cabecera + lista) vive dentro de un visor de
   alto fijo con `overflow:hidden`; `.qrficha__scrollin` es lo que se
   desplaza. Empieza arriba, se enseñan las otras dos incidencias a medio
   ciclo y vuelve a subir antes de que el acto se apague —el mismo reloj
   de 8 s que `planalt-b`, para que el scroll nunca se vea a medias cuando
   la pantalla ya es visible del todo—. El desplazamiento (116px) está
   medido contra el alto real del contenido —457px de `.qrficha__scrollin`
   menos 341px de visor, comprobado con `javascript_tool` contra el DOM
   real—, no puesto a ojo: si se toca una fila de la lista, hay que volver
   a medirlo, o el scroll se para corto o enseña hueco en blanco al final. */
.qrficha__scroll{ flex:1 1 auto; min-height:0; overflow:hidden; position:relative }
.qrficha__scrollin{
  animation:qrficha-scroll 8s ease-in-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes qrficha-scroll{
  0%,58%  { translate:0 0 }
  74%,90% { translate:0 -116px }
  100%    { translate:0 0 }
}

/* =========================================================
   PRIMERO LA LISTA, DESPUÉS LA FICHA — .app__actswap / .app__actlist
   Petición del cliente, 29 sep 2026: que la escena de «La ficha del
   activo» no arranque ya con la ficha abierta, sino que se vea NAVEGAR —
   la pantalla de Activos, una fila que se marca y se abre en su ficha—.
   Mismo cruce de opacidad de dos actos que `.planalt` y `.qrficha`, más
   arriba en esta hoja (mismo reloj de 8 s, mismas `planalt-a`/`planalt-b`,
   reutilizadas tal cual): acto A la lista, acto B la ficha real que ya
   había —sin tocar su marcado—.

   Los dos actos viven dentro de `.app__main`, que ya mide lo que mide por
   ser `flex:1` de un `.app` con alto fijo (lo da `.device__screen`, con
   su `aspect-ratio`): no hace falta un `min-height` a mano como en
   `.planalt`/`.qrficha`, el padre ya está resuelto.

   ⚠️ LA LISTA NO ESTÁ COMPROBADA CONTRA LA APLICACIÓN. Lo único cierto
   —código fuente, 28 sep 2026— es que los activos agrupan por TIPO
   (maquinaria, instalaciones, equipos de medición) en lista plana, sin
   subcarpetas por área ni etiquetas propias: por eso esta lista agrupa
   bajo «Maquinaria», que es real, y no por área. TRA-003 y ENV-002 son
   los dos activos que ya existen en el resto del sitio; las otras dos
   filas (MEZ-001, HOR-002) son plausibles, no comprobadas, mismo criterio
   que el resto de datos «a petición expresa» de esta página —ver el
   aviso de CLAUDE.md—. */
.app__actswap{ position:relative; flex:1; min-height:0 }
.app__actswap__act{
  position:absolute; inset:0;
  display:flex; flex-direction:column; min-height:0;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
.app__actswap__act--a{ animation:planalt-a 8s ease-in-out infinite }
.app__actswap__act--b{ opacity:0; animation:planalt-b 8s ease-in-out infinite }

.app__actlist{ display:flex; flex-direction:column; gap:9px; min-height:0 }
.app__actlist .app__panel--list{ flex:1; min-height:0; overflow:hidden; display:flex; flex-direction:column }
/* `.app__table` trae `min-width:620px` de fábrica (pensado para el
   historial de escritorio de otras pantallas); en el móvil esta ventana
   mide bastante menos que eso, sidebar incluido. Se cancela igual que ya
   hace `.app--ficha .app__table{min-width:0}`, o la tabla se saldría del
   recorte del aparato sin que nadie lo viera hasta llegar a un móvil de
   verdad. */
.app__actlist .app__table{ margin-top:6px; min-width:0 }
.app__actrow td{ padding:7px 0; border-bottom:1px solid var(--app-line) }
.app__actrow:last-child td{ border-bottom:0 }
.app__actname{ font-size:11px; font-weight:500; color:var(--app-text) }
.app__actloc{ font-size:10px; color:var(--app-text-2); margin-top:1px }
.app__actstate{
  display:inline-flex; padding:2px 9px; border-radius:999px;
  background:#E4F6EA; color:#12813B; font-size:10px; font-weight:500;
}
.app__actchev{ width:13px; height:13px; color:var(--app-mute) }
/* La fila que se elige se marca ANTES de que el acto se apague —igual que
   la fila del responsable en `.app__asigna`—, para que se lea «esto es lo
   que se pincha» y no un color puesto porque sí. */
.app__actrow--sel td{
  animation:app-actrow-sel 8s ease-in-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes app-actrow-sel{
  0%,22%  { background:transparent }
  26%,40% { background:var(--app-blue-wash) }
  44%,100%{ background:transparent }
}

/* =========================================================
   DOS ACTOS PARA PROGRAMAR UN REGISTRO — .planalt
   Cruce de opacidad entre el aviso de que el preventivo ya está en el
   calendario y el diálogo real «Planificar registro» (fecha y hora),
   comprobado en demo.trysolved.com/checklist el 28 de septiembre de 2026
   —botón «Planificar para después», modal con dd/mm/aaaa + hora—, con
   Activo y Responsable añadidos a petición expresa. Un solo reloj de 8 s
   para los dos actos, sin animation-delay: cada uno lleva su propio
   fotograma de entrada y salida, complementario del otro, así que no hace
   falta desfasarlos a mano.

   PETICIÓN DEL CLIENTE, 29 sep 2026 (segunda vuelta): el acto A deja de
   ser la biblioteca de plantillas —«Selecciona una plantilla para
   planificar»— y pasa a ser el AVISO: «Registro planificado · TRA-003 ·
   Cinta L3 · hoy · 09:00», con la misma tarjeta `.res` que ya usaba antes
   (icono + título + subtítulo), sólo con el contenido cambiado. Primero
   se ve que el preventivo ya quedó agendado; después, el propio acto B
   enseña de qué campos sale ese aviso. La Hora del acto B pasa de 10:00 a
   09:00 y la Fecha de la cifra verificada (08/06/2026) a «Hoy», para que
   los dos actos cuenten la misma cita y no dos citas distintas. */
/* Los dos actos van ABSOLUTOS y centrados en vertical (`justify-content:
   center`), sobre el mismo `min-height` fijo: así los dos llenan la caja
   por igual —antes el acto A, más corto, dejaba hueco vacío debajo suyo
   dentro de los 252px que fija el acto B, que es el que mide más—. El
   acto A además centra en horizontal (`align-items:center`) para que la
   tarjeta del aviso no se estire a los 340px del recorte `res`; el acto B
   se queda con el `stretch` por defecto, que es lo que ya hacía que su
   `.res` ocupara el ancho entero. */
.planalt{ position:relative; min-height:252px }
.planalt__act--a,
.planalt__act--b{
  position:absolute; inset:0;
  display:flex; flex-direction:column; justify-content:center;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
.planalt__act--a{ align-items:center; animation:planalt-a 8s ease-in-out infinite }
.planalt__act--b{ opacity:0; animation:planalt-b 8s ease-in-out infinite }
@keyframes planalt-a{ 0%,42%{ opacity:1 } 50%,96%{ opacity:0 } 100%{ opacity:1 } }
@keyframes planalt-b{ 0%,42%{ opacity:0 } 50%,96%{ opacity:1 } 100%{ opacity:0 } }

.planalt__hint{ font-size:12px; color:var(--color-ink-mute); margin:0 0 8px }
.planalt__modalt{ margin:0; font:var(--font-weight-medium, 500) 14px/1.25 var(--font-sans) }
/* El diálogo real sólo tiene fecha y hora (comprobado en demo.trysolved.com/
   checklist el 28 de septiembre de 2026). Activo y Responsable se añaden aquí
   a petición expresa del cliente, 29 sep 2026, para que el registro se vea
   ASOCIADO a la máquina y con sus campos de preventivo, no sólo programado:
   mismo criterio que el campo Activo de la escena de acción, más abajo en
   esta página. Si algún día se decide que no vale, son dos celdas del grid. */
.planalt__fields{ display:grid; grid-template-columns:1fr 1fr; gap:8px 10px; margin-top:14px }
.planalt__fieldbox{ display:flex; flex-direction:column; gap:3px; min-width:0 }
.planalt__fieldk{ font-size:9.5px; color:var(--color-ink-mute) }
.planalt__field{
  padding:8px 10px;
  border:1px solid var(--color-hairline);
  border-radius:8px;
  font-size:12px;
  color:var(--color-ink);
  background:var(--color-surface, #fff);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.planalt__btn{
  display:block;
  margin-top:14px;
  padding:8px 14px;
  border-radius:100px;
  background:var(--res-acento, var(--color-primary));
  color:#fff;
  font-size:12px; font-weight:500;
  text-align:center;
}

/* =========================================================
   EL PREVENTIVO LLEGA COMO AVISO Y SE RELLENA — .regpush
   Quinta vuelta del cliente, 29 sep 2026: la notificación y la plantilla
   eran dos gestos distintos y se veían como uno —la banda vivía DENTRO de
   la pantalla del registro, tapándola—. Pidió separarlas: dos animaciones
   diferenciadas, una detrás de otra, no una encima de la otra.

   `.regpush` pasa a tener DOS ACTOS, con el mismo cruce de opacidad que
   `.planalt` (mismo reloj de 8 s, cada uno absoluto e `inset:0`, sin
   `animation-delay` en el cruce):

     Acto A — `.regpush__act--notif`. La notificación SOLA, sin ningún
     aparato alrededor: una tarjeta flotante con su propio marco y su
     propia sombra —ya no una banda pegada al canto de una pantalla—, con
     su toque (`.regpush__tap`).
     Acto B — `.regpush__act--reg`. El iPad SIN BISEL —«quita los bordes de
     la tablet»— con la plantilla dentro y sus controles rellenándose:
     `.regchk`, animación propia que NO reutiliza `app-elige`/`app-ok-in`/
     `app-firma-in` (los botones OK/KO de la home). El responsable es MARI
     MAR, no Javier —la única escena de la página donde aparecía su
     nombre—.

   `data-crop="b"` da la caja proporcional al contenedor —nada anclado a
   una esquina—, y con `--suelto` porque el `.device__screen` trae su
   propia sombra: un panel detrás habría vuelto a ser la «pantalla de
   fondo sin forma» que la regla del 9 de septiembre prohíbe.
   ========================================================= */
.regpush{ position:relative; min-height:360px }
.regpush__act{
  position:absolute; inset:0;
  display:flex; align-items:flex-start; justify-content:center;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
/* Arriba de la tarjeta, no centrados en vertical: petición del cliente de
   la vuelta anterior —la caja deja casi 500px y el aparato mide ~340, y
   centrado quedaba colgado en medio de un hueco enorme—. Sigue valiendo
   para los dos actos ahora que son dos. */
.regpush__act--notif{ padding-top:6%; animation:planalt-a 8s ease-in-out infinite }
.regpush__act--reg{ opacity:0; animation:planalt-b 8s ease-in-out infinite }

/* — ACTO A: LA NOTIFICACIÓN, SOLA —
   Tarjeta flotante —ancha, como una notificación push real, pero ya no
   pegada al canto de una pantalla—: cae con rebote, se lee y SE TOCA
   (`.regpush__tap`, un círculo que nace en el punto donde caería el dedo
   y crece mientras se apaga). El acto entero se apaga con `planalt-a`; la
   tarjeta no necesita su propia salida. */
.regpush__notif{
  position:relative;
  width:min(92%,380px);
  display:flex; align-items:center; gap:8px;
  background:#fff;
  border-radius:12px;
  box-shadow:0 16px 36px rgba(12,32,64,.24);
  padding:8px 14px;
  animation:regpush-notif 8s cubic-bezier(.32,1,.4,1) infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes regpush-notif{
  0%      { translate:0 -60px; opacity:0 }
  12%,32% { translate:0 0; opacity:1; scale:1 }
  36%     { scale:.95 }
  40%,100%{ translate:0 0; opacity:1; scale:1 }
}
.regpush__tap{
  position:absolute; right:16px; top:50%; translate:0 -50%;
  width:26px; height:26px; border-radius:50%;
  background:var(--app-blue);
  opacity:0; scale:.4;
  animation:regpush-tap 8s ease-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes regpush-tap{
  0%,33%  { opacity:0; scale:.4 }
  36%     { opacity:.4; scale:.6 }
  41%,100%{ opacity:0; scale:2.4 }
}
.regpush__ico{
  flex:none; width:22px; height:22px; border-radius:7px;
  display:grid; place-items:center;
  background:var(--app-blue-wash); color:var(--app-blue-deep);
}
.regpush__ico svg{ width:12px; height:12px }
.regpush__txt{ min-width:0 }
.regpush__tit{ font-size:9px; font-weight:500; color:var(--app-mute); line-height:1.2 }
.regpush__msg{ font-size:10.5px; font-weight:500; color:var(--app-text); margin-top:1px; line-height:1.2 }

/* — ACTO B: EL APARATO Y LA PLANTILLA —
   Sin bisel: la variable de `ds/device.css` se apaga en vez de tocar el
   componente compartido. Va contra `.device[data-device="ipad"]`, no sólo
   `.device` —`ds/device.css` carga DESPUÉS de esta hoja y con la misma
   especificidad gana el que va después—, para que el bisel no vuelva. El
   punto de cámara del iPad se apaga con él, y la sombra pasa a la
   pantalla, que es quien la necesita sin marco alrededor. */
.regpush .device{ width:min(98%,452px) }
.regpush .device[data-device="ipad"]{
  --device-bezel:0; --device-shell:transparent; --device-radius:16px;
}
.regpush .device[data-device="ipad"]::after{ display:none }
.regpush .device__screen{ border-radius:16px; box-shadow:0 20px 44px rgba(12,32,64,.22) }

/* Cinco controles reales no caben en el alto de un iPad a este ancho con el
   tamaño de fila por defecto —comprobado con `javascript_tool`: pedía 314px
   de pantalla y sólo hay 262—. Se aprieta la fila, no se quitan controles:
   quitar uno habría sido mentir sobre cuántos tiene el preventivo. */
.regpush .app__rec{ padding:8px 10px; gap:6px }
.regpush .app__rechead{ padding:6px 8px }

/* — LOS CONTROLES: ANIMACIÓN PROPIA, `.regchk` —
   No es `.app__items`/`.app__opt` (esa es la de la home): cada fila es un
   punto —vacío, con su borde— que se llena de color con un rebote de
   escala y dibuja su marca dentro, check o cruz según el color que lleve en
   `--c`. Una sola pareja de fotogramas (`regchk-pop`, `regchk-icon`) sirve
   para las dos, porque el color vive en la variable y no en la regla.

   Los retardos arrancan a los 4,1 s: el acto B no se ve hasta el 50 % del
   reloj (4 s, `planalt-b`), así que nada de esto puede empezar antes —si
   se toca el reparto de los dos actos, hay que volver a correr estos
   números—. */
.regchk{
  list-style:none; margin:0; padding:0;
  background:#fff; border:1px solid var(--app-line); border-radius:9px;
  overflow:hidden;
}
.regchk__row{
  display:flex; align-items:center; gap:9px;
  padding:7px 10px; border-bottom:1px solid var(--app-line);
  font-size:10.5px; color:var(--app-text);
}
.regchk__row:last-child{ border-bottom:0 }
.regchk__q{ flex:1; min-width:0 }
.regchk__code{
  flex:none; font-size:9.5px; font-weight:500; color:var(--app-blue);
  opacity:0;
  animation:regchk-code 8s ease-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes regchk-code{ 0%,10%{opacity:0} 16%,96%{opacity:1} 100%{opacity:0} }
.regchk__dot{
  --c:var(--app-ok);
  flex:none; width:19px; height:19px; border-radius:50%;
  border:2px solid var(--app-line-2,var(--app-line));
  background:#fff;
  display:grid; place-items:center;
  animation:regchk-pop 8s cubic-bezier(.34,1.56,.64,1) infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
.regchk__dot--ko{ --c:var(--app-crit,#B02020) }
@keyframes regchk-pop{
  0%,4%  { background:#fff; border-color:var(--app-line); scale:1 }
  6%     { scale:1.3 }
  9%,96% { background:var(--c); border-color:var(--c); scale:1 }
  100%   { background:#fff; border-color:var(--app-line); scale:1 }
}
.regchk__mark{
  width:10px; height:10px; color:#fff;
  opacity:0; scale:.3;
  animation:regchk-icon 8s ease-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes regchk-icon{
  0%,8%  { opacity:0; scale:.3 }
  12%,96%{ opacity:1; scale:1 }
  100%   { opacity:0; scale:.3 }
}
.regchk__row:nth-child(1) .regchk__dot,
.regchk__row:nth-child(1) .regchk__mark,
.regchk__row:nth-child(1) .regchk__code{ animation-delay:4.1s }
.regchk__row:nth-child(2) .regchk__dot,
.regchk__row:nth-child(2) .regchk__mark,
.regchk__row:nth-child(2) .regchk__code{ animation-delay:4.4s }
.regchk__row:nth-child(3) .regchk__dot,
.regchk__row:nth-child(3) .regchk__mark,
.regchk__row:nth-child(3) .regchk__code{ animation-delay:4.7s }
.regchk__row:nth-child(4) .regchk__dot,
.regchk__row:nth-child(4) .regchk__mark,
.regchk__row:nth-child(4) .regchk__code{ animation-delay:5.0s }
.regchk__row:nth-child(5) .regchk__dot,
.regchk__row:nth-child(5) .regchk__mark,
.regchk__row:nth-child(5) .regchk__code{ animation-delay:5.3s }

/* La barra de progreso: sube un escalón con cada control, no en rampa
   continua —es lo que se ve al responder uno a uno, no una carga—. */
.regprog{ height:4px; border-radius:2px; background:var(--app-line); overflow:hidden }
.regprog__fill{
  display:block; height:100%; width:0%; border-radius:inherit;
  background:var(--app-blue);
  animation:regprog-fill 8s steps(1,end) infinite;
  animation-delay:4s;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes regprog-fill{
  0%      { width:0% }
  8%      { width:20% }
  16%     { width:40% }
  24%     { width:60% }
  32%     { width:80% }
  40%,100%{ width:100% }
}

.regpush .app__sign{
  opacity:0; translate:0 6px;
  animation:regchk-firma 8s cubic-bezier(.16,1,.3,1) infinite;
  animation-delay:5.7s;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes regchk-firma{
  0%      { opacity:0; translate:0 6px }
  10%,88% { opacity:1; translate:0 0 }
  100%    { opacity:0; translate:0 6px }
}

@media (prefers-reduced-motion:reduce){
  .regpush__act--notif{ animation:none; opacity:0 }
  .regpush__act--reg{ animation:none; opacity:1 }
  .regpush__notif{ animation:none; translate:0 0; opacity:1 }
  .regpush__tap{ animation:none; opacity:0 }
  .regchk__dot{ animation:none; background:var(--c); border-color:var(--c) }
  .regchk__mark{ animation:none; opacity:1; scale:1 }
  .regchk__code{ animation:none; opacity:1 }
  .regprog__fill{ animation:none; width:100% }
  .regpush .app__sign{ animation:none; opacity:1; translate:0 0 }
}

/* MISMO ESTADO, PARA CUANDO UNA CAPACIDAD DEL ROTADOR YA TERMINÓ SU VUELTA
   ÚNICA. `ds/rotador.js` pone `data-terminado` en el instante exacto en que
   el reloj de la capacidad (`data-dur`) se cumple — no antes, para no cortar
   el gesto, no después, para que no se vea el reinicio del bucle que estas
   piezas llevan pensado para encadenar consigo mismas, no para pararse—.
   Son las MISMAS reglas de arriba, con el mismo argumento: el fotograma del
   0 % y el del 100 % de un bucle infinito son casi iguales a propósito —para
   que la vuelta no se note—, y por eso «se acabó, para» no puede ser
   `animation-fill-mode:forwards` sobre el 100 %: eso dejaría, por ejemplo, el
   check de «leído» otra vez apagado. Lo resuelto es el pico de la historia,
   no su costura de bucle, y ese pico es justo lo que ya describe el bloque
   de movimiento reducido de arriba. */
.scene[data-terminado] .regpush__act--notif{ animation:none; opacity:0 }
.scene[data-terminado] .regpush__act--reg{ animation:none; opacity:1 }
.scene[data-terminado] .regpush__notif{ animation:none; translate:0 0; opacity:1 }
.scene[data-terminado] .regpush__tap{ animation:none; opacity:0 }
.scene[data-terminado] .regchk__dot{ animation:none; background:var(--c); border-color:var(--c) }
.scene[data-terminado] .regchk__mark{ animation:none; opacity:1; scale:1 }
.scene[data-terminado] .regchk__code{ animation:none; opacity:1 }
.scene[data-terminado] .regprog__fill{ animation:none; width:100% }
.scene[data-terminado] .regpush .app__sign{ animation:none; opacity:1; translate:0 0 }

/* =========================================================
   EL ESTADO DE LA ACCIÓN, DE «EN CURSO» A «CERRADA» — .res__swap
   Última fila en aparecer sobre .res__rows--fill: primero se crea la tarea
   —activo, responsable, fecha de cierre—, y sólo entonces, más tarde, pasa
   a cerrada.
   ========================================================= */
.res__swap{ position:relative; display:inline-block; min-width:60px; height:1.3em; text-align:right }
.res__swap-a,.res__swap-b{ position:absolute; right:0; top:0; white-space:nowrap }
.res__swap-a{ animation:res-swap-out 8s ease-in-out infinite; animation-play-state:var(--play,running); animation-iteration-count:var(--iter,infinite); animation-fill-mode:forwards; }
.res__swap-b{
  color:var(--ok, #0E9F6E);
  opacity:0;
  animation:res-swap-in 8s ease-in-out infinite;
  animation-play-state:var(--play,running);
  animation-iteration-count:var(--iter,infinite);
  animation-fill-mode:forwards;
}
@keyframes res-swap-out{ 0%,60%{ opacity:1 } 72%,100%{ opacity:0 } }
@keyframes res-swap-in{ 0%,60%{ opacity:0 } 72%,100%{ opacity:1 } }

/* Con movimiento reducido, cada pieza se queda en su estado resuelto —el que
   cuenta la historia entera—, no a mitad de una animación: las filas
   puestas, la mira apagada, el acto B del planificador (la fecha ya
   elegida) y la acción cerrada. Misma regla que el resto del sitio. */
@media (prefers-reduced-motion:reduce){
  .res__rows--fill .res__kv{ animation:none; opacity:1; translate:0 0 }
  .qrscan__line{ animation:none; opacity:0 }
  .qrscan__ok{ animation:none; opacity:1; scale:1 }
  .qrficha__act--a{ animation:none; opacity:0 }
  .qrficha__act--b{ animation:none; opacity:1; position:relative; inset:auto }
  .qrficha__scrollin{ animation:none; translate:0 0 }
  .planalt__act--a{ animation:none; opacity:0 }
  .planalt__act--b{ animation:none; opacity:1; position:relative; inset:auto }
  .res__swap-a{ animation:none; opacity:0 }
  .res__swap-b{ animation:none; opacity:1 }
  .app__actswap__act--a{ animation:none; opacity:0 }
  .app__actswap__act--b{ animation:none; opacity:1; position:relative; inset:auto }
  .app__actrow--sel td{ animation:none; background:var(--app-blue-wash) }
}

/* Mismo estado resuelto, para cuando la capacidad del rotador ya terminó su
   vuelta única. Ver la nota de arriba, junto al bloque de `.regpush`. */
.scene[data-terminado] .res__rows--fill .res__kv{ animation:none; opacity:1; translate:0 0 }
.scene[data-terminado] .qrscan__line{ animation:none; opacity:0 }
.scene[data-terminado] .qrscan__ok{ animation:none; opacity:1; scale:1 }
.scene[data-terminado] .qrficha__act--a{ animation:none; opacity:0 }
.scene[data-terminado] .qrficha__act--b{ animation:none; opacity:1; position:relative; inset:auto }
.scene[data-terminado] .qrficha__scrollin{ animation:none; translate:0 0 }
.scene[data-terminado] .planalt__act--a{ animation:none; opacity:0 }
.scene[data-terminado] .planalt__act--b{ animation:none; opacity:1; position:relative; inset:auto }
.scene[data-terminado] .res__swap-a{ animation:none; opacity:0 }
.scene[data-terminado] .res__swap-b{ animation:none; opacity:1 }
.scene[data-terminado] .app__actswap__act--a{ animation:none; opacity:0 }
.scene[data-terminado] .app__actswap__act--b{ animation:none; opacity:1; position:relative; inset:auto }
.scene[data-terminado] .app__actrow--sel td{ animation:none; background:var(--app-blue-wash) }

/* Píldora flotante: el aviso de una sola línea. No lleva marco de tarjeta
   porque no es un registro, es que algo acaba de pasar. */
.res-pill{
  position:absolute;
  z-index:3;
  display:inline-flex; align-items:center; gap:9px;
  padding:9px 14px;
  border-radius:var(--ds-radius-pill);
  background:#fff;
  border:1px solid var(--color-hairline);
  color:var(--color-ink);
  font:var(--font-weight-medium, 500) 13px/1.2 var(--font-sans);
  white-space:nowrap;
  box-shadow:rgba(12,32,64,.24) 0 10px 28px;
}
.res-pill svg{ width:15px; height:15px; flex:none; display:block; color:var(--res-acento, var(--color-primary)) }
.res-pill__dot{
  width:7px; height:7px; border-radius:50%;
  background:var(--res-acento, var(--color-primary));
  flex:none;
}

/* Lo oscuro es de la IA y de nada más. En Solved el grafito no es un recurso
   estético disponible para dar contraste sobre una foto: es la superficie de
   la capa de IA, igual que el naranja es Acciones. Una píldora oscura en una
   escena de Registros dice «IA» sin querer decirlo. */
.scene[data-module="ia"] .res-pill{
  background:rgba(11,18,32,.66);
  border-color:rgba(255,255,255,.22);
  color:#fff;
  box-shadow:rgba(0,0,0,.34) 0 10px 28px;
}
.scene[data-module="ia"] .res-pill svg{ color:var(--module-ia) }

/* =========================================================
   NOTIFICACIÓN DE CRISTAL — .ntf
   =========================================================
   Rotador de "Registra" (industria-alimentaria, 1 oct 2026): el recurso que
   confirma la acción, con el mismo gesto que la demo de Mitti en
   /es/inspecciones-e-informes —se responde un control y aparece un chip de
   confirmación—, pero sin copiar su tarjeta blanca y plana: aquí cae como una
   notificación real de móvil, con rebote, y el fondo es el ESTILO HIELO de
   `.compo--hielo` (vidrio esmerilado, no blanco sólido): es una notificación
   SOBRE una fotografía, y una tarjeta opaca ahí se lee como una pegatina.

   El icono es uno real del brandbook (`.bico`, máscara + currentColor —ver
   ds/app.css y assets/iconos/—), nunca un SVG suelto inventado para la
   ocasión: la ficha de icono dentro del chip usa el mismo acento de módulo
   que ya pinta `.res__ico`, así que el color sigue codificando módulo y no
   un gusto de la escena.

   Cae con el mismo rebote que `.regpush__notif` —la familia ya establecida
   de "esto es una notificación real"— pero UNA SOLA VEZ: fill-mode:both la
   deja clavada arriba, mismo criterio de "resuelto" que data-terminado en el
   resto de la escena, no un bucle de 8 s. */
.ntf{
  position:absolute;
  z-index:3;
  display:flex;
  align-items:flex-start;
  gap:10px;
  width:min(88%, 320px);
  padding:12px 14px;
  border-radius:16px;
  border:1px solid rgba(255,255,255,.5);
  background:rgba(255,255,255,.46);
  -webkit-backdrop-filter:blur(22px) saturate(1.3);
          backdrop-filter:blur(22px) saturate(1.3);
  box-shadow:0 20px 44px rgba(12,16,24,.28), inset 0 1px 0 rgba(255,255,255,.65);
  opacity:0;
  translate:0 -36px;
  scale:.95;
  animation:ntf-drop .6s cubic-bezier(.32,1,.4,1) both;
  animation-delay:1.8s;
  animation-play-state:var(--play,running);
}
.ntf__ico{
  flex:none;
  width:30px; height:30px;
  border-radius:9px;
  display:grid; place-items:center;
  background:var(--res-acento, var(--color-primary));
}
.ntf__ico .bico{ width:16px; height:16px; background:#fff }
.ntf__body{ min-width:0 }
.ntf__t{
  margin:0;
  font:var(--font-weight-medium, 500) 13.5px/1.3 var(--font-sans);
  color:var(--color-ink);
  letter-spacing:-.01em;
}
.ntf__s{
  margin:2px 0 0;
  font:400 12.5px/1.35 var(--font-sans);
  color:var(--color-ink-mute);
}
@keyframes ntf-drop{
  0%  { opacity:0; translate:0 -36px; scale:.95 }
  62% { opacity:1; translate:0 3px;   scale:1.015 }
  100%{ opacity:1; translate:0 0;     scale:1 }
}
@media (prefers-reduced-motion:reduce){
  .ntf{ animation:none; opacity:1; translate:0 0; scale:1 }
}

/* =========================================================
   VARIANTE REALISTA — .ntf--iphone
   =========================================================
   Petición del cliente (5 oct 2026): nada de notificación de cristal con un
   icono de marca al uso — pide una notificación REAL, versión iPhone, con el
   logo de Solved dentro, y si la notificación es de una persona (alguien te
   asigna algo, no el sistema), la cara de esa persona en vez del logo. Mismo
   componente `.ntf` —misma caída, mismo `ntf-drop`, mismo `data-terminado`—,
   sólo cambia el vestido: blanco sólido con desenfoque real (no el 46%
   translúcido del cristal, que aquí se leería como una notificación a medio
   cargar) y una fila de cabecera con el nombre de quien la manda y la hora,
   que es lo que diferencia una notificación real de una tarjeta cualquiera.

   Marcado:
     <div class="ntf ntf--iphone">
       <span class="ntf__ico"><img src="/assets/apple-touch-icon.png" …></span>
       <div class="ntf__body">
         <div class="ntf__meta"><span class="ntf__app">Solved</span><span class="ntf__time">ahora</span></div>
         <p class="ntf__t">…</p>
         <p class="ntf__s">…</p>
       </div>
     </div>
   Para una notificación de persona, `.ntf__ico--avatar` (redondo, con la foto
   de esa persona) y `.ntf__app--persona` (su nombre, sin versalitas: así es
   como iOS distingue un mensaje de alguien de un aviso de la propia app). */
.ntf--iphone{
  border:none;
  background:rgba(255,255,255,.92);
  -webkit-backdrop-filter:blur(26px) saturate(1.5);
          backdrop-filter:blur(26px) saturate(1.5);
}
.ntf--iphone .ntf__ico{
  width:32px; height:32px; border-radius:8px;
  background:#1E6BFF;
  overflow:hidden;
}
.ntf--iphone .ntf__ico img{ width:100%; height:100%; object-fit:cover; display:block }
.ntf--iphone .ntf__ico--avatar{ border-radius:50% }
.ntf__meta{
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  margin-bottom:1px;
}
.ntf__app{
  font:var(--font-weight-medium,500) 10px/1 var(--font-sans);
  letter-spacing:.03em; text-transform:uppercase;
  color:var(--color-ink-mute);
}
.ntf__app--persona{
  font:var(--font-weight-medium,500) 12.5px/1 var(--font-sans);
  letter-spacing:-.01em; text-transform:none;
  color:var(--color-ink);
}
.ntf__time{ font:400 10px/1 var(--font-sans); color:var(--color-ink-mute); flex:none }

/* La respuesta de la IA. Va sobre grafito y no sobre blanco, como todo lo de
   la capa de IA, y siempre con la referencia del parte del que sale: una
   respuesta que no se puede comprobar no sirve en una planta. */
.res-ia{
  background:#111214;
  border:1px solid #2A2F36;
  border-radius:var(--ds-radius-card, 12px);
  padding:16px 18px;
  color:#fff;
  text-align:left;
}
.res-ia__q{ display:flex; align-items:center; gap:9px; font:var(--font-weight-medium, 500) 14px/1.3 var(--font-sans) }
.res-ia__q svg{ width:16px; height:16px; flex:none; color:var(--module-ia) }
.res-ia__a{
  margin:10px 0 0;
  padding-top:10px;
  border-top:1px solid #2A2F36;
  font:var(--type-body-sm, 400 13px/1.45 var(--font-sans));
  color:#C6CCD4;
}
.res-ia__ref{
  display:inline-block;
  margin-top:8px;
  padding:3px 8px;
  border-radius:var(--ds-radius-chip, 4px);
  background:rgba(168,85,247,.18);
  color:#D6B4FD;
  font:var(--font-weight-medium, 500) 12px/1.3 var(--font-mono, var(--font-sans));
  letter-spacing:.02em;
}

/* Encuadre del recurso — UNO SOLO, y esto es la regla, no el valor por defecto.
   Las seis escenas de una rejilla son la misma herramienta contada para un
   trabajo distinto. Si el recurso cambia de ancho o de esquina en cada una, el
   ojo lee seis tarjetas sueltas y no seis versiones de lo mismo: la diferencia
   deja de estar en lo que dice cada una y pasa a estar en dónde se ha puesto.
   Por eso hay un único sitio —abajo a la derecha— y un único ancho en píxeles,
   no en porcentaje: en porcentaje, la escena cuadrada y la de banda darían dos
   tamaños distintos otra vez.

   El ancho está elegido para que quepan las versiones más largas: tres filas
   de pares con un valor de dos palabras («PDF con evidencias», «De planta a
   dirección») sin partirse, y el título del recurso en dos líneas.

   Aquí se rompe a propósito la regla de «el fragmento se sale por un borde»:
   esa regla existe porque una captura cortada dice «esto continúa», y un dato
   cortado no dice que continúe, dice que está roto —se probó, y «Conforme» se
   leía «C»—. El recurso va entero; lo que continúa más allá del marco es la
   foto.

   Los tres nombres (`res`, `res-br`, `res-bl`) resuelven al mismo sitio: los
   variantes quedan como alias para que el marcado antiguo no se descoloque,
   pero no hay dos encuadres. */
.scene__media[data-crop^="res"]{
  left:auto; right:24px;
  top:auto; bottom:24px;
  width:340px;
  box-shadow:rgba(12,32,64,.30) 0 16px 40px;
}

/* Por debajo de 900px el ancho fijo se come el marco: ahí el recurso pasa a
   ocupar el ancho disponible, que sigue siendo el mismo trato para todas. */
@media (max-width:900px){
  .scene__media[data-crop^="res"]{ left:20px; right:20px; width:auto; bottom:20px }
}
