/* ============================================================
   BLUFOX SERVICE — hoja de estilos de service.blufox.net
   Sitio autocontenido: publica esta carpeta como raíz de dominio,
   así que no puede referenciar ../css/. Los tokens son una copia
   literal del bloque :root de css/blufox.css (el contrato de marca);
   el resto se escribe aquí, sin arrastrar carrito, fichas de
   producto ni menú de cuenta, que esta landing no usa.

   Solo tema oscuro: el manual de marca manda 80% de dominancia
   oscura y el catálogo del que sale este contenido es oscuro.
   ============================================================ */

/* ------------------------------------------------------------
   1. TOKENS  (espejo de css/blufox.css)
   ------------------------------------------------------------ */
:root {
  color-scheme: dark;

  --void:      #080E12;
  --base:      #0C1419;
  --surface:   #121D24;
  --surface2:  #1A272F;

  --white:     #DCE4F2;
  --muted:     #8892A4;

  --cyan:      #31C3EA;
  --cyan-fill: #31C3EA;
  --on-accent: #04161D;
  --blue:      #CCD5E7;
  --cyan-dim:  rgba(49,195,234,0.10);
  --cyan-glow: rgba(49,195,234,0.30);

  --border:    rgba(49,195,234,0.16);
  --line-soft: rgba(220,228,242,0.08);

  --green:     #25D366;

  --f-display: 'Space Grotesk', system-ui, sans-serif;
  --f-body:    'Instrument Sans', system-ui, sans-serif;

  --r-xs:   6px;
  --r-sm:  10px;
  --r-md:  14px;
  --r-lg:  20px;
  --r-pill: 999px;

  --sp-section: clamp(72px, 9vw, 120px);
  --sp-gap:     24px;

  --sh-sm: 0 2px 8px rgba(4,10,14,0.35);
  --sh-md: 0 12px 32px rgba(4,10,14,0.45);
  --sh-lg: 0 28px 64px rgba(4,10,14,0.55);

  --ease-smooth: cubic-bezier(0.16, 1, 0.3, 1);

  --nav-h: 60px;
  --nav-bg: rgba(8,14,18,0.86);
  --nav-bg-solid: rgba(8,14,18,0.97);
  --nav-border: rgba(220,228,242,0.14);

  --maxw: 1180px;
}

/* ------------------------------------------------------------
   2. BASE
   ------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; scroll-padding-top: calc(var(--nav-h) + 16px); }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--void);
  color: var(--white);
  font-family: var(--f-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }

/* Titulares: Space Grotesk en peso ligero y caja baja, como el manual.
   La jerarquía la dan el tamaño y el color, nunca la negrita. */
h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-weight: 300;
  line-height: 1.12;
  letter-spacing: -0.015em;
  margin: 0;
  text-transform: lowercase;
}
h1 { font-size: clamp(2.1rem, 5vw, 3.4rem); }
h2 { font-size: clamp(1.75rem, 3.6vw, 2.6rem); }
h3 { font-size: clamp(1.1rem, 1.7vw, 1.35rem); font-weight: 400; }
h4 { font-size: 1rem; font-weight: 400; }

p { margin: 0; }

/* La frase clave del titular va en cian. Es el recurso de énfasis del
   manual (láminas 4, 55 y 56), en lugar de la negrita. */
.ac { color: var(--cyan); }

/* Titular que se escribe solo (TextType de reactbits). Las frases van en el
   atributo `data-frases` del propio span, separadas por "|", y la primera es
   la que ya está escrita en el HTML: sin JS el titular se lee igual.

   OJO: el componente original trae `display: inline-block`. Aquí va `inline`
   a propósito. Un inline-block es una caja atómica y no parte a mitad de
   línea, así que el acento se iba entero al renglón siguiente: el titular de
   la portada pasaba de 122 px a 183 px de alto, una línea de más. */
.text-type {
  display: inline;
  white-space: pre-wrap;
}
.text-type__cursor {
  margin-left: 0.25rem;
  display: inline-block;
  opacity: 1;
  color: var(--cyan);
  animation: parpadeo 1.05s steps(1) infinite;
}
/* Se aplica cuando el tipeo está en pausa: fuera de pantalla o en otra
   pestaña no tiene sentido dejar un cursor parpadeando. */
