/* ============================================================================
   ProFlight — animaciones de landing · kit compartido · v3 (17-sep-2026)
   Copia maestra: Landing pages ProFlight/UI KIT - B/animaciones/animaciones.css
   Va siempre con animaciones.js (mismo número de versión). Manual: sección "Movimiento"
   de SISTEMA-DE-DISENO-PROFLIGHT-2026.md. Origen: landing Open Day (sep-2026).
   v3 (2ª revisión del 17-sep, verificada con pruebas/casos-test.html):
       - los títulos por líneas miden lo que dice su line-height: el título es flex en columna,
         así los márgenes negativos de las máscaras no colapsan (antes sobraban .12em entre
         líneas y .26em en los bordes: 38 px de más en un título de 100 px a dos líneas).
         OJO al pasar una landing a v3: el título del banner queda más compacto; si se quiere
         el aire de antes, se sube el line-height en el CSS de la landing, no acá.
       - el mapa suelta el stroke-dasharray al terminar (.an-fin): si un navegador ignora
         pathLength, el trazo no queda punteado. También en reveal-off y al imprimir.
       - al imprimir se anulan también los transform/opacity que el parallax dejó en línea
         (el texto del banner ya desvanecido salía en blanco en el PDF).
   v2 (revisión del 17-sep): los tiempos viven acá (tokens.css ya no los pisa); el estado
       revelado no fija transform/opacity ni deja transition (los hover de la landing valen);
       el telón suelta el clip-path al terminar (vuelve la sombra del marco); .line con layout
       fuera de prefers-reduced-motion; hasta 5 líneas escalonadas; halo y punto en ::after
       (no tapan el anillo de foco ni la entrada); [data-parallax] y [data-parallax-foto]
       recortan solos; @media print muestra todo.

   Edison, 17-sep-2026: "Me gustan las animaciones del sitio, el parallax, cómo se abren las
   imágenes, todo. Agregalas al manual, que el resto de las landings las tengan también."

   CÓMO SE USA (resumen; el detalle está en el manual):
     <head>  <script>document.documentElement.classList.add("js");setTimeout(function(){if(!window.PF_ANIM)document.documentElement.classList.add("reveal-off")},4000);</script>
             <link rel="stylesheet" href="/assets/animaciones.css?v=N">        ← después de tokens.css y styles.css
             <script src="/assets/animaciones.js?v=N" defer></script>          ← N: número de caché de la landing;
                                                                                  se sube en cada copia (puede ser
                                                                                  mayor que la versión de esta cabecera)
   Ganchos:
     [data-entrada]            banner: sus hijos directos entran en cascada al cargar (CSS puro)
     [data-entrada-lineas]     título del banner: cada .line > span sube desde su máscara (hasta 5)
     .reveal                   el bloque sube con desenfoque al entrar en pantalla
     .reveal-stagger           igual, pero cada hijo directo entra 70 ms después del anterior (tope: 7º)
     [data-anim="sube"]        lo mismo que .reveal (para elementos sueltos)
     [data-anim="barrido"]     etiqueta que aparece de izquierda a derecha
     [data-anim="lineas"]      título en .line > span: cada línea sube desde su máscara (hasta 5)
     [data-palabras]           título que entra palabra por palabra (lo parte el JS)
     .reveal-img               foto que se abre como un telón y se asienta (--r-foto = radio)
     [data-parallax-foto]      la foto se mueve dentro de su marco con el scroll (img al 116 %)
     [data-parallax="banner"]  sección de arranque: [data-parallax-media] baja a media velocidad,
                               [data-parallax-texto] sube y se desvanece
     [data-parallax="banda"]   banda con foto: [data-parallax-media] recorre ±15 % del alto
     [data-deriva="-1|1"]      dentro de un [data-parallax]: se corre a un lado con el scroll
     [data-pop]                ícono que "cae" con rebote cuando entra su bloque
     [data-mapa]               SVG: los <path class="mp-trazo" pathLength="1"> se dibujan,
                               los .mp-tarde aparecen después (--mp-retardo por grupo)
     [data-despliega]          <details>: el contenido baja suave al abrirse
     [data-halo] / [data-late] botón con halo que late / punto que late (en ::after)
   Todo se apaga con prefers-reduced-motion. Red de seguridad: html.reveal-off (la pone el JS
   si un bloque revelado sigue invisible, o el <head> si el JS no cargó a los 4 s).

   REGLAS que no se negocian:
   1. Nada de lo que está en la primera pantalla depende del JS para verse: la entrada del
      banner es CSS puro y termina sola.
   2. Un gancho nuevo se agrega en TRES lugares: acá, en el selector SEL de animaciones.js y
      en el bloque html.reveal-off. Si falta uno, el contenido queda invisible para siempre.
   3. No poner transform en html.reveal-off sobre partes de un SVG: pisa la posición de los
      textos del mapa.
   4. Los tiempos se cambian ACÁ y se sube la versión en todas las landings; la landing no
      los pisa desde tokens.css.
   5. Las reglas que esconden van con :where() para quedar en (0,2,0): así las apaga el
      bloque de prefers-reduced-motion y las pisa una regla normal de la landing si hace falta.
   ============================================================================ */

