/* ============================================================
   MOTION KIT — Light Solutions
   Sistema de movimiento compartido para todas las landings.

   Publicado en:  https://landings.ligthsolutions.com/shared/motion.css
   Fuente:        clients/00-ls-landing/shared/motion.css
                  (deploy-ls lo sube a la raíz del dominio)

   Uso:
     <link rel="stylesheet" href="/shared/motion.css?v=20260831">
     ...antes de css/styles.css, para que el cliente pueda pisar tokens.

   Niveles (ver .ai/MOTION-AGENT.md):
     L1 Esencial   — reveals, stagger, springs, hover
     L2 Pro        — scroll-linked, parallax, view transitions, kinetic type
     L3 Signature  — hooks para backdrop WebGL / motivo propio del rubro

   Sin dependencias. Sin build. Degrada solo en navegadores viejos.
   ============================================================ */


/* ------------------------------------------------------------
   1 · TOKENS DE MOVIMIENTO
   Solo tiempo y curvas. El color y el radio siguen viniendo del
   :root de cada cliente (DESIGN-SYSTEM.md) — el kit no los toca.
   ------------------------------------------------------------ */

:root {
  /* Duraciones */
  --m-fast: 180ms;
  --m-base: 420ms;
  --m-slow: 800ms;
  --m-slower: 1200ms;

  /* Curvas
     out      → expo out. Sale rápido y frena suave. El caballo de batalla.
     spring   → resorte real con overshoot, vía linear(). Reemplaza a Framer Motion.
     snap     → back-out corto. Para micro-interacciones (botones, chips).
     inout    → simétrica. Solo para loops y elementos que van y vuelven. */
  --m-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --m-ease-snap: cubic-bezier(0.34, 1.56, 0.64, 1);
  --m-ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
  --m-ease-spring: linear(
    0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938 16.7%, 1.017,
    1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161, 1.154 29.9%, 1.129 32.8%,
    1.051 39.6%, 1.017 43.1%, 0.991, 0.977 51%, 0.974 53.8%, 0.975 57.1%,
    0.997 69.8%, 1.003 76.9%, 1.001 83.2%, 1
  );

  /* Distancias de entrada — subirlas exagera el movimiento */
  --m-shift: 28px;
  --m-shift-lg: 64px;

  /* Stagger: retraso entre hermanos de un grupo */
  --m-stagger: 70ms;

  /* Parallax: recorrido total del elemento a lo largo del viewport */
  --m-parallax: 8%;
}

/* Permite animar height:auto y otros keywords intrínsecos.
   Sin esto, un acordeón necesita max-height hardcodeado. */
:root {
  interpolate-size: allow-keywords;
}

/* Ángulo animable para gradientes y bordes que giran (L2/L3).
   @property es lo que hace que un custom property sea interpolable. */
@property --m-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --m-beam {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}


/* ------------------------------------------------------------
   2 · ACCESIBILIDAD
   Regla dura: todo lo decorativo se apaga con reduced-motion y el
   contenido queda visible. Nunca dejar contenido oculto esperando
   una animación que no va a correr.
   ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  [class*="m-reveal"],
  .m-split,
  .m-split > span,
  .m-parallax,
  .m-scroll-fade,
  .m-scroll-scale,
  .m-scroll-reveal {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    animation: none !important;
    animation-timeline: none !important;
  }
}

:where(:focus-visible) {
  outline: 3px solid var(--color-accent, currentColor);
  outline-offset: 2px;
}


/* ============================================================
   NIVEL 1 · ESENCIAL
   ============================================================ */

/* ------------------------------------------------------------
   3 · REVEALS (one-shot)
   Se disparan una vez con IntersectionObserver desde motion.js,
   que agrega .is-in. Deliberadamente NO usan scroll-driven: un
   reveal que se des-revela al subir distrae. El scroll-driven se
   reserva para efectos continuos (sección 6).

   El estado oculto vive dentro de @media (scripting: enabled).
   Eso importa por dos motivos:
     · sin JS el contenido nunca queda invisible;
     · el ocultamiento aplica desde el primer paint, sin esperar a
       que corra un script — no hay flash de contenido que aparece
       y se vuelve a esconder para animar.
   ------------------------------------------------------------ */