.text-type__cursor--hidden { display: none; }
@keyframes parpadeo {
  0%, 50%      { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

/* Texto que solo leen los lectores de pantalla. Lo usa el tipeo: lo que se
   anima queda `aria-hidden` —un texto que cambia letra a letra no hay quien
   lo lea— y al lado queda el titular real, completo. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

.container { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--cyan); color: var(--on-accent);
  padding: 10px 16px; border-radius: 0 0 var(--r-sm) 0; font-weight: 600;
}
.skip-link:focus { left: 0; }

/* ------------------------------------------------------------
   3. NAV
   ------------------------------------------------------------ */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 50;
  height: var(--nav-h);
  background: var(--nav-bg);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid transparent;
  transition: background .25s var(--ease-smooth), border-color .25s var(--ease-smooth);
}
.nav.scrolled { background: var(--nav-bg-solid); border-bottom-color: var(--nav-border); }

.nav-inner {
  max-width: var(--maxw); margin: 0 auto; padding: 0 24px;
  height: 100%; display: flex; align-items: center; gap: 28px;
}
.nav-logo { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.nav-logo img { height: 26px; width: auto; }
.nav-sub {
  font-family: var(--f-display); font-size: .72rem; letter-spacing: .22em;
  color: var(--muted); text-transform: lowercase; padding-top: 2px;
}

.nav-links { display: flex; align-items: center; gap: 26px; margin: 0 0 0 auto; padding: 0; list-style: none; }
.nav-links a {
  font-size: .93rem; color: var(--white); opacity: .82;
  transition: opacity .2s, color .2s;
}
.nav-links a:hover { opacity: 1; color: var(--cyan); }
.nav-links a[aria-current="page"] { color: var(--cyan); opacity: 1; }
/* El padre de la sección en la que estás —«Páginas web» mientras lees un
   nivel— se marca más flojo que la página actual. */
.nav-links a[aria-current="true"] { color: var(--white); opacity: 1; }

.nav-cta {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--cyan-fill); color: var(--on-accent) !important;
  padding: 9px 18px; border-radius: var(--r-pill);
  font-weight: 600; font-size: .9rem; opacity: 1 !important;
  transition: transform .2s var(--ease-smooth), box-shadow .2s;
}
.nav-cta:hover { transform: translateY(-1px); box-shadow: 0 6px 20px var(--cyan-glow); }

.nav-toggle {
  display: none; margin-left: auto;
  background: none; border: 1px solid var(--nav-border); border-radius: var(--r-sm);
  color: var(--white); width: 40px; height: 36px; align-items: center; justify-content: center;
  cursor: pointer;
}

@media (max-width: 900px) {
  .nav-sub { display: none; }
  .nav-toggle { display: inline-flex; }
  .nav-links {
    position: fixed; inset: var(--nav-h) 0 auto 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--nav-bg-solid); border-bottom: 1px solid var(--nav-border);
    padding: 8px 24px 20px;
    margin: 0;
    display: none;
  }
  .nav-links.open { display: flex; }
  .nav-links li { border-bottom: 1px solid var(--line-soft); }
  .nav-links li:last-child { border-bottom: 0; padding-top: 14px; }
  .nav-links a { display: block; padding: 14px 0; font-size: 1rem; }
  .nav-cta { justify-content: center; }
}

/* ------------------------------------------------------------
   4. SECCIONES Y CABECERAS
   ------------------------------------------------------------ */
section { padding: var(--sp-section) 0; }
section + section { padding-top: 0; }

/* Rótulo de dos líneas: blanco arriba, gris abajo. Es el patrón de
   cabecera del manual de marca. */
.eyebrow { margin-bottom: 26px; }
.eyebrow b {
  display: block; font-family: var(--f-display); font-weight: 400;
  font-size: .95rem; color: var(--white); text-transform: lowercase;
}
.eyebrow span {
  display: block; font-family: var(--f-display); font-weight: 300;
  font-size: .95rem; color: var(--muted); text-transform: lowercase;
}

.lead {
  margin-top: 20px; max-width: 62ch;
  color: var(--muted); font-size: 1.05rem; line-height: 1.65;
}
.section-head { margin-bottom: 52px; }

/* ------------------------------------------------------------
   5. HERO
   ------------------------------------------------------------ */
.hero {
  position: relative; overflow: hidden;
  padding: calc(var(--nav-h) + clamp(64px, 9vw, 120px)) 0 clamp(64px, 8vw, 104px);
  background:
    radial-gradient(900px 420px at 78% 12%, var(--cyan-dim), transparent 62%),
    var(--void);
}
/* ---- Haces del hero ----
   El original (reactbits Beams) es una escena WebGL: prismas alargados con
   una luz direccional y ruido encima, sobre three + react-three-fiber. Aquí
   se reproduce el aspecto con degradados: doce franjas suaves que derivan
   dentro de un plano girado, más una capa de grano. Cuesta ~1 KB en vez de
   600 KB de three.js autoalojado, no abre un contexto WebGL en el móvil del
   cliente y solo anima `transform` y `opacity`, que van por GPU. */