/* ---- Tiempos y curvas ------------------------------------------------------------------ */
:root {
  --an-ease-sale: cubic-bezier(.16, 1, .3, 1);      /* arranca rápido, frena largo */
  --an-ease:      var(--ease, cubic-bezier(.2, 0, 0, 1));
  --an-dur:       900ms;
  --an-paso:      70ms;
  --an-shift:     32px;
  --an-blur:      12px;
}
@media (max-width: 767px) { :root { --an-shift: 20px; } }

/* ---- Contenedores: lo que se mueve adentro no puede asomar ----------------------------- */
[data-parallax] { overflow: hidden; }
.reveal-img, [data-parallax-foto] { overflow: hidden; }

/* ============================ 1. ENTRADA DEL BANNER ============================
   CSS puro: corre en cuanto pinta la página, sin esperar al script (con internet lenta el
   banner quedaba vacío un par de segundos cuando lo disparaba el JS). Los hijos directos
   entran en cascada; el título marcado con [data-entrada-lineas] no se mueve entero: cada
   línea sube desde una máscara que se estira arriba y abajo para no recortar acentos.
   El layout de las líneas (block + máscara) vale siempre, con o sin movimiento. El título es
   flex en columna: en un contenedor flex los márgenes negativos de las máscaras no colapsan
   entre sí ni con el título, así que las líneas miden exactamente su line-height. */
[data-entrada-lineas], [data-anim="lineas"] { display: flex; flex-direction: column; align-items: stretch; }
[data-entrada-lineas] .line { display: block; overflow: hidden; padding-block: .12em .14em; margin-block: -.12em -.14em; }
[data-entrada-lineas] .line > span { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  [data-entrada] > * { animation: an-entra 1s var(--an-ease-sale) both; animation-delay: var(--an-t, 0s); }
  [data-entrada] > :nth-child(1) { --an-t: .05s; }
  [data-entrada] > :nth-child(2) { --an-t: .2s; }
  [data-entrada] > :nth-child(3) { --an-t: .35s; }
  [data-entrada] > :nth-child(4) { --an-t: .5s; }
  [data-entrada] > :nth-child(5) { --an-t: .65s; }
  [data-entrada] > :nth-child(6) { --an-t: .8s; }
  [data-entrada] > :nth-child(7) { --an-t: .95s; }
  [data-entrada] > :nth-child(n+8) { --an-t: 1.1s; }
  /* Lo que viene después del título espera a que suba su segunda línea (+.15 s) */
  [data-entrada] > [data-entrada-lineas] ~ :nth-child(2) { --an-t: .35s; }
  [data-entrada] > [data-entrada-lineas] ~ :nth-child(3) { --an-t: .5s; }
  [data-entrada] > [data-entrada-lineas] ~ :nth-child(4) { --an-t: .65s; }
  [data-entrada] > [data-entrada-lineas] ~ :nth-child(5) { --an-t: .8s; }
  [data-entrada] > [data-entrada-lineas] ~ :nth-child(6) { --an-t: .95s; }
  [data-entrada] > [data-entrada-lineas] ~ :nth-child(7) { --an-t: 1.1s; }
  [data-entrada] > [data-entrada-lineas] ~ :nth-child(n+8) { --an-t: 1.25s; }
  [data-entrada] > [data-entrada-lineas] { animation: none; }
  [data-entrada-lineas] .line > span { animation: an-mascara 1.2s var(--an-ease-sale) both; animation-delay: var(--an-t, 0s); }
  [data-entrada-lineas] .line:nth-child(2) > span { animation-delay: calc(var(--an-t, 0s) + .15s); }
  [data-entrada-lineas] .line:nth-child(3) > span { animation-delay: calc(var(--an-t, 0s) + .3s); }
  [data-entrada-lineas] .line:nth-child(4) > span { animation-delay: calc(var(--an-t, 0s) + .45s); }
  [data-entrada-lineas] .line:nth-child(n+5) > span { animation-delay: calc(var(--an-t, 0s) + .6s); }
}
@keyframes an-entra   { from { opacity: 0; transform: translateY(28px); filter: blur(10px); } }
@keyframes an-mascara { from { transform: translateY(112%); } }

