/* demo-site/styles.css
 *
 * Página de demo visual para ventas (candidato de la Sección 7, ítem 5
 * de docs/estado-del-proyecto.md) — proyecto SEPARADO del panel interno
 * (apps/dashboard/): sin backend, sin build, sin JS. Todo el contenido
 * es de ejemplo, fijo en el HTML — nunca llama a la API real de Claude
 * (costo $0 cada vez que se muestra). El efecto de "nodo se expande,
 * los demás bajan opacidad" es 100% CSS (selectores de hermanos +
 * :hover/:focus) — ni siquiera el JS cosmético mínimo que sí tiene el
 * flujo de nodos del Wizard interno (layout.ts, apps/dashboard).
 *
 * Paleta propia, no la de apps/dashboard/views/layout.ts a propósito —
 * esta es una página de marketing para prospectos, no la herramienta
 * operativa interna (aunque comparte la misma idea visual de nodos
 * conectados estilo n8n, por pedido explícito del usuario). El azul de
 * acento es el mismo --color-primary del Dashboard (#06c) para una
 * continuidad de marca mínima, sin copiar el resto del sistema.
 */

:root {
  --color-bg: #f7f8fb;
  --color-surface: #ffffff;
  --color-text: #14161a;
  --color-text-muted: #5b6472;
  --color-primary: #0066cc;
  --color-primary-dark: #0055aa;
  --color-border: #e2e5ea;
  --shadow-md: 0 4px 14px rgba(15, 23, 42, 0.08);
  --shadow-lg: 0 20px 45px rgba(15, 23, 42, 0.18);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f1115;
    --color-surface: #181b21;
    --color-text: #eef0f3;
    --color-text-muted: #a3a9b5;
    --color-primary: #4da3ff;
    --color-primary-dark: #7cbaff;
    --color-border: #262a33;
    --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 20px 45px rgba(0, 0, 0, 0.55);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
}

/* ── Hero: siempre oscuro, independiente del tema del sistema — es un
   banner de marca, no contenido que deba seguir prefers-color-scheme. ── */