.hero-beams {
  position: absolute; inset: 0; overflow: hidden;
  pointer-events: none; z-index: 0;
  /* El plano se desvanece hacia abajo para no pelear con el texto ni con
     el borde de la sección. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 55%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 55%, transparent 100%);
}
.hero-beams .campo {
  position: absolute; top: -60%; left: -30%;
  width: 160%; height: 220%;
  transform: rotate(-24deg);
}
/* Los bordes se suavizan con una máscara horizontal, no con `filter: blur`.
   Un blur sobre esta capa obliga al navegador a rehacer el desenfoque en
   cada cuadro porque los hijos se mueven dentro; la máscara viaja con el
   elemento y no cuesta nada. */
.hero-beams i {
  position: absolute; top: 0; left: var(--x);
  width: var(--w); height: 100%;
  background: linear-gradient(to bottom,
    transparent 0%,
    rgba(49,195,234,var(--o)) 38%,
    rgba(204,213,230,calc(var(--o) * .5)) 55%,
    transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 45%, #000 55%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 45%, #000 55%, transparent);
  animation: haz var(--d) linear infinite;
  animation-delay: var(--t);
  will-change: transform;
}
/* En pantallas pequeñas basta con la mitad: menos capas que componer. */
@media (max-width: 700px) {
  .hero-beams i:nth-child(2n) { display: none; }
}
@keyframes haz {
  from { transform: translateY(105%); }
  to   { transform: translateY(-105%); }
}

/* Lienzo de la rejilla. Se inserta por JS: si WebGL no está disponible o el
   contexto se pierde, nunca aparece y quedan los haces en CSS. */
.hero-ripple {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 58%, transparent 100%);
}
/* El lienzo va a resolución completa: son líneas finas y estirarlas desde
   media resolución las deja sucias. Se puede permitir porque el shader es
   barato: dos senos y un puñado de exponenciales por píxel. */
.hero-ripple canvas { width: 100%; height: 100%; display: block; }
.hero.has-ripple .hero-beams { display: none; }

/* Sombra bajo el texto del hero. La rejilla es del mismo cian que el acento
   del titular, así que donde se cruzan las líneas con las letras no se
   distingue una cosa de la otra. Esto la apaga por la izquierda —que es donde
   vive el texto, `hero-inner` va a 860 px— y la deja intacta a la derecha.
   Va en `::after` porque el contenido generado se coloca como último hijo, y
   el lienzo lo inserta el JS al final: así queda por encima de la rejilla y
   por debajo del texto, que va en z-index 1. */
.hero.has-ripple::after {
  content: "";
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(96deg,
    rgba(8, 14, 18, .94) 0%,
    rgba(8, 14, 18, .88) 34%,
    rgba(8, 14, 18, .55) 58%,
    rgba(8, 14, 18, .12) 78%,
    transparent 92%);
}
/* `hero-inner` va a 860 px, así que por encima de ~1000 el titular ocupa dos
   tercios y la rejilla puede quedarse entera a la derecha. Por debajo el
   texto llega hasta el borde —medido: al 96% del ancho a 900 px— y ahí la
   sombra tiene que cubrir de lado a lado. */
@media (max-width: 1000px) {
  .hero.has-ripple::after {
    background: linear-gradient(96deg,
      rgba(8, 14, 18, .94) 0%,
      rgba(8, 14, 18, .90) 60%,
      rgba(8, 14, 18, .72) 100%);
  }
}

/* Grano: SVG en data URI, permitido por `img-src 'self' data:`. */
.hero-grain {
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  opacity: .055;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) {
  .hero-beams i { animation: none; transform: translateY(0); }
}

.hero-inner { position: relative; z-index: 1; max-width: 860px; }
.hero h1 { margin-bottom: 26px; }
.hero .lead { font-size: 1.15rem; max-width: 58ch; }

.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 38px; }

.btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 14px 24px; border-radius: var(--r-pill);
  font-family: var(--f-body); font-weight: 600; font-size: .98rem;
  border: 1px solid transparent; cursor: pointer;
  transition: transform .2s var(--ease-smooth), box-shadow .2s, background .2s, border-color .2s;
}
.btn-primary { background: var(--cyan-fill); color: var(--on-accent); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 28px var(--cyan-glow); }
.btn-ghost { border-color: var(--border); color: var(--white); background: transparent; }
.btn-ghost:hover { border-color: var(--cyan); color: var(--cyan); }
.btn svg { flex-shrink: 0; }