/* ============================ 2. REVELADO AL HACER SCROLL ============================
   .in la pone el JS cuando el bloque entra en pantalla (15 % visible); .an-fin cuando la
   transición terminó. Escondido = :not(.in); en transición = :not(.an-fin). Después de
   .an-fin el kit no declara nada sobre el elemento: valen los hover y transiciones de la
   landing. Cuando varios entran juntos, el JS les escalona --retardo (90 ms, en orden de
   lectura). */
.js :where(.reveal, [data-anim="sube"]):not(.in),
.js :where(.reveal-stagger):not(.in) > * {
  opacity: 0;
  transform: translateY(var(--an-shift));
  filter: blur(var(--an-blur));
}
.js :where(.reveal, [data-anim="sube"]):not(.an-fin),
.js :where(.reveal-stagger):not(.an-fin) > * {
  transition: opacity var(--an-dur) var(--an-ease-sale),
              transform var(--an-dur) var(--an-ease-sale),
              filter var(--an-dur) var(--an-ease-sale);
  transition-delay: var(--retardo, 0ms);
}
.js :where(.reveal-stagger:not(.an-fin) > :nth-child(2))   { transition-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 1); }
.js :where(.reveal-stagger:not(.an-fin) > :nth-child(3))   { transition-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 2); }
.js :where(.reveal-stagger:not(.an-fin) > :nth-child(4))   { transition-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 3); }
.js :where(.reveal-stagger:not(.an-fin) > :nth-child(5))   { transition-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 4); }
.js :where(.reveal-stagger:not(.an-fin) > :nth-child(6))   { transition-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 5); }
.js :where(.reveal-stagger:not(.an-fin) > :nth-child(n+7)) { transition-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 6); }

/* Palabra por palabra. El JS envuelve cada palabra en .w (máscara) > .wi (la palabra) y le
   numera --i. La máscara se estira arriba y abajo para no cortar acentos ni descendentes. */
.js [data-palabras] .w { display: inline-block; overflow: hidden; vertical-align: top; padding-block: .1em .18em; margin-block: -.1em -.18em; }
.js [data-palabras] .wi { display: inline-block; transform-origin: 0 100%; }
.js :where([data-palabras]:not(.in)) .wi { opacity: 0; transform: translateY(108%) rotate(5deg); }
.js :where([data-palabras]:not(.an-fin)) .wi {
  transition: transform 1.05s var(--an-ease-sale), opacity .6s var(--an-ease);
  transition-delay: calc(var(--retardo, 0ms) + var(--i, 0) * 45ms);
}

/* Etiqueta que se descubre de izquierda a derecha */
.js :where([data-anim="barrido"]):not(.in) { opacity: 0; clip-path: inset(0 100% 0 0); transform: translateX(-10px); }
.js :where([data-anim="barrido"]):not(.an-fin) {
  transition: clip-path 1s var(--an-ease-sale), transform 1s var(--an-ease-sale), opacity .4s var(--an-ease);
  transition-delay: var(--retardo, 0ms);
}

/* Título por líneas: <span class="line"><span>texto</span></span>, hasta 5 escalonadas */
[data-anim="lineas"] .line { display: block; overflow: hidden; padding-block: .12em .14em; margin-block: -.12em -.14em; }
[data-anim="lineas"] .line > span { display: inline-block; }
.js :where([data-anim="lineas"]:not(.in)) .line > span { opacity: 0; transform: translateY(112%); }
.js :where([data-anim="lineas"]:not(.an-fin)) .line > span { transition: transform 1.2s var(--an-ease-sale), opacity .5s var(--an-ease); transition-delay: var(--retardo, 0ms); }
.js :where([data-anim="lineas"]:not(.an-fin) .line:nth-child(2)) > span { transition-delay: calc(var(--retardo, 0ms) + 140ms); }
.js :where([data-anim="lineas"]:not(.an-fin) .line:nth-child(3)) > span { transition-delay: calc(var(--retardo, 0ms) + 280ms); }
.js :where([data-anim="lineas"]:not(.an-fin) .line:nth-child(4)) > span { transition-delay: calc(var(--retardo, 0ms) + 420ms); }
.js :where([data-anim="lineas"]:not(.an-fin) .line:nth-child(n+5)) > span { transition-delay: calc(var(--retardo, 0ms) + 560ms); }

