/* ==========================================================================
   OBSCURA · BASE — reset, tipografía, layout primitivo, movimiento global
   ========================================================================== */

/* ==========================================================================
   RESPALDO DE MÉTRICAS AJUSTADAS — el arreglo del CLS (16ª pasada)
   NO cambia ninguna tipografía. Es Georgia con las métricas de Instrument
   Serif encima, para que el texto ocupe EL MISMO LUGAR antes y después del
   swap de la fuente web.

   El único salto de layout que le quedaba a la home lo causaba ese swap a los
   ~430 ms: .hero__title-key pasaba de 450,5 a 340,4 px de ancho y arrastraba
   los 868 px de .hero__inner. La 14ª pasada lo midió con un
   PerformanceObserver sobre layout-shift y lo dejó anotado sin arreglar.

   CÓMO SE CALIBRÓ, que importa porque el número obvio está mal:
   medir la fuente pelada en canvas da 0,7595, y medir el ancho de los
   elementos en el DOM da 0,8263 — pero ese 0,83 es FALSO: lo inflaban tres
   .numero__value que viven en celdas de ancho fijo y no pueden saltar nunca.
   Sacándolos, el DOM da 0,7520 y coincide con el canvas. Se iteró sobre el
   render (0,752 / 0,757 / 0,762) hasta dejar el desvío ponderado en 0,07%.
   Medido con 76%: el H1 pasa de +33,43% a +1,39% y title-key de +32,35% a
   −1,07%. El peor caso que queda es −4,14% en el «©26» de Proyectos, que son
   128 px y no arrastra layout.
   asc/desc van divididos por el size-adjust: la cara ya viene escalada, así
   que los overrides se expresan en el em nuevo (99/0,76 y 31/0,76).

   VA SOLO size-adjust, SIN ascent/descent-override, y es a propósito:
   .display tiene line-height fijo (--lh-tight), así que la altura de la caja
   de línea no depende de las métricas verticales de la fuente — los overrides
   sólo moverían la línea de base adentro de la caja, o sea que agregan un
   desplazamiento vertical en vez de sacarlo. Medido: con overrides el CLS
   sube a 0,190.

   Y no alcanza con esto solo: hace falta además que .hero__title lleve su
   max-width en `em` y no en `ch` (sections.css § 01 HERO). El motivo largo
   está ahí, pero en corto: `ch` depende de la fuente en uso, así que la caja
   se mueve junto con el texto y el titular cambia de cantidad de renglones.
   Las dos cosas van juntas o esto empeora el CLS en vez de arreglarlo.

   ⚠ Va con local('Georgia'), que existe en Windows, macOS e iOS. En Android y
   Linux no está: ahí cae a serif y el salto vuelve. Cubrir eso pide subir un
   .woff2 propio, que es otra decisión.
   ========================================================================== */
@font-face {
  font-family: 'Obscura Fallback';
  src: local('Georgia');
  size-adjust: 76%;
}

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;          /* se apaga cuando Lenis toma el control */
  /* Reserva el canal de la barra SIEMPRE. Es lo que hace que bloquear el scroll
     (el loader mientras el diafragma está puesto, y el menú móvil) no corra
     todo el ancho ~15px al soltarlo — un shift que cuenta como CLS. En esta
     página la barra está siempre presente, así que visualmente no cambia nada.
     Safari < 18.2 lo ignora y se queda con el comportamiento de antes. */
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: var(--ink-mute) transparent;
}

/* Pista transparente a propósito: esta barra es una sola, para toda la
   página, y cruza secciones claras y oscuras por igual (data-theme vive en
   <section>, no en <html>). Una pista con color fijo se vería como una
   franja ajena metida sobre el negro. El pulgar en --ink-mute lee bien en
   los dos fondos porque es un color sólido, no un rgba atado a la tinta
   clara; en :hover pasa a --accent, el mismo gesto que ::selection más
   abajo — es el otro lugar donde el cromo del navegador lleva la marca. */