@media (scripting: enabled) {
  :is(.m-reveal, .m-reveal-up, .m-reveal-left, .m-reveal-right, .m-reveal-scale, .m-reveal-clip) {
    opacity: 0;
    transition:
      opacity var(--m-slow) var(--m-ease-out),
      transform var(--m-slow) var(--m-ease-out),
      clip-path var(--m-slow) var(--m-ease-out);
    transition-delay: var(--m-delay, 0ms);
  }

  :is(.m-reveal, .m-reveal-up) { transform: translate3d(0, var(--m-shift), 0); }
  .m-reveal-left  { transform: translate3d(calc(var(--m-shift) * -1), 0, 0); }
  .m-reveal-right { transform: translate3d(var(--m-shift), 0, 0); }
  .m-reveal-scale { transform: scale(0.92); }
  .m-reveal-clip  { clip-path: inset(0 0 100% 0); }

  :is(.m-reveal, .m-reveal-up, .m-reveal-left, .m-reveal-right, .m-reveal-scale).is-in {
    opacity: 1;
    transform: none;
  }
  .m-reveal-clip.is-in {
    opacity: 1;
    clip-path: inset(0 0 0 0);
  }

  /* Entrada con resorte. Más viva, ideal para tarjetas y badges.
     No usarla en bloques de texto largo: el overshoot marea. */
  .m-spring {
    transition-timing-function: var(--m-ease-spring);
    transition-duration: var(--m-slower);
  }
}

/* ------------------------------------------------------------
   4 · STAGGER
   Los hijos entran en cascada. motion.js numera cada hijo en
   --m-i; acá solo se traduce ese índice a un delay.
   ------------------------------------------------------------ */

.m-stagger > * {
  --m-delay: calc(var(--m-i, 0) * var(--m-stagger));
}

.m-stagger-fast  { --m-stagger: 40ms; }
.m-stagger-slow  { --m-stagger: 120ms; }

/* ------------------------------------------------------------
   5 · MICRO-INTERACCIONES
   ------------------------------------------------------------ */

/* Botón magnético — motion.js escribe --m-mx / --m-my */
.m-magnetic {
  transition: transform var(--m-base) var(--m-ease-spring);
  transform: translate3d(var(--m-mx, 0), var(--m-my, 0), 0);
}

/* Tilt 3D — motion.js escribe --m-rx / --m-ry */
.m-tilt {
  transform: perspective(900px)
             rotateX(var(--m-rx, 0deg))
             rotateY(var(--m-ry, 0deg))
             scale(var(--m-tz, 1));
  transition: transform var(--m-base) var(--m-ease-out);
  transform-style: preserve-3d;
}

/* Tilt + reveal en el MISMO elemento.
   Las dos cosas quieren escribir `transform`, así que sin esta regla
   la última en el cascade gana y el otro efecto desaparece. Acá se
   fusionan: entra desplazado y, una vez dentro, el tilt toma el mando.
   Es la única combinación de transform que el kit soporta apilada. */
@media (scripting: enabled) {
  .m-tilt:is(.m-reveal, .m-reveal-up) {
    transform: perspective(900px) translate3d(0, var(--m-shift), 0);
  }
  .m-tilt.m-reveal-left  { transform: perspective(900px) translate3d(calc(var(--m-shift) * -1), 0, 0); }
  .m-tilt.m-reveal-right { transform: perspective(900px) translate3d(var(--m-shift), 0, 0); }
  .m-tilt.m-reveal-scale { transform: perspective(900px) scale(0.92); }

  .m-tilt:is(.m-reveal, .m-reveal-up, .m-reveal-left, .m-reveal-right, .m-reveal-scale).is-in {
    transform: perspective(900px)
               rotateX(var(--m-rx, 0deg))
               rotateY(var(--m-ry, 0deg))
               scale(var(--m-tz, 1));
  }
}

/* Subrayado que crece desde la izquierda */
.m-underline {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size var(--m-base) var(--m-ease-out);
}
.m-underline:hover,
.m-underline:focus-visible { background-size: 100% 1px; }

/* Borde que gira. Necesita --m-angle (@property, sección 1). */
.m-border-glow {
  position: relative;
  isolation: isolate;
}
.m-border-glow::before {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(
    from var(--m-angle),
    transparent 0deg,
    var(--color-accent, currentColor) 60deg,
    transparent 140deg,
    transparent 360deg
  );
  /* mask recorta el relleno y deja solo el contorno */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: m-spin 4s linear infinite;
  z-index: -1;
}
@keyframes m-spin { to { --m-angle: 360deg; } }