/* ============================ 3. FOTOS ============================
   Telón que se abre y la imagen que se asienta. --r-foto es el radio del marco (0 a sangre):
   lo define la landing por selector, con el mismo valor que su border-radius. Al terminar
   (.an-fin) el clip-path se suelta: mientras el telón se abre, la sombra del marco no se ve.
   La foto con parallax va agrandada 16 %: así se mueve dentro del marco sin que asome el
   borde; el elemento que lleva el atributo es el que recorta. */
[data-parallax-foto] img { scale: 1.16; }
.js :where(.reveal-img:not(.in)) { clip-path: inset(12% 8% 12% 8% round var(--r-foto, 0px)); }
.js :where(.reveal-img.in:not(.an-fin)) { clip-path: inset(0 0 0 0 round var(--r-foto, 0px)); }
.js :where(.reveal-img:not(.an-fin)) { transition: clip-path 1.3s var(--an-ease-sale); transition-delay: var(--retardo, 0ms); }
.js :where(.reveal-img:not(.in)) img { scale: 1.34; }
.js :where(.reveal-img:not(.an-fin)) img { transition: scale 1.8s var(--an-ease-sale); transition-delay: var(--retardo, 0ms); }

/* ============================ 4. ÍCONOS ============================
   Dentro de cualquier bloque revelado (.in), el ícono marcado "cae" con un rebote corto,
   160 ms después de su bloque; en una lista en cascada, después de su propio hijo. */
.js :where(.in) [data-pop] { animation: an-pop .8s cubic-bezier(.34, 1.56, .64, 1) both; animation-delay: calc(var(--retardo, 0ms) + 160ms); }
.js :where(.reveal-stagger.in > :nth-child(2))   [data-pop] { animation-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 1 + 160ms); }
.js :where(.reveal-stagger.in > :nth-child(3))   [data-pop] { animation-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 2 + 160ms); }
.js :where(.reveal-stagger.in > :nth-child(4))   [data-pop] { animation-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 3 + 160ms); }
.js :where(.reveal-stagger.in > :nth-child(5))   [data-pop] { animation-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 4 + 160ms); }
.js :where(.reveal-stagger.in > :nth-child(6))   [data-pop] { animation-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 5 + 160ms); }
.js :where(.reveal-stagger.in > :nth-child(n+7)) [data-pop] { animation-delay: calc(var(--retardo, 0ms) + var(--an-paso) * 6 + 160ms); }
@keyframes an-pop { from { transform: scale(.3) rotate(-20deg); opacity: 0; } }

/* ============================ 5. MAPA (SVG) ============================
   El SVG lleva [data-mapa]; el JS le pone .in al entrar en pantalla. Cada trazo (calle,
   recorrido, flecha) es un <path class="mp-trazo" pathLength="1"> (sólo <path>: en otras
   figuras pathLength no siempre vale y el trazo quedaría punteado); se dibuja en 1,8 s. Un
   grupo puede retrasar sus trazos con --mp-retardo (ej. .mp-recorrido { --mp-retardo: .9s }).
   Lo que no es trazo (zonas, rótulos, leyenda) lleva .mp-tarde y aparece a 1 s + --mp-retardo. */
.js [data-mapa] .mp-trazo { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 1.8s var(--an-ease-sale); transition-delay: var(--mp-retardo, 0s); }
.js [data-mapa].in .mp-trazo { stroke-dashoffset: 0; }
/* Dibujado y terminado: el trazo deja de depender del dasharray (si el navegador ignoró
   pathLength, con dasharray 1 en unidades reales quedaría punteado para siempre). El JS pone
   .an-fin cuando terminó el grupo más retrasado (1,8 s + el mayor --mp-retardo). */
.js [data-mapa].an-fin .mp-trazo { stroke-dasharray: none; }
.js [data-mapa] .mp-tarde { opacity: 0; transition: opacity .9s var(--an-ease) calc(1s + var(--mp-retardo, 0s)); }
.js [data-mapa].in .mp-tarde { opacity: 1; }

/* ============================ 6. MICRO ============================ */
/* <details data-despliega>: el contenido baja suave al abrirse (el cierre es inmediato) */
@media (prefers-reduced-motion: no-preference) {
  details[data-despliega][open] > :not(summary) { animation: an-despliega .5s var(--an-ease-sale); }
}
@keyframes an-despliega { from { opacity: 0; transform: translateY(-8px); } }