html::-webkit-scrollbar { width: 10px; height: 10px; }
html::-webkit-scrollbar-track { background: transparent; }
html::-webkit-scrollbar-thumb {
  background-color: var(--ink-mute);
  border-radius: var(--radius-pill);
  border: 2px solid transparent;
  background-clip: content-box;
}
html::-webkit-scrollbar-thumb:hover { background-color: var(--accent); }

html.lenis, html.lenis body { height: auto; }
html.lenis { scroll-behavior: auto; }
/* ⚠ SÓLO -X, NO EL SHORTHAND. Es la regla que recomienda la documentación de
   Lenis, con una corrección: el shorthand `overscroll-behavior: contain`
   bloquea el encadenado en LOS DOS ejes. Un elemento con overflow-x:auto /
   overflow-y:hidden —un carrusel horizontal— "contiene" también el vertical,
   así que un wheel de mouse (deltaY puro) con el puntero encima no tiene a
   dónde ir: ni el elemento lo consume ni la página lo recibe, y el scroll
   queda trabado.

   ⚠ Y OJO CON ESTA REGLA EN GENERAL, que es lo que costó descubrir: la clase
   `lenis-smooth` sólo está en el <html> MIENTRAS se scrollea (medido: al parar
   queda `lenis` a secas). O sea que lo que declare acá se enciende y se apaga
   en mitad del gesto — mal lugar para nada que tenga que ser estable.

   Hoy no la usa nadie: /proyectos/ tenía `data-lenis-prevent` en su tira y la
   25ª lo sacó, porque además hacía que Lenis se desentendiera del scroll
   vertical ahí y la página terminara con dos motores de scroll peleándose.
   Se deja corregida por si algún día vuelve a hacer falta. */
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior-x: contain; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: clip;
}

img, svg, video { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; background: none; border: none; color: inherit; padding: 0; }
input, select, textarea { font: inherit; color: inherit; }
:where(input, select, textarea):focus { outline: none; }
::placeholder { color: var(--ink-faint); opacity: 1; }
::selection { background: var(--accent); color: var(--accent-ink); }

/* --------------------------------------------------------------------------
   Foco visible — obligatorio, y correcto en los dos temas
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* El destino del skip-link es <main tabindex="-1">, y ahí el anillo NO sirve:
   dibujaría un marco de 2px alrededor de la página entera. El usuario acaba de
   pulsar el enlace y la página ya saltó — la confirmación es el salto, no el
   contorno. Sólo se apaga en el landmark, no en nada que se pueda tabular. */
main[tabindex="-1"]:focus,
main[tabindex="-1"]:focus-visible { outline: none; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; top: -100px; left: var(--gutter); z-index: 100;
  background: var(--accent); color: var(--accent-ink);
  padding: var(--sp-3) var(--sp-5); border-radius: var(--radius-pill);
  font-family: var(--font-ui); font-weight: 600; font-size: var(--fs-sm);
  transition: top var(--dur) var(--ease-out);
}
.skip-link:focus-visible { top: var(--sp-4); }

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container--narrow { max-width: var(--container-narrow); margin-inline: auto; }

/* La isla ocupa 12 + 76 = 88px. Lenis ya descuenta ese alto en su scrollTo
   (modulo 1), pero SIN Lenis --touch, reduced-motion, sin JS-- el salto de
   ancla es nativo y no descuenta nada: el titular de la seccion quedaba abajo
   del header. Un renglon, y arregla un defecto que la isla solo agrando. */
:where(main) > [id] { scroll-margin-top: 100px; }