/* Acordeón sin max-height hardcodeado (usa interpolate-size) */
.m-collapse {
  height: 0;
  overflow: hidden;
  transition: height var(--m-base) var(--m-ease-out);
}
.m-collapse.is-open { height: auto; }


/* ============================================================
   NIVEL 2 · PRO
   ============================================================ */

/* ------------------------------------------------------------
   6 · SCROLL-LINKED (CSS puro, sin JS)
   Estas animaciones corren atadas a la posición del scroll y en
   el compositor — no tocan el hilo principal, así que no las
   frena el JS del cliente. Es el salto de calidad más grande
   del kit y no cuesta un solo kilobyte de JavaScript.

   Requisito de layout: ningún ancestro con overflow:hidden en el
   eje vertical. En el reset usar `overflow-x: clip` en body,
   NUNCA `overflow-x: hidden` (crea un scroll container y rompe
   el timeline). Ver MOTION-AGENT.md.
   ------------------------------------------------------------ */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* Parallax: el elemento se desplaza más lento que la página.
       Ajustar recorrido con --m-parallax (8% por defecto).
       Aplicar a la IMAGEN, no al contenedor, y darle al
       contenedor overflow:hidden + una imagen ~115% de alto. */
    .m-parallax {
      animation: m-parallax-kf linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }
    @keyframes m-parallax-kf {
      from { transform: translate3d(0, calc(var(--m-parallax) * -1), 0); }
      to   { transform: translate3d(0, var(--m-parallax), 0); }
    }

    /* Hero que se desvanece y se aleja al salir por arriba.
       El truco de "la página tiene profundidad". */
    .m-scroll-fade {
      animation: m-scroll-fade-kf linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 90%;
    }
    @keyframes m-scroll-fade-kf {
      to { opacity: 0; transform: scale(0.94) translate3d(0, -6%, 0); }
    }

    /* Imagen que se abre a medida que entra: de recortada a completa.
       Muy efectivo en galerías y en la foto de "nosotros". */
    .m-scroll-reveal {
      animation: m-scroll-reveal-kf linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 42%;
    }
    @keyframes m-scroll-reveal-kf {
      from { clip-path: inset(18% 12% 18% 12% round var(--border-radius, 0)); transform: scale(1.12); }
      to   { clip-path: inset(0 0 0 0 round var(--border-radius, 0)); transform: scale(1); }
    }

    /* Escala sutil ligada al scroll — para fondos y cards grandes */
    .m-scroll-scale {
      animation: m-scroll-scale-kf linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 50%;
    }
    @keyframes m-scroll-scale-kf {
      from { transform: scale(1.14); }
      to   { transform: scale(1); }
    }

    /* Barra de progreso de lectura. Pegar a la navbar.
       Se ata al scroll de la raíz, no a la vista del elemento. */
    .m-progress {
      transform-origin: 0 50%;
      animation: m-progress-kf linear both;
      animation-timeline: scroll(root block);
    }
    @keyframes m-progress-kf {
      from { transform: scaleX(0); }
      to   { transform: scaleX(1); }
    }

    /* Marquee que acelera con el scroll en vez de girar solo.
       Se siente intencional; el marquee automático se siente ruido. */
    .m-marquee-scroll {
      animation: m-marquee-kf linear both;
      animation-timeline: scroll(root block);
    }
    @keyframes m-marquee-kf {
      to { transform: translate3d(-45%, 0, 0); }
    }
  }
}

/* ------------------------------------------------------------
   7 · VIEW TRANSITIONS
   Transición entre páginas del mismo origen. Como todas las
   landings viven bajo landings.ligthsolutions.com, navegar de la
   00 a una demo se siente como una app, no como un salto.
   ------------------------------------------------------------ */

@view-transition { navigation: auto; }

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    animation: m-vt-out 260ms var(--m-ease-out) both;
  }
  ::view-transition-new(root) {
    animation: m-vt-in 380ms var(--m-ease-out) both;
  }
  @keyframes m-vt-out { to { opacity: 0; transform: scale(0.99); } }
  @keyframes m-vt-in  { from { opacity: 0; transform: scale(1.01); } }
}

/* Elementos que persisten entre páginas (logo, tarjeta clickeada).
   El mismo nombre en ambas páginas hace que el navegador
   interpole la posición: es la animación de layout de Framer
   Motion, nativa y gratis. Un nombre no puede repetirse en una
   misma página. */