/* Halo que late (CTA principal) y punto que late: viven en ::after, así no pisan la sombra
   ni el anillo de foco del elemento, ni su animación de entrada. --halo-rgb / --late-rgb
   es el color. El elemento no debe usar ::after para otra cosa. */
:where([data-halo], [data-late]) { position: relative; }
[data-halo]::after, [data-late]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; }
@media (prefers-reduced-motion: no-preference) {
  [data-halo]::after { animation: an-halo 2.6s var(--an-ease) 1.8s infinite; }
  [data-late]::after { animation: an-late 1.6s var(--an-ease) infinite; }
}
@keyframes an-halo {
  0%   { box-shadow: 0 0 0 0 rgba(var(--halo-rgb, 5, 56, 92), .45); }
  70%  { box-shadow: 0 0 0 14px rgba(var(--halo-rgb, 5, 56, 92), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--halo-rgb, 5, 56, 92), 0); }
}
@keyframes an-late {
  0%   { box-shadow: 0 0 0 0 rgba(var(--late-rgb, 181, 71, 8), .55); }
  70%  { box-shadow: 0 0 0 9px rgba(var(--late-rgb, 181, 71, 8), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--late-rgb, 181, 71, 8), 0); }
}

/* ============================ 7. SIN MOVIMIENTO ============================
   prefers-reduced-motion: todo visible de entrada, sin transiciones. El parallax no corre
   (lo decide el JS con la misma media query). Estas reglas ganan a las de arriba por
   especificidad (las que esconden van en :where) y por orden. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .js .reveal, .js .reveal-stagger > *, .js [data-anim], .js [data-anim] .line > span, .js [data-palabras] .wi {
    opacity: 1; transform: none; filter: none; clip-path: none; transition: none;
  }
  .js .reveal-img { clip-path: none; transition: none; }
  .js .reveal-img img, [data-parallax-foto] img { scale: 1; transition: none; }
  .js [data-mapa] .mp-trazo { stroke-dashoffset: 0; transition: none; }
  .js [data-mapa] .mp-tarde { opacity: 1; transition: none; }
  .js .in [data-pop] { animation: none; }
}

/* ============================ 8. RED DE SEGURIDAD ============================
   .in sólo sirve si la transición corre. Si el navegador no la avanza (pestaña en segundo
   plano, ahorro de energía), el contenido quedaría invisible aunque tenga .in: el JS lo
   detecta y pone html.reveal-off, que muestra todo de una. El <head> hace lo mismo si el
   JS no cargó. Al imprimir, también todo visible. OJO: sin transform sobre partes del SVG
   del mapa (pisaría la posición de los rótulos). */
html.reveal-off .reveal,
html.reveal-off .reveal-stagger > *,
html.reveal-off [data-anim],
html.reveal-off [data-anim] .line > span,
html.reveal-off [data-palabras] .wi,
html.reveal-off [data-entrada] > *,
html.reveal-off [data-entrada-lineas] .line > span,
html.reveal-off .in [data-pop] {
  opacity: 1 !important; transform: none !important; filter: none !important; clip-path: none !important;
  transition: none !important; animation: none !important;
}
html.reveal-off .reveal-img { clip-path: none !important; transition: none !important; }
html.reveal-off .reveal-img img { transition: none !important; }
html.reveal-off .reveal-img:not([data-parallax-foto]) img { scale: 1 !important; }
html.reveal-off [data-parallax-foto] img { scale: 1.16 !important; }
html.reveal-off [data-mapa] .mp-trazo { stroke-dashoffset: 0 !important; stroke-dasharray: none !important; transition: none !important; }
html.reveal-off [data-mapa] .mp-tarde { opacity: 1 !important; transition: none !important; }
@media print {
  .reveal, .reveal-stagger > *, [data-anim], [data-anim] .line > span, [data-palabras] .wi,
  [data-entrada] > *, [data-entrada-lineas] .line > span, .in [data-pop], .reveal-img, .reveal-img img {
    opacity: 1 !important; transform: none !important; filter: none !important; clip-path: none !important;
    scale: 1 !important; transition: none !important; animation: none !important;
  }
  /* El parallax deja transform/opacity en línea (el texto del banner se desvanece al bajar):
     en papel todo vuelve a su lugar y se ve entero. */
  [data-parallax-media], [data-parallax-texto], [data-deriva], [data-parallax-foto] img {
    transform: none !important; translate: none !important; opacity: 1 !important; scale: 1 !important;
  }
  [data-mapa] .mp-trazo { stroke-dashoffset: 0 !important; stroke-dasharray: none !important; }
  [data-mapa] .mp-tarde { opacity: 1 !important; }
}
