/* =====================================================================
   floating-contact.css -- Boton flotante de contacto «?Hablamos?»

   Componente compartido por las 8 paginas que lo muestran:
     /  ·  /en/  ·  /conocimiento/  ·  /en/conocimiento/
     /rendimiento/  ·  /en/rendimiento/  ·  /sphera.html  ·  /en/sphera.html

   Extraido de home.css el 2026-07-27, cuando el boton dejo de ser exclusivo de
   la portada. Antes vivia incrustado alli y solo lo cargaban las dos portadas;
   replicarlo copiando y pegando habria dejado 3 copias del CSS y 8 del JS
   desincronizandose. Mismo motivo por el que home.css se extrajo de index.html
   en su dia.

   Se acompana de /js/floating-contact.js. Ninguno de los dos sirve sin el otro.

   REVELADO -- dos caminos, y esto importa:
     · Portada: la coreografia la manda el CSS con `.hero.is--ready ~`, que
       aparece cuando el loader de intro termina (~7 s). Ese selector no casa en
       ninguna otra pagina, asi que alli no hace nada.
     · Resto: no hay loader, asi que revela el JS poniendo `.is-armed` tras
       `data-reveal-delay` (300 ms por defecto). En la portada ese mismo
       temporizador va a 9000 ms y actua solo de red de seguridad por si el
       timeline fallara.
   ===================================================================== */

/* ============ BOTÓN FLOTANTE DE CONTACTO ============ */
/* Entrada: el loader ocupa z-200 durante ~7 s y este botón vive en z-150, así
   que antes no "aparecía": quedaba DESTAPADO cuando el loader hacía
   display:none. En ese instante el ojo sigue el centro de la pantalla y nunca
   registraba algo estático en la esquina. Ahora entra como último tiempo del
   reveal, 0,9 s después del hero.

   El disparador es `.hero.is--ready`, que revealHero() ya pone en las TRES
   rutas de arranque (timeline GSAP, fallback sin GSAP y reduced-motion).
   `.floating-contact` es hermano de `.hero` dentro de <body>, así que basta el
   combinador `~`: cero JS nuevo y cero delay hardcodeado que se desincronice
   del timeline si alguien lo retoca.

   NO se sube el z-index por encima del loader a propósito: un botón flotando
   sobre la intro cinematográfica se lee como un bug de render. */