.hero {
  /* BUG real, 3ª vuelta — esta vez confirmado contra la especificación
     de CSS, no supuesto. El arreglo anterior (`transform` en vez de
     `position: relative`) partía de una premisa FALSA: pensé que
     `transform` evitaba que .hero se pintara por encima de .stage.
     No es así — la spec de CSS Transforms dice textual que un elemento
     con `transform` distinto de `none` se trata, a efectos de
     apilamiento, EXACTAMENTE IGUAL que si tuviera
     `position: relative; z-index: 0` — o sea, .hero seguía "elevado"
     por encima de .stage igual que antes, solo que por un camino
     distinto. Por eso el síntoma se redujo (arreglé el problema de las
     esquinas redondeadas en la ronda anterior, que tapaba GRAN PARTE
     del síntoma) pero no desapareció del todo.
     Arreglo real esta vez: en vez de intentar que .hero "no compita"
     (imposible — necesita `position`/`transform` para contener el
     fondo interactivo de adentro, y eso SIEMPRE lo pone en la categoría
     "elevada"), le doy a .hero Y a .stage un nivel de apilamiento
     EXPLÍCITO y comparable — .hero en 0, .stage en 1 (ver .stage más
     abajo) — así .stage gana por número, sin depender de ninguna
     sutileza de qué "categoría" ocupa cada uno. */
  position: relative;
  z-index: 0;
  overflow: hidden; /* contiene el fondo interactivo — nunca se filtra hacia .stage */
  background: radial-gradient(circle at 20% 20%, #163a63 0%, #0b1220 55%, #05070c 100%);
  color: #f4f7fb;
  padding: 3.5rem 1.5rem 4.5rem;
  text-align: center;
}
.hero-inner { position: relative; z-index: 1; max-width: 46rem; margin: 0 auto; }
.brand { font-weight: 700; font-size: 1.1rem; letter-spacing: -0.01em; color: #c9d6e8; margin-bottom: 1.75rem; }
.brand em { color: #6cb2ff; font-style: normal; }
.hero h1 { font-size: clamp(1.6rem, 4vw, 2.5rem); line-height: 1.25; letter-spacing: -0.02em; margin: 0 0 0.85rem; }
.hero-sub { color: #b7c3d6; font-size: 1.05rem; margin: 0; }

/* ── El flujo de nodos, centro de la página ── */
.stage {
  /* La mitad del arreglo real de arriba: z-index EXPLÍCITAMENTE más
     alto que el de .hero (0) — garantiza que .stage se pinte encima de
     .hero donde se superponen (el margen negativo de acá abajo), sin
     depender de ninguna sutileza de "qué categoría de apilamiento"
     ocupa cada uno. Comparación directa de números, nada ambiguo. */
  position: relative;
  z-index: 1;
  max-width: 72rem;
  margin: -2.75rem auto 0;
  padding: 0 1.5rem 3.5rem;
}

/* .node-flow-wrap + .node-flow-backer: arreglo #2 del bug geométrico
   de las esquinas — 2ª vuelta, esta vez SIN depender de z-index ni de
   `overflow` en absoluto (la 1ª vuelta usaba un ::before con
   `z-index: -1` DENTRO de .node-flow, contenido por su `overflow`
   distinto de `visible` — y esa dependencia oculta fue justo lo que
   rompió el arreglo del scroll: sacar `overflow-x: auto` de .node-flow
   para que el panel flotante de .node-detail no se cortara, sin
   querer, también le sacó el recorte al ::before, que pasó a verse
   como un cuadrado tapando TODA la tarjeta en vez de solo las
   esquinas). Esta vez: .node-flow-backer es un elemento HERMANO, no un
   hijo — los dos ocupan la MISMA celda de un `display: grid`, y el
   orden en el HTML decide solo qué queda atrás/adelante (el backer
   primero, la tarjeta real después) — sin ningún z-index, sin
   `overflow`, nada que un arreglo futuro pueda romper sin querer. El
   grid estira al backer a la altura/ancho exactos de .node-flow
   automáticamente (comportamiento por defecto de CSS Grid), así que
   sigue funcionando aunque el contenido de los nodos cambie. */
.node-flow-wrap {
  display: grid;
  /* Margen de seguridad para el panel flotante de .node-detail (ver
     .node-detail más abajo): como ya no empuja el layout a propósito,
     necesita espacio VACÍO reservado abajo para no superponerse con el
     texto/botón que sigue (.stage-caption/.cta-wrap) cuando el panel
     del nodo con más texto esté abierto. Valor generoso, estimado para
     el peor caso (no medido pixel a pixel en un navegador real) — si
     algún texto futuro más largo todavía se corta, subir este número.
     En columna (celular) se anula, ver el media query de abajo: ahí el
     panel vuelve a empujar el layout normalmente, no hace falta
     reservar nada. */
  margin-bottom: 7rem;
}
.node-flow-backer,
.node-flow {
  grid-column: 1;
  grid-row: 1;
}
.node-flow-backer {
  background: var(--color-bg);
  /* BUG real encontrado por el usuario: tenía `border-radius: 0` parejo
     en las 4 esquinas — cuadrado en las 4. Arriba hace falta (rellena
     el huequito que deja el redondeado de .node-flow donde .hero se
     superpone) pero ABAJO nunca hizo falta ningún relleno (nunca hubo
     nada que tapar ahí) — el cuadrado del hermano sobresalía por detrás
     de la curva del contenedor real, sin ningún propósito. Ahora
     cuadrado SOLO arriba, redondeado abajo — mismo valor exacto que
     `border-radius` de .node-flow (ver esa regla) — así el hermano
     sigue el mismo contorno de .node-flow en las esquinas de abajo,
     sin sobresalir nada. */
  border-radius: 0 0 16px 16px;
}

.node-flow {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 0;
  background: var(--color-surface);
  border-radius: 16px;
  box-shadow: var(--shadow-lg);
  padding: 1.75rem 1.25rem;
  /* `overflow` NUNCA se toca en esta regla — ver el comentario grande
     de .node-flow-wrap arriba (por qué) y el de .node-detail más abajo
     (por qué el panel flotante lo necesita en `visible`, el valor por
     defecto). */
}

.node {
  /* `position: relative` VIVE ACÁ AHORA (antes solo en :hover/:focus) —
     BUG real encontrado por el usuario, arreglado: .node-detail crecía
     DENTRO del flujo normal (max-height 0 → 12rem), y como .node-flow
     usa `align-items: stretch`, TODAS las tarjetas se estiran para
     igualar a la más alta de la fila — el nodo con más texto
     ("Credenciales conectadas") empujaba TODA la fila (y la página)
     hacia abajo, apareciendo una barra de scroll. Ver .node-detail más
     abajo — ahora es un panel flotante, position:absolute, que
     necesita este `position: relative` como referencia SIEMPRE
     presente (no solo al pasar el mouse). */
  position: relative;
  flex: 1 1 0;
  min-width: 9.5rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 12px;
  padding: 1.1rem 1rem;
  text-align: center;
  cursor: default;
  outline: none;
  transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.28s ease, opacity 0.28s ease, filter 0.28s ease, border-color 0.2s ease;
}

/* Al pasar el mouse por CUALQUIER nodo del flujo, los demás bajan
   opacidad y se achican levemente — el nodo bajo el mouse (o con foco de
   teclado, para el respaldo táctil/accesibilidad) crece y se destaca. */
.node-flow:hover .node:not(:hover):not(:focus),
.node-flow:focus-within .node:not(:hover):not(:focus) {
  opacity: 0.45;
  transform: scale(0.95);
  filter: saturate(0.7);
}
.node:hover,
.node:focus {
  transform: translateY(-6px) scale(1.08);
  box-shadow: var(--shadow-md);
  border-color: var(--color-primary);
  z-index: 2;
}

.node-icon {
  width: 2.75rem;
  height: 2.75rem;
  margin: 0 auto 0.65rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-primary) 12%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.35rem;
}
.node-title { font-size: 0.95rem; font-weight: 700; margin: 0 0 0.25rem; letter-spacing: -0.01em; }
.node-sub { font-size: 0.8rem; font-weight: 600; color: var(--color-primary); margin: 0; }

/* Detalle: AHORA un panel flotante debajo del nodo (position:absolute),
   no un bloque que crece adentro — decisión confirmada con el usuario
   tras el bug real de arriba (el nodo con más texto empujaba TODA la
   fila y la página). Al quedar afuera del flujo normal, su alto NUNCA
   afecta el alto de .node-flow — pasar el mouse por CUALQUIER nodo,
   sin importar cuánto texto tenga, jamás mueve el resto de la página. */
.node-detail {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.6rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow-md);
  padding: 0.85rem 0.9rem;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  /* El `visibility` se retrasa a la salida (para que alcance a
     desvanecerse) pero NUNCA a la entrada (aparece de inmediato al
     pasar el mouse) — mismo truco que .cta usa implícitamente vía
     opacity, acá explícito porque hace falta ocultarlo también de
     lectores de pantalla/tab cuando no está visible. */
  transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0.22s;
  pointer-events: none; /* es solo texto informativo, no hace falta que sea clickeable */
  z-index: 5; /* por encima de los conectores y nodos vecinos, nunca por debajo */
}
.node:hover .node-detail,
.node:focus .node-detail {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0s;
}
.node-detail p {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--color-text-muted);
  text-align: left;
}
.node-detail strong { color: var(--color-text); }

.node--final .node-icon { background: color-mix(in srgb, #1baf7a 16%, transparent); }
.node--final .node-sub { color: #1baf7a; }

.connector {
  flex: 0 0 2.25rem;
  align-self: center;
  height: 2px;
  background: linear-gradient(90deg, var(--color-border), var(--color-primary), var(--color-border));
  position: relative;
  min-width: 1.5rem;
}
/* Puntos de luz "viajando" por los conectores (idea 1, sesión aparte) —
   100% CSS (@keyframes), sin JS: sugiere flujo constante de datos entre
   los 5 pasos. 2 puntos por conector (::before/::after), desfasados a
   mitad de camino uno del otro, para que se sienta como un goteo
   continuo en vez de un solo punto solitario. El delay por conector
   (nth-of-type) escalona el inicio entre los 4 conectores — nunca
   viajan todos sincronizados. Nota: nth-of-type cuenta TODOS los <div>
   hermanos (nodos incluidos), no solo .connector — por eso son 2, 4, 6,
   8 (las posiciones pares, ya que node/connector alternan siempre) en
   vez de 1, 2, 3, 4. Si se agrega o saca un nodo del flujo, hay que
   revisar estos números. */
.connector::before,
.connector::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0%;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 6px 1px color-mix(in srgb, var(--color-primary) 70%, transparent);
  transform: translate(-50%, -50%);
  animation: connector-travel 2.6s linear infinite;
  /* Delay NEGATIVO a propósito: arranca YA a mitad de recorrido en vez
     de hacer una pausa antes del primer viaje — se ve "vivo" desde el
     primer frame, no hay que esperar el primer ciclo. */
  animation-delay: calc(var(--connector-order, 0) * -0.65s);
}
/* El segundo punto de cada conector va medio ciclo (1.3s de 2.6s)
   desfasado del primero — un goteo continuo, no un único punto solitario. */
.connector::after { animation-delay: calc(var(--connector-order, 0) * -0.65s - 1.3s); }

.connector:nth-of-type(2) { --connector-order: 0; }
.connector:nth-of-type(4) { --connector-order: 1; }
.connector:nth-of-type(6) { --connector-order: 2; }
.connector:nth-of-type(8) { --connector-order: 3; }

@keyframes connector-travel {
  0% { left: 0%; opacity: 0; }
  12% { opacity: 1; }
  88% { opacity: 1; }
  100% { left: 100%; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .connector::before, .connector::after { animation: none; left: 50%; opacity: 1; }
}

.stage-caption {
  text-align: center;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  font-style: italic;
  margin: 1.25rem 0 0;
}

.foot {
  text-align: center;
  padding: 2rem 1.5rem 3rem;
  color: var(--color-text-muted);
  font-size: 0.85rem;
}

/* ── Botón de llamada a la acción (idea 3, sesión aparte) — destino
   placeholder a propósito (ver index.html, TODO marcado en un
   comentario HTML, nunca texto visible en la página): el usuario
   todavía no tiene el email/WhatsApp de negocio listos. Esto solo
   define la INTERACCIÓN (reacción satisfactoria al pasar el mouse) —
   el destino real se completa después, antes de mostrarle esto a
   cualquier prospecto. ── */
.cta-wrap { text-align: center; margin: 2.25rem 0 0; }
.cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 1.85rem;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
  color: #fff;
  font-weight: 700;
  font-size: 0.95rem;
  text-decoration: none;
  box-shadow: 0 6px 18px color-mix(in srgb, var(--color-primary) 35%, transparent);
  transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.22s ease;
}
.cta:hover, .cta:focus-visible {
  transform: translateY(-3px) scale(1.03);
  box-shadow: 0 14px 30px color-mix(in srgb, var(--color-primary) 45%, transparent);
}
.cta-arrow { display: inline-block; transition: transform 0.22s ease; }
.cta:hover .cta-arrow, .cta:focus-visible .cta-arrow { transform: translateX(4px); }

/* ── Animación de entrada al hacer scroll (idea 2, sesión aparte) —
   scroll-reveal.js agrega estas clases vía IntersectionObserver, UNA
   sola vez por elemento (nunca se re-dispara). Deliberadamente
   `animation` (@keyframes), NO `transition` — así nunca pisa la
   transition propia de .node (la del hover, 0.28s cubic-bezier) que
   ya existe más arriba; son mecanismos CSS independientes. Si JS falla
   o está desactivado, `.reveal-init` nunca se agrega — el contenido
   simplemente se ve normal (visible) desde el principio, nunca oculto
   por error. */
.reveal-init { opacity: 0; transform: translateY(24px); }
.reveal-init.reveal-in { animation: reveal-in 0.6s ease forwards; }
@keyframes reveal-in {
  from { opacity: 0; transform: translateY(24px); }
  to { opacity: 1; transform: translateY(0); }
}
/* Los 5 nodos entran en cascada, no todos a la vez — mismo motivo que
   el comentario de nth-of-type de los conectores: cuentan TODOS los
   <div> hermanos (nodo/conector alternan), por eso 1, 3, 5, 7, 9. */
.node.reveal-init:nth-of-type(1) { animation-delay: 0ms; }
.node.reveal-init:nth-of-type(3) { animation-delay: 90ms; }
.node.reveal-init:nth-of-type(5) { animation-delay: 180ms; }
.node.reveal-init:nth-of-type(7) { animation-delay: 270ms; }
.node.reveal-init:nth-of-type(9) { animation-delay: 360ms; }

@media (prefers-reduced-motion: reduce) {
  .reveal-init { opacity: 1; transform: none; }
  .reveal-init.reveal-in { animation: none; }
}

/* ── Angosto (celular, o una tablet en vertical): el flujo pasa a
   columna, los conectores rotan a verticales. Sigue siendo 0 JS.
   Punto de corte movido de 760px a 860px (bug real encontrado al
   arreglar el panel flotante de .node-detail): 5 nodos a su
   min-width (9.5rem) más el padding de .node-flow (2.5rem) y de
   .stage (3rem) necesitan mínimo 53rem = 848px para no apretarse —
   por debajo de eso, ANTES esto se resolvía con `overflow-x: auto` en
   .node-flow (scroll horizontal adentro de la tarjeta). Pero
   `overflow-x` distinto de `visible` OBLIGA a que `overflow-y` deje de
   ser `visible` también (regla real de CSS, no un descuido) — y eso
   recortaba el panel flotante nuevo, que necesita salirse verticalmente
   de la tarjeta sin que nada lo corte. Arreglo de raíz: subir el punto
   de corte a 860px (con margen sobre los 848px calculados, cubre
   anchos reales como una tablet en vertical de 768px) para que la fila
   NUNCA necesite `overflow-x`, y así puede quedar en `visible` en los
   dos ejes siempre. */
@media (max-width: 860px) {
  .node-flow-wrap { margin-bottom: 0; }
  .node-flow { flex-direction: column; }
  .node { min-width: 0; }
  .connector { width: 2px; height: 1.75rem; flex: 0 0 1.75rem; align-self: center; background: linear-gradient(180deg, var(--color-border), var(--color-primary), var(--color-border)); }
  /* Vertical acá — los puntos viajan de arriba a abajo, no de
     izquierda a derecha (mismo timing/stagger, otra geometría). */
  .connector::before, .connector::after { top: 0%; left: 50%; animation-name: connector-travel-vertical; }
  @keyframes connector-travel-vertical {
    0% { top: 0%; opacity: 0; }
    12% { opacity: 1; }
    88% { opacity: 1; }
    100% { top: 100%; opacity: 0; }
  }
  .node-flow:hover .node:not(:hover):not(:focus),
  .node-flow:focus-within .node:not(:hover):not(:focus) {
    transform: scale(0.97);
  }
  .node:hover, .node:focus { transform: scale(1.03); }

  /* El panel flotante de .node-detail (arreglo del bug de la fila que
     empujaba la página) NO funciona bien acá: en columna, los nodos
     están apilados muy cerca uno del otro — el panel del nodo de
     arriba se superpondría visualmente con la tarjeta del nodo de
     ABAJO. Acá NO hace falta el panel flotante de todos modos: en
     táctil, el hover real de mouse no aplica (es tap), así que
     "crece en el lugar" (el comportamiento original, antes del bug)
     es lo esperado — y en columna, un poco de scroll extra al tocar un
     nodo es un patrón de acordeón normal en celular, no el problema
     que se reportó (ese era específico de la fila en escritorio). */
  .node-detail {
    position: static;
    margin-top: 0;
    max-height: 0;
    opacity: 0;
    visibility: visible;
    overflow: hidden;
    box-shadow: none;
    border: none;
    padding: 0;
    transform: none;
    transition: max-height 0.3s ease, opacity 0.25s ease 0.05s, margin-top 0.3s ease;
  }
  .node:hover .node-detail,
  .node:focus .node-detail {
    max-height: 12rem;
    opacity: 1;
    margin-top: 0.75rem;
  }
}

/* ── Fondo interactivo del hero (aditivo, sesión aparte — ver
   interactive-bg.js). Spotlight que sigue el cursor + 3 manchas de
   color con parallax sutil. Alcance CONFINADO al hero a propósito
   (decisión confirmada con el usuario): .hero ya tiene overflow:hidden,
   así que nada de esto se filtra hacia .stage (los nodos), y el propio
   degradé oscuro del hero ya lo apaga hacia los bordes — no hace falta
   una máscara extra.
   --spot-x/--spot-y (posición del cursor, 0%-100% dentro del hero) y
   --tilt-x/--tilt-y (-1..1, offset del centro) los actualiza el JS vía
   requestAnimationFrame; los valores de acá son el estado por defecto
   (sin JS corriendo, con prefers-reduced-motion, o en un dispositivo
   táctil sin mouse) — el hero se ve bien igual, solo estático. ── */
.hero {
  --spot-x: 50%;
  --spot-y: 32%;
  --tilt-x: 0;
  --tilt-y: 0;
}
.hero-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none; /* nunca intercepta clics/hover, ni siquiera dentro del hero */
}
.spotlight {
  position: absolute;
  inset: 0;
  background: radial-gradient(600px circle at var(--spot-x) var(--spot-y), rgba(109, 178, 255, 0.16), transparent 62%);
  transition: background 0.05s linear; /* suaviza el paso entre frames del rAF, no una animación en sí */
}
.blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  opacity: 0.55;
  will-change: transform;
}
.blob-1 { width: 22rem; height: 22rem; top: -6rem; left: -4rem; background: #1d4ed8; transform: translate3d(calc(var(--tilt-x) * 18px), calc(var(--tilt-y) * 12px), 0); }
.blob-2 { width: 18rem; height: 18rem; top: 2rem; right: -5rem; background: #0ea5b7; transform: translate3d(calc(var(--tilt-x) * -14px), calc(var(--tilt-y) * 10px), 0); }
.blob-3 { width: 16rem; height: 16rem; bottom: -7rem; left: 38%; background: #7c3aed; opacity: 0.4; transform: translate3d(calc(var(--tilt-x) * 10px), calc(var(--tilt-y) * -14px), 0); }

/* Quien prefiere menos movimiento no debería recibir NINGUNO — el JS ya
   no se engancha en este caso (ver interactive-bg.js), esto es el
   respaldo puramente CSS por si algo falla igual. */
@media (prefers-reduced-motion: reduce) {
  .blob { transition: none; }
  .spotlight { transition: none; }
}

/* ── Fondo interactivo de TODA la página (cambio pedido después de ver
   la versión confinada al hero) — capa fija, DETRÁS de todo el
   contenido normal, con `z-index: -1` a propósito (en vez de
   `position: relative` + `z-index` positivo, que fue justo la causa
   del bug de arriba): un z-index NEGATIVO se pinta antes que cualquier
   contenido normal de la página, así que esta capa queda garantizada
   detrás de .hero, .stage y .foot sin importar nada más — nunca puede
   repetir el mismo bug.
   Nunca compite con el hover de los nodos por diseño: .node-flow tiene
   fondo OPACO (--color-surface), así que tapa esta capa por completo
   justo donde está la tarjeta — esto solo se ve alrededor (los
   márgenes de la página, y detrás del pie de página). Mucho más sutil
   que las manchas del hero (opacity bajo) porque acá convive con el
   contenido claro U oscuro del resto de la página (según el sistema),
   no con un fondo siempre oscuro garantizado como el del hero. ── */
:root {
  --page-spot-x: 50%;
  --page-spot-y: 30%;
  --page-tilt-x: 0;
  --page-tilt-y: 0;
}
.page-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
.page-spotlight {
  position: absolute;
  inset: 0;
  background: radial-gradient(560px circle at var(--page-spot-x) var(--page-spot-y), rgba(109, 178, 255, 0.1), transparent 65%);
}
.page-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(95px);
  opacity: 0.22;
  will-change: transform;
}
.page-blob-1 { width: 30rem; height: 30rem; top: 5%; left: -10%; background: #1d4ed8; transform: translate3d(calc(var(--page-tilt-x) * 16px), calc(var(--page-tilt-y) * 10px), 0); }
.page-blob-2 { width: 26rem; height: 26rem; top: 42%; right: -11%; background: #0ea5b7; transform: translate3d(calc(var(--page-tilt-x) * -14px), calc(var(--page-tilt-y) * 12px), 0); }
.page-blob-3 { width: 24rem; height: 24rem; bottom: 4%; left: 22%; background: #7c3aed; opacity: 0.16; transform: translate3d(calc(var(--page-tilt-x) * 10px), calc(var(--page-tilt-y) * -12px), 0); }

@media (prefers-reduced-motion: reduce) {
  .page-blob { transition: none; }
  .page-spotlight { transition: none; }
}