.section {
  position: relative;
  /* Los dos lados salen de una variable propia para que la regla de ritmo de
     más abajo pueda aflojarlos SIN tocar la propiedad. Es lo que deja que
     .fases (padding-block: 0, el aire lo pone el pin) siga ganando: sobrescribe
     la propiedad entera y nunca lee estas variables. */
  padding-block: var(--section-pad-top, var(--section-pad))
                 var(--section-pad-bottom, var(--section-pad));
  background: var(--bg);
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   RITMO ENTRE SECCIONES — el aire vale distinto según qué separa

   Entre una sección clara y una oscura, el CORTE DE COLOR y la .seam con su
   filete y su fleurón ya hacen todo el trabajo: los 152 + 152 px se leen como
   corte de montaje. Pero la página tiene un tramo de CUATRO secciones oscuras
   seguidas (04 → 07) y otro de tres claras (08 → 10), y ahí no hay ni corte ni
   costura: quedan hasta 304 px de fondo plano sin nada. Medido sobre render, no
   se lee como aire — se lee como que algo no cargó.

   Cuando la sección que sigue COMPARTE tema con la anterior, las dos aflojan a
   --section-pad-tight. A 1440 el hueco pasa de 304 a 192 px.

   ⚠ Esto es la capa GENÉRICA. Hay una segunda capa con nombre propio en
   sections.css (§ RITMO VERTICAL): .clientes y .proyectos descuentan el alto de
   su costura, y .formulario (×1.15) y .qa (×.70) varían a propósito. Esa capa
   LE GANA a ésta, y está bien que así sea: declara padding-top sobre la
   propiedad, mientras que acá sólo se escriben las variables que lee el atajo
   de .section. Por eso .fases (padding-block: 0) también sigue intacta.
   -------------------------------------------------------------------------- */
[data-theme="dark"]  + .section[data-theme="dark"],
[data-theme="light"] + .section[data-theme="light"] {
  --section-pad-top: var(--section-pad-tight);
}
.section[data-theme="dark"]:has(+ [data-theme="dark"]),
.section[data-theme="light"]:has(+ [data-theme="light"]) {
  --section-pad-bottom: var(--section-pad-tight);
}
/* Cada sección temada pinta su propio fondo, así el híbrido claro/oscuro
   no necesita una sola regla extra por sección. */
[data-theme] { background: var(--bg); color: var(--ink); }

.grid { display: grid; gap: var(--gap-grid); }
.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: 1024px) {
  .grid--3, .grid--4 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------------------
   Grano — profundidad, no textura. Debajo del nav y del menú (era z 9999).
   -------------------------------------------------------------------------- */
.grain {
  position: fixed; inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-blend);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* ==========================================================================
   TIPOGRAFÍA — las tres leyes del manual de marca, aplicadas como reglas
   · Instrument Serif SOLO: H1 del hero, numerales display, pull-quotes, énfasis
   · Plus Jakarta Sans: estructura y acción — H2, H3, nav, botones, eyebrows, tags
   · IBM Plex Sans: lectura e input — párrafos, listas, campos, captions. Sin itálica.
   ========================================================================== */
h1, h2, h3, h4, p { margin: 0; }

.display {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--fs-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
}
.display em, .display i { font-style: italic; }

/* ⚠ RED DE SEGURIDAD CONTRA LA PALABRA QUE NO ENTRA — 45ª pasada.
   En todo el sitio no había ni un `overflow-wrap`, ni un `hyphens`, ni un
   `word-break`, y el castellano tiene palabras largas. La escala de tokens.css
   está calibrada 390→1440, así que a 320 px los titulares quedan en su PISO
   (--fs-display no baja de 40 px) contra una caja mucho más chica: medido en
   /nosotros/, «Acreditaciones» a 40 px pedía 285 px donde había 232, y esos
   53 px salían del panel, pasaban a .sangre y terminaban corriendo el
   documento entero 9 px. Un solo sustantivo movía la página.

   `break-word` y no `anywhere`: `anywhere` deja partir aunque haya otra
   solución y afecta el cálculo de min-content; `break-word` es el último
   recurso y SÓLO actúa cuando la palabra no entra de ninguna manera. O sea
   que hoy no cambia un solo titular del sitio — sólo impide que el próximo
   sustantivo largo vuelva a correr la página. */
h1, h2, h3, .display, .numeral { overflow-wrap: break-word; }

.numeral {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--fs-numeral);
  line-height: 1;
  letter-spacing: var(--track-snug);
  font-variant-numeric: lining-nums tabular-nums;
}

.h2 {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: var(--fs-h2);
  line-height: var(--lh-snug);
  letter-spacing: var(--track-snug);
  color: var(--ink);
  text-wrap: balance;
}
.h2 em, .h2 i {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
}