.hero-meta {
  display: flex; flex-wrap: wrap; gap: 10px 28px; margin-top: 36px;
  color: var(--muted); font-size: .9rem;
}
.hero-meta span { display: inline-flex; align-items: center; gap: 8px; }
.hero-meta .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--cyan); }

/* ------------------------------------------------------------
   6. REJILLAS Y TARJETAS
   ------------------------------------------------------------ */
.grid { display: grid; gap: var(--sp-gap); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }

.card {
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  padding: 28px;
}
.card-hi { background: var(--surface2); border-color: var(--border); }
.card h3 { margin-bottom: 12px; }
.card p { color: var(--muted); font-size: .95rem; }

/* Insignia numerada: el motivo que se repite en el catálogo. */
.badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: var(--r-pill);
  border: 1px solid var(--cyan); color: var(--cyan);
  font-family: var(--f-display); font-size: .82rem; font-weight: 400;
  margin-bottom: 20px;
}
.tag {
  display: block; margin-top: 18px;
  font-family: var(--f-display); font-size: .82rem; color: var(--cyan);
  text-transform: lowercase;
}
.card-kicker {
  font-family: var(--f-display); font-size: .82rem; color: var(--cyan);
  text-transform: lowercase; margin-bottom: 10px;
}

/* Tarjeta de servicio enlazable */
a.card { display: block; transition: transform .22s var(--ease-smooth), border-color .22s, background .22s; }
a.card:hover { transform: translateY(-3px); border-color: var(--border); background: var(--surface2); }
/* En bloque, no en línea: si no, la etiqueta y el enlace se pegan en la
   misma línea dentro de la tarjeta. */
/* Precio de un nivel. La cifra en cian y en display; lo que cubre, al lado y
   en gris, para que nadie lea «299» y entienda que ahí está todo incluido. */
.precio {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 9px;
  margin: 16px 0 6px;
}
.precio b {
  font-family: var(--f-display); font-weight: 300;
  font-size: 1.9rem; line-height: 1; letter-spacing: -.02em;
  color: var(--cyan);
}
.precio span { color: var(--muted); font-size: .85rem; }

/* Las tres cifras juntas en la banda de cierre. */
.precios-fila {
  display: flex; flex-wrap: wrap; gap: 14px 44px;
  margin: 0 0 28px;
}
.precios-fila span {
  display: flex; flex-direction: column; gap: 4px;
  color: var(--muted); font-size: .9rem; text-transform: lowercase;
}
.precios-fila b {
  font-family: var(--f-display); font-weight: 300;
  font-size: clamp(1.7rem, 3vw, 2.2rem); line-height: 1; letter-spacing: -.02em;
  color: var(--cyan);
}

.card-more {
  display: flex; align-items: center; gap: 7px; margin-top: 16px;
  color: var(--cyan); font-size: .9rem; font-weight: 600;
}

/* ------------------------------------------------------------
   7. LISTAS
   ------------------------------------------------------------ */
.ticks { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.ticks li {
  position: relative; padding-left: 26px;
  color: var(--blue); font-size: .95rem; line-height: 1.55;
}
.ticks li::before {
  content: ""; position: absolute; left: 0; top: .62em;
  width: 12px; height: 2px; background: var(--cyan);
}
.ticks.compact li { font-size: .9rem; }

/* Filas numeradas con divisor, como la lámina de recepción */
.rows { display: grid; gap: 0; }
.row {
  display: grid; grid-template-columns: 44px minmax(0, 1fr) minmax(0, 1.6fr);
  gap: 18px; align-items: start;
  padding: 22px 0; border-bottom: 1px solid var(--line-soft);
}
.row:last-child { border-bottom: 0; }
.row-n {
  font-family: var(--f-display); font-size: .82rem; color: var(--cyan);
  border: 1px solid var(--cyan); border-radius: var(--r-pill);
  width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
}
.row-t { font-family: var(--f-display); font-weight: 400; font-size: 1.05rem; }
.row-d { color: var(--muted); font-size: .95rem; }
@media (max-width: 780px) {
  .row { grid-template-columns: 34px minmax(0, 1fr); }
  .row-d { grid-column: 2; }
}

/* ------------------------------------------------------------
   8. FLUJO DE PASOS
   ------------------------------------------------------------ */
.flow { display: grid; gap: 16px; grid-template-columns: repeat(5, 1fr); }
.flow.flow-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1000px) { .flow, .flow.flow-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px)  { .flow, .flow.flow-4 { grid-template-columns: 1fr; } }