.floating-contact {
  /* -- Tokens propios -----------------------------------------------------
     El componente NO puede depender de las variables de la pagina: fuera de
     home.css no existe ninguna de ellas (--accent-hot, --line-strong, --mono
     y --accent-* dan CERO ocurrencias en styles.css y en sphera.html). Con
     var(--accent-hot) suelto, el gradiente del boton seria invalido y el
     lanzador se quedaria literalmente SIN FONDO en 6 de las 8 paginas.

     Los colores van fijos a proposito: el fucsia es el color de "contacto" de
     la marca y tiene que ser identico en todas partes -- ese es el sentido de
     replicar el boton. La unica que si hereda de la pagina es la mono, para
     que la etiqueta hable con la voz de cada pagina (IBM Plex Mono en la
     portada, Roboto Mono en sphera y conocimiento, la del sistema en el resto). */
  --fc-hot:   #F21651;
  --fc-warm:  #F9921C;
  --fc-cool:  #913793;
  --fc-ink:   #FFFFFF;
  --fc-line:  rgba(255, 255, 255, 0.22);
  --fc-panel: rgba(2, 24, 61, 0.92);
  /* "Roboto Mono" va ANTES de ui-monospace a propósito: ui-monospace es un
     genérico que siempre resuelve, así que detrás de él ninguna fuente concreta
     llegaría a usarse nunca. sphera y conocimiento cargan Roboto Mono y la usan
     precisamente para rótulos en versalitas como este; rendimiento no la carga y
     cae al mono del sistema. La portada define --mono y ni siquiera usa esta
     lista de reserva. */
  --fc-mono:  var(--mono, "Roboto Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace);

  position: fixed;
  right: 22px;
  bottom: calc(22px + env(safe-area-inset-bottom, 0px));
  z-index: 150;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;

  opacity: 0;
  /* visibility, y no sólo opacity: sin ella el botón es invisible pero SÍ
     enfocable durante los ~9 s previos al armado, y con teclado se puede
     aterrizar en un control que no se ve. */
  visibility: hidden;
  transform: translateY(14px) scale(0.94);
  pointer-events: none;
  transition:
    opacity 0.55s ease-out 0.9s,
    transform 0.55s cubic-bezier(0.22, 1, 0.36, 1) 0.9s,
    visibility 0s linear 0.9s;
}

/* Dos disparadores, y esto es deliberado: colgar la visibilidad SÓLO de
   `.hero.is--ready` haría que un fallo del timeline del loader (CDN de GSAP
   caído, excepción a mitad de la animación, navegador exótico) dejara el
   contacto invisible PARA SIEMPRE — un fallo peor que el que veníamos a
   arreglar. `.is-armed` lo pone el propio script del botón por temporizador
   como red de seguridad: pase lo que pase, a los 9 s el contacto está ahí. */
/* body:has(...) y no `.hero.is--ready ~ .floating-contact`: en la portada el
   hero vive dentro de <main> y el widget es hijo directo de <body>, así que
   dejaron de ser hermanos y el combinador `~` ya no los alcanzaba. `:has()`
   funciona igual en las páginas donde sí son hermanos. `.is-armed` sigue siendo
   la red de seguridad por temporizador. */
body:has(.hero.is--ready) .floating-contact,
.floating-contact.is-armed {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
}

/* Y si directamente no hay JS, el hero tampoco se revela: se muestra sin más. */
@media (scripting: none) {
  .floating-contact {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
}

/* El lanzador es una PÍLDORA CON ETIQUETA, no un círculo anónimo: un círculo
   obliga a hacer clic para saber qué es. Y va en el fucsia de marca
   (--accent-hot), el mismo del CTA "Contacto" de la cabecera, para que los dos
   accesos al contacto se lean como un sistema. El verde #25D366 de antes era
   el corporativo de WhatsApp: sobre esta paleta se leía como widget de
   terceros —categoría que la vista ya aprendió a ignorar— y encima prometía un
   solo canal cuando el botón abre tres. El verde se queda donde sí informa: en
   el disco de su propia opción dentro del menú. */
.floating-contact-button {
  display: inline-flex;
  align-items: center;
  height: 60px;
  padding: 0 22px 0 10px;
  border: 1px solid rgba(255, 255, 255, 0.26);
  border-radius: 999px;
  background: linear-gradient(135deg, #FF3D6A 0%, var(--fc-hot) 55%, #C0113F 100%);
  color: #fff;
  cursor: pointer;
  position: relative;
  box-shadow:
    0 12px 30px rgba(0, 0, 0, 0.50),
    0 0 26px rgba(242, 22, 81, 0.38),
    inset 0 1px 0 rgba(255, 255, 255, 0.22);
  transition:
    transform 0.25s ease,
    box-shadow 0.25s ease,
    padding 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}

.floating-contact-button:hover,
.floating-contact-button:focus-visible {
  transform: translateY(-2px);
  box-shadow:
    0 16px 38px rgba(0, 0, 0, 0.55),
    0 0 34px rgba(242, 22, 81, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.28);
}

.floating-contact-button:active {
  transform: translateY(0) scale(0.97);
}

.floating-contact-button:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}

/* Disco del icono — mismo lenguaje que los discos de 30 px de las opciones */
.floating-contact-button__icons {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.16);
}

.floating-contact-button__label {
  margin-left: 12px;
  max-width: 14ch;
  overflow: hidden;
  white-space: nowrap;
  font-family: var(--fc-mono);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition:
    max-width 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    margin-left 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.22s ease;
}

/* Se contrae a círculo cuando: (a) el usuario ya bajó —la etiqueta ya cumplió
   y hay que devolverle el rincón al footer, que reserva 84 px en .foot-flex y
   el círculo mide 60 + 22 = 82— o (b) el menú está abierto, porque las tres
   píldoras ya dicen todo y dos niveles de etiqueta serían ruido. */
.floating-contact.is-compact .floating-contact-button,
.floating-contact.is-open    .floating-contact-button {
  padding: 10px;
}

.floating-contact.is-compact .floating-contact-button__label,
.floating-contact.is-open    .floating-contact-button__label {
  max-width: 0;
  margin-left: 0;
  opacity: 0;
}

/* ══ HALO QUE RESPIRA ══════════════════════════════════════════════════
   El botón vive en un rincón vacío: quieto, la vista lo archiva como
   "widget" y deja de verlo. Esto no es adorno, es lo que lo mantiene en la
   visión periférica sin pedir nada.

   ¿Por qué filter:blur y no radial-gradient? Porque el botón MUTA: píldora
   de ~174 px en el hero, círculo de 60 (56 en móvil) al bajar, y transiciona
   entre ambos en 0,42 s. `border-radius: inherit` + blur describe las tres
   formas con una sola declaración y se deforma gratis con esa transición.
   Un radial-gradient `circle` dejaría los dos extremos redondeados de la
   píldora SIN halo; uno `ellipse` daría una caída vertical ~3× más apretada
   que la horizontal y cambiaría al contraerse. Y como el halo va DETRÁS de
   un botón opaco sólo se ve un anillo fino: justo donde el error geométrico
   canta.

   inset:-5px y no -4px: el inset se resuelve contra la CAJA DE RELLENO, y el
   botón tiene border:1px → -5 deja el halo 4 px fuera del borde visible.

   opacity:0 en la base a propósito: apagar la animación lo devuelve solo a
   invisible, sin !important y sin quedarse congelado en un fotograma. */
.floating-contact-button::before {
  content: '';
  position: absolute;
  inset: -5px;
  z-index: -1;
  border-radius: inherit;
  background: rgba(242, 22, 81, 0.45);
  filter: blur(8px);
  opacity: 0;
  pointer-events: none;
}

body:has(.hero.is--ready) .floating-contact .floating-contact-button::before,
.floating-contact.is-armed .floating-contact-button::before {
  animation: fab-breathe 5.2s ease-in-out infinite alternate;
}

/* 5,2 s y no los 4,5 s de .about-logo__glow: con `alternate` el ciclo completo
   son 2× la duración. 4,5 daría 9,0 s = EXACTAMENTE el periodo del ping, y el
   ping caería siempre en la misma fase del aliento — se leerían como un solo
   mecanismo. 5,2 → 10,4 s y nunca se sincronizan. Sigue en la banda lenta de
   la casa (4,5–6 s), que es la reservada para respiración perpetua.

   El movimiento real lo hace la OPACIDAD, no la escala: el borde exterior del
   resplandor viaja ~3,4 px, pero la opacidad modula ×2,3. La visión periférica
   detecta mucho mejor un cambio de luminancia que 3 px de desplazamiento. Es
   el mismo truco de .about-logo__glow. */
@keyframes fab-breathe {
  0%   { opacity: 0.35; transform: scale(0.97); }
  100% { opacity: 0.80; transform: scale(1.07); }
}

/* ══ ANILLO: ráfaga de aterrizaje + ping periódico ═════════════════════ */
.floating-contact-button::after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  border: 2px solid rgba(242, 22, 81, 0.65);
  opacity: 0;
  pointer-events: none;
}