.h3 {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: var(--fs-h3);
  line-height: var(--lh-snug);
  letter-spacing: -0.005em;
  color: var(--ink);
  text-wrap: balance;
}

.eyebrow {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  font-family: var(--font-ui); font-weight: 600;
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-soft);
}
.eyebrow::before {
  content: ""; width: 24px; height: 1px; background: var(--accent);
  flex-shrink: 0;
}
.eyebrow--bare::before { display: none; }
/* Punto en vez de raya: SÓLO para cabeceras a dos columnas, donde la raya
   apuntaba a un titular que ya no está al lado.
   ⚠ La 18ª pasada acotó esto a las dos secciones que de verdad son a dos
   columnas (02 Clientes y 04 Proyectos). El formulario lo llevaba sin serlo.
   Y se eliminó .eyebrow--accent, que pintaba el eyebrow de azul en la 08 sin
   ningún motivo estructural: era la única sección que lo hacía, y la jerarquía
   extra que buscaba no se resuelve recoloreando el rótulo más chico. */
.eyebrow--dot::before {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--accent-2);
}

/* ==========================================================================
   NUMERACIÓN — dos familias y sólo dos.

   Antes había NUEVE tratamientos conviviendo (romanos, badge circular, marca
   de año, píldora de índice, medidor con paréntesis, numeral de fila, etiqueta
   de paso, fantasma de ficha y numeral de beneficio). Tres secciones seguidas
   —06, 08 y 09— numeraban de 01 a 04 con tres tipografías distintas, y dos de
   ellas usaban acentos OPUESTOS para el mismo rol semántico.

   · Familia A · .num-paso — pasos que el lector RECORRE en secuencia.
     Servicios, Fases y los beneficios del formulario. Display serif sobre
     --accent-2. El tamaño lo pone cada contexto con --num-paso-fs, para que el
     numeral guarde siempre la misma proporción contra el título de SU fila.

   · Familia B · inventario — cosas que se CUENTAN, no que se recorren.
     Los romanos del libro (.fuga__num) y los índices de proyecto
     (.proyecto__pill--index). No comparten regla porque no comparten forma:
     lo que comparten es no ser pasos. Los romanos minúsculos se conservan tal
     cual — son la convención del XVIII y la mejor decisión tipográfica de la
     página.
   ========================================================================== */
.num-paso {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--num-paso-fs, clamp(1.75rem, 1.4rem + 1.4vw, 2.5rem));
  line-height: 1;
  color: var(--accent-2);
  /* Cifras alineadas: en una columna de 01..04 el ancho variable se nota. */
  font-variant-numeric: lining-nums tabular-nums;
}

.lead {
  font-size: var(--fs-lead);
  line-height: var(--lh-relaxed);
  color: var(--ink-mute);
  max-width: 62ch;
  text-wrap: pretty;
}

.section__head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-4); }
.section__head .lead { margin-top: var(--sp-2); }
.section__head--center { align-items: center; text-align: center; }
.section__head--center .lead { margin-inline: auto; }

/* ==========================================================================
   MOVIMIENTO GLOBAL
   Dos sistemas, deliberadamente separados:
   1 · REVEALS — se disparan UNA vez con IntersectionObserver. El manual de
       marca prohíbe re-disparar al volver a subir, así que NO usan
       scroll-timeline (que sería reversible por naturaleza).
   2 · SCROLL-TRACKED — continuos y reversibles, con animation-timeline nativo.
       Viven en sections.css, donde cada uno tiene sentido.
   ========================================================================== */