.step {
  background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: var(--r-md); padding: 22px;
}
.step .badge { width: 32px; height: 32px; font-size: .75rem; margin-bottom: 16px; }
.step h3 { font-size: 1rem; margin-bottom: 8px; }
.step p { color: var(--muted); font-size: .87rem; line-height: 1.5; }

/* ------------------------------------------------------------
   9. TABLAS
   ------------------------------------------------------------ */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table {
  width: 100%; border-collapse: collapse; min-width: 620px;
  font-size: .95rem;
}
thead th {
  text-align: left; padding: 0 14px 12px;
  font-family: var(--f-display); font-weight: 400; font-size: .85rem;
  color: var(--cyan); text-transform: lowercase;
  border-bottom: 2px solid var(--cyan);
}
tbody td { padding: 15px 14px; border-bottom: 1px solid var(--line-soft); color: var(--blue); }
tbody tr:nth-child(odd) td { background: var(--surface); }
tbody th {
  text-align: left; padding: 15px 14px; border-bottom: 1px solid var(--line-soft);
  font-family: var(--f-display); font-weight: 400; color: var(--white);
}
tbody tr:nth-child(odd) th { background: var(--surface); }
td.no { color: var(--muted); }

/* ------------------------------------------------------------
   10. CIFRAS
   ------------------------------------------------------------ */
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-gap); }
@media (max-width: 640px) { .stats { grid-template-columns: 1fr; } }
.stat b {
  display: block; font-family: var(--f-display); font-weight: 300;
  font-size: clamp(2rem, 4vw, 2.8rem); color: var(--cyan); line-height: 1;
}
.stat span { display: block; margin-top: 10px; color: var(--muted); font-size: .92rem; }

/* ------------------------------------------------------------
   11. DIAGRAMA DE CONVERGENCIA — órbitas
   Adaptación en CSS puro del componente OrbitingCircles (React).
   Cada ficha recorre su propia órbita: no hay un contenedor que
   gire, así que no hace falta contrarrotar a los hijos. El desfase
   entre fichas se logra con animation-delay negativo, y el
   `rotate(-360deg)` final mantiene el icono derecho todo el giro.
   ------------------------------------------------------------ */
.converge { display: grid; justify-items: center; gap: 0; }
.converge-out { width: 100%; margin-top: 0; }

/* ---- Haces animados (adaptación de AnimatedBeam) ----
   El original mide los nodos con ResizeObserver y dibuja la curva en JS.
   Aquí el trazado es fijo, así que basta con que el SVG y los nodos
   compartan sistema de coordenadas: el contenedor tiene aspect-ratio
   900/260, el viewBox es el mismo, y los nodos se anclan en porcentajes
   de esos 900x260. Así encajan a cualquier ancho sin una línea de JS.
   El pulso es un guion que recorre el trazo con stroke-dashoffset, no un
   degradado animado, para que `prefers-reduced-motion` lo apague solo. */
.beams {
  position: relative;
  width: 100%; max-width: 900px;
  aspect-ratio: 900 / 260;
  margin-bottom: 46px;
}
/* Hijo directo, no descendiente: si no, la regla también alcanza a los
   iconos que van dentro de los nodos y los estira sobre el texto. */
.beams > svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.beam-base { fill: none; stroke: var(--line-soft); stroke-width: 2; stroke-linecap: round; }
.beam-flow {
  fill: none; stroke: var(--cyan); stroke-width: 2.5; stroke-linecap: round;
  stroke-dasharray: 22 458;
  filter: drop-shadow(0 0 4px var(--cyan-glow));
  animation: beam-run 3.4s linear infinite;
}
.beam-flow.b2 { animation-delay: -1.1s; }
.beam-flow.b3 { animation-delay: -2.2s; }
@keyframes beam-run { from { stroke-dashoffset: 480; } to { stroke-dashoffset: 0; } }

.beam-node {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: 10px;
  background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: var(--r-sm); padding: 11px 16px;
  white-space: nowrap; box-shadow: var(--sh-sm);
}
.beam-node span[data-icon] { color: var(--cyan); display: grid; place-items: center; }
.beam-node b {
  font-family: var(--f-display); font-weight: 400; font-size: .95rem;
  color: var(--white); text-transform: lowercase;
}
.beam-node.core {
  background: var(--surface2); border-color: var(--cyan);
  padding: 15px 20px; box-shadow: 0 0 30px var(--cyan-dim);
}
.beam-node.core b { color: var(--cyan); font-size: 1.05rem; }

@media (prefers-reduced-motion: reduce) {
  .beam-flow { animation: none; stroke-dasharray: none; opacity: .45; }
}

/* Bajo 860px el diagrama se aprieta y los nodos se pisan: se cambia por la
   misma información en columna, que es lo que ya cuentan las tarjetas. */