/* Dos animaciones encadenadas por delay, nunca solapadas:
     ráfaga  1,6 s de delay + 3 × 2,4 s  → activa en [1,6 ; 8,8]
     silencio                            → 9,0 s
     ping    delay 8,8 + 9,0 = 17,8 s    → activa en [17,8 ; ∞), cada 9 s
   El atajo `animation` resetea fill-mode a `none`, y una animación en su
   periodo de delay NO aporta valores: entre 8,8 y 17,8 s el anillo vuelve
   solo a su base opacity:0, sin destello ni anillo congelado. */
body:has(.hero.is--ready) .floating-contact .floating-contact-button::after,
.floating-contact.is-armed .floating-contact-button::after {
  animation:
    fab-pulse 2.4s cubic-bezier(0.22, 1, 0.36, 1)  1.6s 3,
    fab-ping  9s   cubic-bezier(0.22, 1, 0.36, 1) 17.8s infinite;
}

@keyframes fab-pulse {
  0%   { transform: scale(1);    opacity: 0.85; }
  70%  { transform: scale(1.16); opacity: 0; }
  100% { transform: scale(1.16); opacity: 0; }
}

/* Ciclo de 9 s: 1,62 s de movimiento y 7,38 s de NADA. El descanso va dentro
   de los porcentajes y no en el delay, porque el delay sólo sirve una vez y
   esto se repite. Arranca desde opacity 0 con una rampa de 0,36 s: tres
   pulsos seguidos aguantan un nacimiento seco, pero un pulso solitario cada
   9 s con borde duro se lee como un glitch de render — el ojo tiene que
   enganchar el CRECIMIENTO, no el pop.

   Usa la curva de la ráfaga y no ease-in-out a propósito: no es respiración
   perpetua, es el MISMO gesto discreto de atención, espaciado. Con otra curva,
   ráfaga y ping se leerían como dos cosas distintas. La regla "perpetuo =
   ease-in-out" la cumple el halo, que es el bucle continuo de verdad. */