.m-vt-logo { view-transition-name: m-logo; }
.m-vt-hero { view-transition-name: m-hero; }

/* ------------------------------------------------------------
   8 · KINETIC TYPE
   motion.js parte el texto en palabras o letras dentro de
   <span> y numera cada una en --m-i. La tipografía deja de ser
   un bloque y pasa a ser el motivo visual.
   ------------------------------------------------------------ */

.m-split { display: inline-block; }

.m-split > span {
  display: inline-block;
  white-space: pre;   /* preserva los espacios entre palabras */
  will-change: transform, opacity;
}

@media (scripting: enabled) {
  /* Mientras motion.js no partió el texto, el bloque entero está
     oculto: si no, se ve la frase completa y desaparece al partirse. */
  .m-split:not([data-split-done]) { opacity: 0; }
  .m-split[data-split-done] { opacity: 1; }

  .m-split > span {
    opacity: 0;
    transform: translate3d(0, 0.75em, 0);
    transition:
      opacity var(--m-slow) var(--m-ease-out),
      transform var(--m-slow) var(--m-ease-out);
    transition-delay: calc(var(--m-i, 0) * var(--m-stagger));
  }
  .m-split.is-in > span {
    opacity: 1;
    transform: none;
  }
}

/* Variante con máscara: las líneas suben desde detrás de un borde.
   Requiere que .m-split-mask envuelva a .m-split. */
.m-split-mask {
  display: block;
  overflow: hidden;
  padding-bottom: 0.12em;   /* aire para descendentes: g, j, p, y */
}
@media (scripting: enabled) {
  .m-split-mask .m-split > span { transform: translate3d(0, 1.1em, 0); }
}

/* Peso variable animado al hover. Solo con fuentes variables. */
.m-weight {
  transition: font-variation-settings var(--m-base) var(--m-ease-out);
  font-variation-settings: "wght" var(--m-wght, 400);
}
.m-weight:hover { --m-wght: 700; }


/* ============================================================
   NIVEL 3 · SIGNATURE
   ============================================================ */

/* ------------------------------------------------------------
   9 · BACKDROP
   El canvas WebGL lo monta shared/webgl-backdrop.js. Este bloque
   define el contenedor y — clave — el fallback: un gradiente
   animado en CSS que se ve bien por sí solo. Si WebGL no está
   disponible, el sitio no se rompe ni queda plano.
   ------------------------------------------------------------ */

.m-backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background:
    radial-gradient(60% 80% at 20% 15%, var(--m-bd-1, rgba(255,255,255,0.10)), transparent 60%),
    radial-gradient(50% 70% at 85% 75%, var(--m-bd-2, rgba(255,255,255,0.07)), transparent 60%);
}

.m-backdrop canvas {
  width: 100%;
  height: 100%;
  display: block;
}

/* Fallback animado mientras no hay WebGL (o si falló) */
.m-backdrop:not(.is-gl)::after {
  content: "";
  position: absolute;
  inset: -20%;
  background: conic-gradient(
    from var(--m-angle),
    var(--m-bd-1, rgba(255,255,255,0.10)),
    var(--m-bd-2, rgba(255,255,255,0.05)),
    var(--m-bd-1, rgba(255,255,255,0.10))
  );
  filter: blur(70px);
  animation: m-spin 22s linear infinite;
}

/* Grano de película. Le saca el aspecto "plano de CSS" a
   cualquier fondo de color. El SVG va inline como data URI:
   sin petición de red. */
.m-grain::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: var(--m-grain, 0.045);
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Foco que sigue al cursor sobre una sección (motion.js escribe
   --m-px / --m-py como porcentajes) */
.m-spotlight {
  position: relative;
}
.m-spotlight::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    30rem circle at var(--m-px, 50%) var(--m-py, 50%),
    var(--m-bd-1, rgba(255,255,255,0.08)),
    transparent 65%
  );
  opacity: 0;
  transition: opacity var(--m-slow) var(--m-ease-out);
}
.m-spotlight:hover::before { opacity: 1; }


/* ------------------------------------------------------------
   10 · UTILIDADES
   ------------------------------------------------------------ */

.m-no-motion { animation: none !important; transition: none !important; }

/* El navegador ya no necesita la promesa de will-change una vez
   que el elemento entró. motion.js la libera con transitionend;
   esto es el respaldo. */
.is-in { will-change: auto; }