@media (max-width: 860px) { .beams { display: none; } }

/* Los radios ceden ante el ancho disponible: si no, en pantallas estrechas
   las fichas se salen del contenedor y empujan la página entera. El
   overflow es la red de seguridad para anchos absurdos. */
.orbit-scene {
  --r-out: min(150px, 32vw);
  --r-in:  min(82px, 18vw);
  position: relative;
  width: 100%;
  height: calc(var(--r-out) * 2 + 80px);
  display: grid; place-items: center;
  margin-bottom: 44px;
  overflow: hidden;
}

/* Los aros: sin ellos las fichas parecen flotar sin sistema. */
.orbit-ring {
  position: absolute; top: 50%; left: 50%;
  border: 1px dashed var(--line-soft); border-radius: 50%;
  transform: translate(-50%, -50%);
}
.orbit-ring.out { width: calc(var(--r-out) * 2); height: calc(var(--r-out) * 2); }
.orbit-ring.in  { width: calc(var(--r-in) * 2);  height: calc(var(--r-in) * 2); border-color: var(--border); }

.orbit-core {
  position: relative; z-index: 2;
  background: var(--surface2); border: 1px solid var(--cyan);
  border-radius: var(--r-md); padding: 18px 24px; text-align: center;
  max-width: 220px;
  box-shadow: 0 0 0 8px var(--void), 0 0 34px var(--cyan-dim);
}
.orbit-core b {
  display: block; font-family: var(--f-display); font-weight: 400;
  font-size: 1.05rem; color: var(--cyan); text-transform: lowercase;
}
.orbit-core span { display: block; margin-top: 4px; color: var(--muted); font-size: .8rem; line-height: 1.4; }

@keyframes orbita {
  from { transform: rotate(0deg)   translateY(calc(-1 * var(--r))) rotate(0deg); }
  to   { transform: rotate(360deg) translateY(calc(-1 * var(--r))) rotate(-360deg); }
}

.orbit-item {
  --size: 44px;
  position: absolute; top: 50%; left: 50%;
  width: var(--size); height: var(--size);
  margin: calc(var(--size) / -2) 0 0 calc(var(--size) / -2);
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--surface); border: 1px solid var(--border);
  color: var(--cyan); box-shadow: var(--sh-sm);
  animation: orbita var(--dur) linear infinite;
  animation-delay: calc(var(--dur) / var(--n) * var(--i) * -1);
}
.orbit-item.rev { animation-direction: reverse; }
.orbit-item.dato {
  --size: 34px; --r: var(--r-in);
  color: var(--muted); background: var(--base); border-color: var(--line-soft);
}
.orbit-item.canal { --r: var(--r-out); }

/* Etiqueta del canal: aparece pegada a la ficha, sin girar con ella. */
.orbit-item .orbit-label {
  position: absolute; top: calc(100% + 7px); left: 50%; transform: translateX(-50%);
  font-family: var(--f-display); font-size: .72rem; font-style: normal;
  color: var(--muted); text-transform: lowercase;
  white-space: nowrap; pointer-events: none;
}

@media (max-width: 640px) {
  .orbit-scene { --r-out: 108px; --r-in: 60px; height: calc(var(--r-out) * 2 + 70px); }
  .orbit-item { --size: 38px; }
  .orbit-item.dato { --size: 28px; }
  .orbit-item .orbit-label { display: none; }
}

/* Sin movimiento: las fichas se quedan repartidas en su órbita. */
@media (prefers-reduced-motion: reduce) {
  .orbit-item {
    animation: none;
    transform: rotate(calc(360deg / var(--n) * var(--i)))
               translateY(calc(-1 * var(--r)))
               rotate(calc(-360deg / var(--n) * var(--i)));
  }
}

/* ------------------------------------------------------------
   11b. MAPA DE PUNTOS
   El SVG se genera con scripts/generar-mapa-service.py y se sirve como
   archivo aparte: son ~2.100 puntos y en línea engordarían el HTML. Las
   animaciones declarativas de un SVG sí corren dentro de un <img>; los
   scripts no, y aquí no hace falta ninguno.
   ------------------------------------------------------------ */
.mapa-wrap { position: relative; }
.mapa-wrap img { width: 100%; height: auto; }
.mapa-pies {
  display: flex; flex-wrap: wrap; gap: 12px 28px; margin-top: 26px;
}
.mapa-pie { display: flex; align-items: baseline; gap: 10px; }
.mapa-pie i {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--cyan); box-shadow: 0 0 0 4px var(--cyan-dim);
  flex-shrink: 0; align-self: center;
}
.mapa-pie b {
  font-family: var(--f-display); font-weight: 400; font-size: .95rem;
  color: var(--white); text-transform: lowercase;
}
.mapa-pie span { color: var(--muted); font-size: .9rem; }