@keyframes fab-ping {
  0%   { transform: scale(1);    opacity: 0;   }
  4%   { transform: scale(1.02); opacity: 0.7; }
  18%  { transform: scale(1.18); opacity: 0;   }
  100% { transform: scale(1.18); opacity: 0;   }
}

/* ══ APAGADOS ══════════════════════════════════════════════════════════
   Al contraerse: el ping para, el halo SIGUE respirando. El ping es para
   presentar el botón; a quien ya recorrió la página, insistirle es lo que
   vuelve molesto un FAB. */
.floating-contact.is-compact .floating-contact-button::after {
  visibility: hidden;
}

/* Con el menú abierto o con el puntero encima se apagan los dos.
   El apagado en HOVER no es cosmético, es obligatorio: :hover aplica
   `transform: translateY(-2px)` y :active un `scale(.97)`, y un transform
   distinto de none CREA UN CONTEXTO DE APILAMIENTO. En ese momento el botón
   pasa a ser uno, y el ::before con z-index:-1 deja de pintarse detrás para
   pintarse ENCIMA del gradiente y del filete blanco de 1 px, velando el borde
   justo cuando el ojo está ahí. Apagarlo lo resuelve, y además es el patrón
   de la casa (.lang-fab:hover::after en sphera.html).

   Se usa `visibility` y no `animation: none` por dos razones concretas:

   1. ESPECIFICIDAD. Esto SUSTITUYE a la antigua regla
      `.floating-contact.is-open ...::after { animation:none; opacity:0 }`,
      que pesaba (0,3,1) mientras el gate de arriba pesa (0,4,1): NO ganaba,
      así que el anillo seguía latiendo con el menú abierto. Con la ráfaga
      finita de 8,8 s era un fallo invisible; con el ping infinito sería
      permanente. `visibility` no la declara nadie más en estos pseudos, así
      que no hay pelea de cascada ni hace falta !important.

   2. RELOJ. `animation:none` cancela la lista ENTERA; al salir del estado se
      vuelve a aplicar y fab-pulse REARRANCA sus 3 pulsos de aterrizaje. Como
      .is-compact entra y sale con cada cruce de scroll (histéresis 0,50/0,25),
      el aterrizaje se repetiría cada vez que el usuario vuelve arriba.
      `visibility` no toca el reloj de ninguna de las dos. */
.floating-contact.is-open .floating-contact-button::before,
.floating-contact.is-open .floating-contact-button::after,
.floating-contact-button:hover::before,
.floating-contact-button:hover::after,
.floating-contact-button:focus-visible::before,
.floating-contact-button:focus-visible::after,
.floating-contact-button:active::before,
.floating-contact-button:active::after {
  visibility: hidden;
}

/* Crossfade burbuja de chat ↔ X */
.floating-contact-button__icon {
  grid-area: 1 / 1;
  transition: opacity 0.25s ease, transform 0.25s ease;
}

.floating-contact-button__icon--close {
  opacity: 0;
  transform: rotate(-90deg) scale(0.6);
}

.floating-contact.is-open .floating-contact-button__icon--chat {
  opacity: 0;
  transform: rotate(90deg) scale(0.6);
}

.floating-contact.is-open .floating-contact-button__icon--close {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}

/* Menú desplegable */
/* `order` en vez de invertir la dirección del contenedor: la portada tiene el
   botón ANTES que el menú en el DOM (para que Tab entre en las opciones al
   abrirlo), pero las otras 7 páginas todavía lo tienen al revés. Con `order` el
   menú sale arriba en los dos casos, así que esta hoja compartida no depende
   del orden del marcado de cada página. */
.floating-contact-menu {
  order: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}

.floating-contact-button { order: 2; }