@keyframes obs-rise {
  from { opacity: 0; transform: translate3d(0, 22px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes obs-wipe-up {
  from { opacity: 0; clip-path: inset(105% 0 0 0); transform: translate3d(0, 12px, 0); }
  to   { opacity: 1; clip-path: inset(-15% 0 0 0); transform: none; }
}
@keyframes obs-wipe-right {
  from { opacity: 0; clip-path: inset(0 100% 0 0); }
  to   { opacity: 1; clip-path: inset(0 -2% 0 0); }
}
/* opacity: 1 explícito. [data-reveal-item] arranca en opacity 0 y este
   keyframe corre con fill both: sin declararla, una .rule que además sea
   [data-reveal-item] se dibujaba pero quedaba invisible para siempre. */
@keyframes obs-draw-x {
  from { opacity: 1; transform: scaleX(0); }
  to   { opacity: 1; transform: scaleX(1); }
}
@keyframes obs-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* El bloque se POSA: entra desenfocado y BAJANDO, al revés que obs-rise, que
   sube desde 22px abajo. La inversión es el punto — es lo que hace que el pie
   de página se sienta como algo que se asienta al final del recorrido.
   blur(0px) explícito y no none: none no interpola parejo entre motores. */
@keyframes obs-settle {
  from { opacity: 0; filter: blur(4px); transform: translate3d(0, -8px, 0); }
  to   { opacity: 1; filter: blur(0px); transform: none; }
}

[data-reveal-item] { opacity: 0; }

[data-reveal-item].is-visible {
  animation: obs-rise var(--dur-reveal) var(--ease-expo) both;
  animation-delay: var(--reveal-delay, 0ms);
}
[data-reveal-item][data-reveal="wipe"].is-visible { animation-name: obs-wipe-up; }
[data-reveal-item][data-reveal="wipe-x"].is-visible { animation-name: obs-wipe-right; }
[data-reveal-item][data-reveal="fade"].is-visible { animation-name: obs-fade; }

/* OJO — esta variante se declara en el ÁMBITO y no en el ítem, al revés que
   las tres de arriba. El módulo 5 junta los ámbitos con $$('[data-reveal]'),
   así que cualquier elemento que lleve el atributo se vuelve ámbito propio y
   se muestra solo, con delay 0. En las filas de Filosofía eso daba igual (se
   buscaba justamente que cada fila entrara al aparecer), pero acá rompería el
   escalonado: los cinco bloques del pie se dispararían por separado. Con la
   variante en el ámbito, los ítems llevan sólo [data-reveal-item], no son
   ámbitos, y la escalera de --reveal-delay queda intacta. */
[data-reveal="settle"] [data-reveal-item].is-visible { animation-name: obs-settle; }

/* will-change solo mientras dura la animación — main.js lo limpia en animationend */
[data-reveal-item].is-animating { will-change: opacity, transform, clip-path, filter; }

/* Reglas que se dibujan al entrar */
.rule {
  height: 1px; background: var(--line); transform-origin: left center;
  border: 0; margin: 0; width: 100%;
}
.rule--accent { height: 2px; background: var(--accent); }
/* Punteada. Sigue entrando con el scaleX de .rule: sobre un gradiente
   repetido los puntos se estiran mientras dura la animación y asientan al
   final — que es el mismo efecto de trazo que se logra animando el ancho,
   pero por transform, que es lo único que la regla de la casa deja animar. */
.rule--dotted {
  background: none;
  background-image: repeating-linear-gradient(90deg,
    var(--line-strong) 0 2px, transparent 2px 6px);
}
.rule.is-visible,
.is-visible .rule {
  animation: obs-draw-x var(--dur-slow) var(--ease-expo) both;
  animation-delay: var(--reveal-delay, 0ms);
}

/* --------------------------------------------------------------------------
   Reduced motion — interruptor general.
   Todo renderiza en su estado final; Lenis y los mockups se apagan en JS.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  [data-reveal-item] { opacity: 1 !important; clip-path: none !important; transform: none !important; }
  .rule { transform: none !important; }
}

/* Fallback sin JS: nada queda invisible */
.no-js [data-reveal-item] { opacity: 1; }
/* Y los íconos tampoco. Los paths de line-art arrancan con el trazo entero en
   dasharray y solo vuelven a 0 bajo .is-visible, que la pone el módulo 5: sin
   JavaScript quedaban invisibles para siempre en toda la página. (Con
   reduced-motion no pasa, porque ahí el módulo 5 llama a show() de una.) */
.no-js .icon--figure path,
.no-js .icon--draw path { stroke-dashoffset: 0; }