/* ------------------------------------------------------------
   11-bis. PARALAJE DE PIEZAS   (HeroParallax de aceternity)

   El original es React con motion: 300vh de alto, `perspective: 1000px`,
   tres filas que se desplazan en horizontal mientras la escena entera se
   endereza. Aquí las cifras son las mismas y el movimiento lo calcula
   `initParallax` en js/site.js contra el progreso de scroll de la sección.

   En /paginas-web/ ESTE es el hero, así que el mismo `<header>` lleva
   `class="hero parallax"`. Ahí las fichas son el fondo: ni la rejilla WebGL
   ni los haces se dibujan, porque dos efectos peleándose en la misma caja no
   dejan ver ninguno.
   ------------------------------------------------------------ */
.parallax {
  position: relative;
  height: 300vh;
  overflow: hidden;
  perspective: 1000px;
  transform-style: preserve-3d;
}
/* El 300vh de arriba es el del componente y solo vale de punto de partida:
   `initParallax` lo sustituye por el alto exacto que necesitan las filas,
   para que no quede un hueco vacío al final. Sin JavaScript no hay ni
   movimiento ni recorte y las filas se ven quietas. */
html:not(.js) .parallax { height: auto; overflow: visible; }

.parallax-escena {
  transform-style: preserve-3d;
  will-change: transform, opacity;
}
.parallax-fila {
  display: flex;
  gap: 80px;
  margin-bottom: 80px;
  will-change: transform;
}
/* Las filas 1 y 3 se arman desde la derecha y la 2 desde la izquierda: es lo
   que hace que se desplacen en sentidos contrarios sin más cálculo. */
.parallax-fila.rev { flex-direction: row-reverse; }
.parallax-fila:last-child { margin-bottom: 0; }

/* Apilado dentro del hero: rejilla y haces al fondo (aunque aquí no se
   dibujen), fichas en medio, texto arriba. */
.hero.parallax .parallax-escena { position: relative; z-index: 1; }
.hero.parallax .hero-inner { z-index: 2; }
.hero.parallax .hero-beams { display: none; }

.parallax-ficha {
  position: relative;
  display: flex; flex-direction: column;
  flex: 0 0 auto;
  width: 30rem; height: 24rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  overflow: hidden;
  transition: transform .28s ease, border-color .28s ease, box-shadow .28s ease;
}
/* El `whileHover={{ y: -20 }}` del original. */
.parallax-fila:hover .parallax-ficha:hover {
  transform: translateY(-20px);
  border-color: var(--cyan);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .45);
}

/* Cada ficha es una ventana de navegador: la barra la dibuja el CSS y dentro
   va la captura real de la maqueta. Las capturas salen de ../maquetas-web,
   que es un proyecto aparte con las 17 páginas HTML y su render con Chrome.

   La barra imita la de un Mac: los tres semáforos a la izquierda con sus
   colores de verdad y la dirección centrada, como Safari. Es lo que hace que
   se lea como «una web abierta» de un vistazo y a tamaño de miniatura. */