.floating-contact-option {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 9px 16px 9px 10px;
  border-radius: 999px;
  background: var(--fc-panel);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border: 1px solid var(--fc-line);
  color: var(--fc-ink);
  text-decoration: none;
  font-family: var(--fc-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4);
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px) scale(0.96);
  transition: opacity 0.28s ease, transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 0.28s, border-color 0.25s ease;
}

.floating-contact.is-open .floating-contact-option {
  opacity: 1;
  visibility: visible;
  transform: translateY(0) scale(1);
  transition: opacity 0.28s ease, transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s, border-color 0.25s ease;
}

/* Stagger: la opción más cercana al botón aparece primero */
.floating-contact.is-open .floating-contact-option:nth-child(3) { transition-delay: 0s; }
.floating-contact.is-open .floating-contact-option:nth-child(2) { transition-delay: 0.06s; }
.floating-contact.is-open .floating-contact-option:nth-child(1) { transition-delay: 0.12s; }

.floating-contact.is-open .floating-contact-option:hover {
  transform: translateY(-2px);
  transition-delay: 0s;
}

.floating-contact-option:focus-visible {
  outline: 2px solid var(--fc-ink);
  outline-offset: 2px;
}

.floating-contact-option__icon {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  flex: 0 0 auto;
}

.floating-contact-option--wsp .floating-contact-option__icon { background: rgba(37, 211, 102, 0.16); color: #25D366; }
.floating-contact-option--cal .floating-contact-option__icon { background: rgba(249, 146, 28, 0.16); color: var(--fc-warm); }
.floating-contact-option--mail .floating-contact-option__icon { background: rgba(145, 55, 147, 0.22); color: #C77BC9; }

.floating-contact.is-open .floating-contact-option--wsp:hover { border-color: #25D366; }
.floating-contact.is-open .floating-contact-option--cal:hover { border-color: var(--fc-warm); }
.floating-contact.is-open .floating-contact-option--mail:hover { border-color: var(--fc-cool); }

@media (max-width: 600px) {
  .floating-contact {
    right: 16px;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
  /* Contraído da 8 + 40 + 8 = 56×56, idéntico al botón de antes */
  .floating-contact-button { height: 56px; padding: 0 18px 0 8px; }
  .floating-contact.is-compact .floating-contact-button,
  .floating-contact.is-open    .floating-contact-button { padding: 8px; }
  .floating-contact-button__label {
    margin-left: 10px;
    font-size: 11.5px;
    letter-spacing: 0.06em;
  }
  .floating-contact-option { font-size: 11px; padding: 8px 14px 8px 9px; }
}

@media (prefers-reduced-motion: reduce) {
    /* Visible de inmediato y sin coreografía de entrada. La contracción por
       scroll sigue existiendo (es layout, no decoración) pero ocurre instantánea. */
    .floating-contact {
      opacity: 1 !important;
      transform: none !important;
      transition: none !important;
      pointer-events: auto !important;
    }
    .floating-contact-button,
    .floating-contact-button__icon,
    .floating-contact-button__label,
    .floating-contact-option {
      transition: none !important;
    }
    /* `animation: none` elimina la lista completa, así que esto mata a la vez
       la ráfaga de aterrizaje y el ping periódico. */
    .floating-contact-button::after {
      animation: none !important;
      opacity: 0 !important;
    }
    /* El halo, fuera. Con animation:none ya volvería a su base (opacity:0, sin
       transform), pero se declaran los tres explícitos para que no quede
       congelado en un fotograma arbitrario si alguien retoca la regla base.
       Resultado: con reduced motion el botón queda IDÉNTICO a como estaba. */
    .floating-contact-button::before {
      animation: none !important;
      transform: none !important;
      opacity: 0 !important;
    }
}

/* -- Reserva de la esquina inferior derecha --------------------------------
   home.css ya reservaba 84 px en el footer de la portada; styles.css NO lo
   hace, asi que en los hubs el circulo contraido (60 px + 22 de margen = 82)
   se comeria el borde de .foot-nav. Va aqui y no en styles.css porque esta
   hoja solo la cargan las paginas que tienen el boton: la reserva viaja con el
   componente que la necesita.
   Solo en escritorio: por debajo de 900 px el footer se apila y el boton deja
   de competir con el horizontalmente. */
@media (min-width: 901px) {
  .foot-flex { padding-right: 84px; }
}