.vent-barra {
  position: relative;
  display: flex; align-items: center; gap: 6px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface2);
  flex: 0 0 auto;
  /* La dirección va absoluta, así que no cuenta para el alto: sin este suelo
     la barra la marcarían los semáforos de 7 px y la píldora quedaría pegada
     a los bordes. Con 26 le quedan casi 5 px por arriba y por abajo. */
  min-height: 26px;
}
.vent-barra i {
  width: 7px; height: 7px; border-radius: 50%;
  flex: 0 0 auto;
}
.vent-barra i:nth-of-type(1) { background: #FF5F57; }
.vent-barra i:nth-of-type(2) { background: #FEBC2E; }
.vent-barra i:nth-of-type(3) { background: #28C840; }
/* Centrada de verdad respecto a la ficha, no después de los semáforos: por
   eso va absoluta y no en el flujo del flex. */
.vent-ruta {
  position: absolute; left: 50%; transform: translateX(-50%);
  /* Va centrada en la barra, así que hay que reservarle a los dos lados el
     sitio que ocupan los semáforos —45 px— más un respiro. Con un tope
     simétrico no puede pisarlos por mucho que crezca la dirección. */
  max-width: calc(100% - 114px);
  padding: 1px 12px;
  border-radius: 5px;
  background: var(--void);
  color: var(--muted);
  font-size: .68rem; line-height: 1.35; letter-spacing: .01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* La captura se queda con lo que sobra de la ficha. Así el alto de la barra
   se puede tocar sin tener que cuadrar ningún número a mano. */
.vent-captura {
  display: block;
  flex: 1 1 auto; min-height: 0;
  width: 100%;
  object-fit: cover; object-position: top center;
  background: var(--surface2);
}

.parallax-ficha__titulo {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 34px 20px 16px;
  background: linear-gradient(to top, var(--void) 55%, transparent);
  font-family: var(--f-display); font-weight: 300; font-size: 1.15rem;
  text-transform: lowercase; color: var(--white);
}

/* En pantallas chicas se apaga el 3D entero: dos mil y pico píxeles de
   tarjetas girando es caro y encima casi no se ven. Quedan tres tiras que se
   arrastran con el dedo. Lo mismo con animaciones reducidas. */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .parallax { height: auto; overflow: visible; perspective: none; padding-bottom: 20px; }
  .parallax-escena { transform: none !important; opacity: 1 !important; }
  /* Con `.rev` incluido: la clase tiene más peso que el selector suelto y sin
     esto las filas 1 y 3 se quedaban al revés, arrancando por la última ficha. */
  .parallax-fila,
  .parallax-fila.rev {
    flex-direction: row;
    transform: none !important;
    gap: 18px; margin-bottom: 18px;
    overflow-x: auto; scroll-snap-type: x mandatory;
    padding-bottom: 6px;
    -webkit-overflow-scrolling: touch;
  }
  .parallax-ficha { width: 19rem; height: 15rem; scroll-snap-align: start; }
  .parallax-fila:hover .parallax-ficha:hover { transform: none; }
}

/* ------------------------------------------------------------
   12. BANDA DE CIERRE
   ------------------------------------------------------------ */
.cta-band {
  background:
    radial-gradient(720px 320px at 20% 10%, var(--cyan-dim), transparent 65%),
    var(--surface);
  border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: clamp(34px, 5vw, 56px);
}
.cta-band h2 { margin-bottom: 16px; }
.cta-band .lead { margin-bottom: 30px; }

.contact-list { display: grid; gap: 14px; margin-top: 34px; }
.contact-list div { display: flex; gap: 16px; align-items: baseline; }
.contact-list dt {
  font-family: var(--f-display); font-size: .85rem; color: var(--muted);
  text-transform: lowercase; min-width: 96px;
}
.contact-list dd { margin: 0; font-family: var(--f-display); color: var(--blue); }

/* ------------------------------------------------------------
   13. PIE
   ------------------------------------------------------------ */
.site-footer {
  border-top: 1px solid var(--line-soft);
  padding: 56px 0 34px; margin-top: var(--sp-section);
  background: var(--base);
}
.footer-inner {
  display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: 40px;
}
@media (max-width: 780px) { .footer-inner { grid-template-columns: 1fr; gap: 30px; } }
.footer-logo { height: 28px; width: auto; }
.footer-tagline { margin-top: 16px; color: var(--muted); font-size: .92rem; max-width: 42ch; }
.footer-col-title {
  font-family: var(--f-display); font-size: .88rem; color: var(--white);
  text-transform: lowercase; margin-bottom: 14px;
}
.footer-links { display: grid; gap: 10px; }
.footer-links a { color: var(--muted); font-size: .92rem; transition: color .2s; }
.footer-links a:hover { color: var(--cyan); }
.footer-bottom {
  display: flex; flex-wrap: wrap; gap: 10px 24px; justify-content: space-between;
  margin-top: 44px; padding-top: 22px; border-top: 1px solid var(--line-soft);
  color: var(--muted); font-size: .84rem;
}

/* ------------------------------------------------------------
   14. FLOTANTE DE WHATSAPP
   ------------------------------------------------------------ */
.wa-float {
  position: fixed; right: 18px; bottom: 18px; z-index: 40;
  width: 54px; height: 54px; border-radius: var(--r-pill);
  background: var(--green); color: #04150B;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: var(--sh-md);
  transition: transform .2s var(--ease-smooth);
}
.wa-float:hover { transform: scale(1.06); }

/* ------------------------------------------------------------
   15. REVELADO
   ------------------------------------------------------------ */
.reveal { opacity: 0; transform: translateY(18px); }
.reveal.visible {
  opacity: 1; transform: none;
  transition: opacity .6s var(--ease-smooth), transform .6s var(--ease-smooth);
}
/* Sin JS nunca se queda contenido invisible esperando un evento. */
html:not(.js) .reveal { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal.visible { opacity: 1; transform: none; transition: none; }
}
