/* ============================================================================
   TTM — ttmstudio.xyz
   ----------------------------------------------------------------------------
   La idea del diseño: los dos brazos del negocio son dos mundos visuales. El
   brazo físico va en azul noche, industrial, con reglas y números. El brazo
   digital va en papel claro. El contraste hace obvia la estructura de la
   empresa antes de leer una palabra.
   ========================================================================== */

:root {
  /* --- Lado oscuro (operación física) ------------------------------------ */
  --ink: #0A1A2B;
  --ink-2: #0D2135;
  --ink-3: #071320;
  --on-ink: #F2F7FC;
  --on-ink-2: #A8BCCE;
  --on-ink-3: #74899E;
  --rule-ink: rgba(168, 188, 206, .16);

  /* --- Lado claro (operación digital) ------------------------------------ */
  --paper: #F3F1EA;
  --paper-2: #E9E5DA;
  --on-paper: #14212E;
  --on-paper-2: #4A5A6B;
  --on-paper-3: #7B8794;
  --rule-paper: rgba(20, 33, 46, .14);

  /* --- Marca ------------------------------------------------------------- */
  --amber: #C8852E;
  --amber-deep: #9A6420;
  --teal: #0E8F97;
  --teal-deep: #0A6E75;

  /* --- Tipografía --------------------------------------------------------
     Superfamilia IBM Plex. Diseñada para una marca de industria y tecnología,
     que es exactamente lo que TTM es: un brazo físico y uno digital. Al ser una
     sola familia con dos voces —sans y mono— los dos mundos del sitio se ven
     distintos sin dejar de pertenecer al mismo sistema.

     Plex Sans tiene la altura de x más alta y el ancho más estrecho que la
     familia anterior, así que el tracking de los títulos se abrió un poco y el
     interlineado se cerró: las mismas cifras de antes se verían apretadas. */
  --display: 'IBM Plex Sans', system-ui, sans-serif;
  --body: 'IBM Plex Sans', system-ui, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, monospace;

  /* --- Movimiento --------------------------------------------------------
     Las curvas nativas de CSS son demasiado suaves. Nunca ease-in en interfaz:
     arranca lento justo en el instante que el ojo más observa.

     La curva depende de qué hace el elemento: entrar o salir pide ease-out
     (arranca rápido, se siente responsivo); moverse en pantalla pide
     ease-in-out; un cambio de color pide ease, que es más discreto.

     Los tiempos siguen el presupuesto de interfaz: nada de esto pasa de 300 ms.
     Un hover de 160 ms se siente más atento que uno de 400 ms. */
  --ease-out: cubic-bezier(.23, 1, .32, 1);
  --ease-in-out: cubic-bezier(.77, 0, .175, 1);
  --ease-hover: ease;

  --t-press: 130ms;      /* acuse de recibo al presionar */
  --t-hover: 160ms;      /* hover: se ve decenas de veces, va corto */
  --t: 200ms;            /* cambios de estado del encabezado */
  --t-enter: 620ms;      /* entradas de una sola vez */

  /* --- Métrica ----------------------------------------------------------- */
  --shell: 1240px;
  --gut: clamp(22px, 5vw, 72px);
}

/* ============================================================================
   Base
   ========================================================================== */

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

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  /* El encabezado fijo mide 63 px. Sin esto, un salto por ancla deja la
     sección arrancando justo debajo del borde del encabezado y se come la
     mitad del aire superior de la sección. */
  scroll-padding-top: 84px;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--on-ink);
  font-family: var(--body);
  font-size: 16.5px;
  font-weight: 400;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg { display: block; max-width: 100%; }
img { height: auto; }

a { color: inherit; text-decoration: none; -webkit-tap-highlight-color: transparent; }

ul, ol { margin: 0; padding: 0; list-style: none; }
p { margin: 0; }
h1, h2, h3 { margin: 0; font-weight: 600; }

em { font-style: normal; text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 4px; }

::selection { background: var(--amber); color: #fff; }

.shell {
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  padding-inline: var(--gut);
}

/* Sin esto era imposible saber dónde estaba el foco al navegar con teclado. */
:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 12px;
  z-index: 200;
  background: var(--on-ink);
  color: var(--ink);
  padding: 12px 20px;
  font-family: var(--mono);
  font-size: 13px;
  border-radius: 3px;
}
.skip:focus { left: 12px; }

/* ============================================================================
   Etiquetas pequeñas
   ----------------------------------------------------------------------------
   Texto chico en mayúsculas: necesita tracking positivo para respirar. Los
   títulos grandes, al contrario, lo necesitan negativo. Un solo valor de
   letter-spacing para todo siempre está mal en algún lado.
   ========================================================================== */

.eyebrow {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--on-ink-3);
}
.eyebrow--a { color: var(--amber); }
.eyebrow--b { color: var(--teal-deep); }

/* ============================================================================
   Encabezado
   ========================================================================== */

.hdr {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 16px var(--gut);
  background: rgba(10, 26, 43, .6);
  backdrop-filter: blur(18px) saturate(150%);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  border-bottom: 1px solid transparent;
  transition: background var(--t) var(--ease-out), border-color var(--t-hover) var(--ease-hover);
}

/* El divisor sólo aparece cuando ya hay contenido pasando por debajo. Sobre la
   portada sería una línea sin nada que separar. */
.hdr.is-scrolled { background: rgba(7, 19, 32, .9); }

/* Un degradado que se desvanece, no una línea de 1 px. La regla dura corta la
   página en dos y compite con las reglas de contenido, que sí significan algo.
   El degradado dice «aquí hay una capa flotando encima» sin dibujar un borde:
   separa donde de verdad se encima el encabezado y desaparece donde no. */
.hdr::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 20px;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(7, 19, 32, .55), transparent);
  opacity: 0;
  transition: opacity var(--t) var(--ease-hover);
}
.hdr.is-scrolled::after { opacity: 1; }

/* ── El encabezado sobre la portada partida ────────────────────────────────
   Arriba del todo, el encabezado flota sobre los dos mundos a la vez. Su barra
   azul translúcida sobre el papel da una banda gris sucia, y el texto claro
   sobre papel no se lee.

   La barra se quita y el contenido pasa a `difference`: sobre el azul noche el
   blanco sale crema, sobre el papel sale casi negro. Un solo encabezado que se
   lee correcto en los dos mundos, sin duplicarlo y sin saber dónde quedó la
   costura. En cuanto hay scroll —ya no hay dos mundos abajo— vuelve a ser la
   barra de siempre. */
.hdr:has(~ #contenido .hero.is-split):not(.is-scrolled) {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.hdr:has(~ #contenido .hero.is-split):not(.is-scrolled) .brand,
.hdr:has(~ #contenido .hero.is-split):not(.is-scrolled) .hdr__nav {
  mix-blend-mode: difference;
  color: #fff;
}
.hdr:has(~ #contenido .hero.is-split):not(.is-scrolled) .hdr__nav a,
.hdr:has(~ #contenido .hero.is-split):not(.is-scrolled) .brand__words i { color: inherit; }

/* La barra de progreso no entra al blend: es una franja de color de marca y
   `difference` se la comería. */
.hdr__bar { mix-blend-mode: normal; }

.brand {
  display: flex;
  align-items: center;
  gap: 13px;
  color: var(--on-ink);
}
.brand__mark { width: 30px; height: 30px; flex: none; }
.brand__words { display: flex; flex-direction: column; line-height: 1; }
.brand__words b {
  font-family: var(--mono);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: .09em;
}
.brand__words i {
  font-family: var(--mono);
  font-style: normal;
  font-size: 9px;
  letter-spacing: .22em;
  color: var(--on-ink-3);
  margin-top: 3px;
}

.hdr__nav { display: flex; gap: clamp(16px, 2.4vw, 34px); }
.hdr__nav a {
  position: relative;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--on-ink-2);
  transition: color var(--t-hover) var(--ease-hover);
}

/* Dónde estás. El sitio tiene tres anclas y hasta ahora no daba ninguna señal
   de en cuál ibas. El subrayado toma el color del mundo al que pertenece la
   sección —ámbar el brazo físico, verde azulado el digital—, así que además de
   ubicar, repite la estructura de la empresa en el encabezado. */
.hdr__nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -5px;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--t) var(--ease-out);
}
.hdr__nav a.is-here { color: var(--on-ink); }
.hdr__nav a.is-here::after { transform: scaleX(1); }
.hdr__nav a[href="#fisica"].is-here::after { background: var(--amber); }
.hdr__nav a[href="#digital"].is-here::after { background: var(--teal); }

/* ============================================================================
   Portada
   ========================================================================== */

/* ── La costura ────────────────────────────────────────────────────────────
   Un solo número gobierna la portada entera: dónde parte la pantalla. Lo leen
   el recorte del panel oscuro y el recorte de la capa clara, así que los dos
   no pueden desincronizarse por construcción.

   Va registrado con @property para que se pueda transicionar. Una variable sin
   registrar salta de un valor al otro; registrada como <percentage>, el
   navegador la interpola. Y como solo alimenta clip-path —nunca width ni
   grid— moverla no dispara layout: es puro compositor. */
@property --costura {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 50%;
}

.hero {
  position: relative;
  padding: 0;
  background:
    radial-gradient(120% 80% at 12% -10%, #1B3D5C 0%, transparent 58%),
    radial-gradient(90% 70% at 92% 4%, rgba(14, 143, 151, .22) 0%, transparent 60%),
    var(--ink);
}

/* El aire de la portada vive aquí y no en .hero, para que las dos capas —la
   oscura y su copia clara— midan exactamente lo mismo y se encimen al pixel. */
.hero__in {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(48px, 7vw, 84px);
  min-height: 100svh;
  padding: clamp(140px, 20vh, 210px) 0 0;
}

/* ── Partida ───────────────────────────────────────────────────────────────
   Todo lo de aquí abajo se enciende cuando app.js pone .is-split. Sin JS la
   portada queda como siempre: un solo mundo oscuro, legible de cabo a rabo. */

/* El mundo papel es el fondo; el oscuro es un panel encima recortado a la
   costura. Se recorta, no se le cambia el ancho: cambiar ancho es layout en
   cada cuadro, recortar es una operación de composición. */
.hero.is-split { background: var(--paper); }

/* El lienzo no se recorta: lo hacen sus dos pseudos. Si el recorte viviera en
   el elemento, se llevaría por delante la línea de la costura, que tiene que
   dibujarse justo en el filo. */
.hero.is-split .hero__lienzo {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* El panel del mundo oscuro. */
.hero.is-split .hero__lienzo::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 80% at 12% -10%, #1B3D5C 0%, transparent 58%),
    radial-gradient(90% 70% at 92% 4%, rgba(14, 143, 151, .22) 0%, transparent 60%),
    var(--ink);
  clip-path: inset(0 calc(100% - var(--costura)) 0 0);
}

/* Las dos capas de contenido se encinman. La oscura abajo con los enlaces de
   verdad; la clara encima, recortada al mundo papel y sin recibir clics, así
   que el cursor siempre llega al enlace de abajo y no hay dos juegos de
   enlaces para el teclado ni para el lector de pantalla. */
.hero.is-split .hero__capa { position: relative; z-index: 1; }

.hero.is-split .hero__capa--paper {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  clip-path: inset(0 0 0 var(--costura));
  color: var(--on-paper);
}

/* La paleta del mundo claro, aplicada a la copia. Es la misma maqueta con los
   colores del otro lado: por eso la costura puede caer donde sea. */
.hero.is-split .hero__capa--paper .eyebrow { color: var(--on-paper-3); }
.hero.is-split .hero__capa--paper .hero__h { color: var(--on-paper); }
.hero.is-split .hero__capa--paper .hero__sub { color: var(--on-paper-2); }
.hero.is-split .hero__capa--paper .arms { background: var(--rule-paper); border-block-color: var(--rule-paper); }
/* Papel, no transparente: si la tarjeta de la copia no pinta su fondo, se ve
   el azul noche de la tarjeta de abajo y el mundo claro se corta a la altura
   de los brazos. */
.hero.is-split .hero__capa--paper .arm { background: var(--paper); }
.hero.is-split .hero__capa--paper .arm__name { color: var(--on-paper); }
.hero.is-split .hero__capa--paper .arm__desc { color: var(--on-paper-2); }
.hero.is-split .hero__capa--paper .arm__go { color: var(--on-paper-3); }
.hero.is-split .hero__capa--paper .arm--b .arm__tag { color: var(--teal-deep); }

/* La costura cede al mundo que estás mirando. 8 puntos: suficiente para que se
   note que la página respondió, no tanto como para que el texto salte de
   mundo. 900 ms porque no es un control de interfaz — es el gesto que explica
   la empresa, y tiene que dejarse ver. */
.hero.is-split { transition: --costura 900ms var(--ease-in-out); }

@media (hover: hover) and (pointer: fine) {
  .hero.is-split:has(.arm--a:hover) { --costura: 58%; }
  .hero.is-split:has(.arm--b:hover) { --costura: 42%; }
}

/* La línea de la costura: ámbar arriba, verde azulado abajo, desvanecida en
   las dos puntas. Una regla dura de borde a borde partiría la página en dos
   documentos; ésta la parte en dos mundos.

   Es un degradado a toda la pantalla del que solo se ve una tira de 1 px,
   recortada en la costura. Así sigue a --costura sola, sin animar `left`,
   que sería layout en cada cuadro. */
.hero.is-split .hero__lienzo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    transparent, var(--amber) 18%, var(--teal) 82%, transparent);
  clip-path: inset(0 calc(100% - var(--costura)) 0 calc(var(--costura) - 1px));
}

/* La luz que respira se queda en el mundo oscuro. Suelta sobre el papel se
   vería como una mancha ámbar en una hoja. */
.hero.is-split::before,
.hero.is-split::after {
  clip-path: inset(0 calc(100% - var(--costura)) 0 0);
}

/* Los dos brazos van de borde a borde de la pantalla, no dentro del contenedor
   de 1240 px: la costura cae al 50% del viewport y las dos columnas tienen que
   caer con ella. El aire del margen se lo devuelven las dos tarjetas de las
   orillas. */
.hero__in > .arms { margin-top: 0; border-inline: 0; }
.hero__in > .arms > .arm:first-child { padding-left: var(--gut); }
.hero__in > .arms > .arm:last-child { padding-right: var(--gut); }

/* En pantalla angosta no hay dos mundos que poner lado a lado: dos columnas de
   texto de 190 px no se leen. La costura se va al 100% —el mundo oscuro cubre
   todo— y la capa clara se apaga. La portada vuelve a ser la de siempre. */
@media (max-width: 899px) {
  .hero.is-split { --costura: 100%; transition: none; }
  .hero.is-split .hero__capa--paper { display: none; }
  .hero.is-split .hero__lienzo::after { display: none; }
}

.hero__h {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(50px, 8.8vw, 124px);
  line-height: .98;
  letter-spacing: -.026em;   /* el tracking se cierra a medida que crece */
  margin: 18px 0 0;
  text-wrap: balance;
}

.hero__sub {
  max-width: 46ch;
  margin-top: clamp(22px, 3vw, 32px);
  font-size: clamp(18px, 1.5vw, 22px);
  line-height: 1.55;
  color: var(--on-ink-2);
  text-wrap: pretty;
}

/* --- Los dos brazos ------------------------------------------------------- */

.arms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: 1px;
  margin-top: clamp(48px, 7vw, 84px);
  background: var(--rule-ink);   /* el gap de 1px se ve como una regla */
  border-block: 1px solid var(--rule-ink);
}

.arm {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: clamp(28px, 3.4vw, 44px) clamp(22px, 2.6vw, 38px);
  background: var(--ink);
  position: relative;
  isolation: isolate;
  transition: background var(--t) var(--ease-out),
              transform var(--t-hover) var(--ease-out);
}

/* La franja de color es lo único que distingue un brazo del otro aquí, así que
   crece al acercarse: telegrafía a dónde lleva el clic. */
.arm::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  /* Ancho fijo y scaleX en vez de animar width: transform no toca el layout,
     corre en la GPU y no obliga al navegador a recalcular la caja. */
  width: 8px;
  z-index: 1;
  transform: scaleX(.375);
  transform-origin: left center;
  transition: transform var(--t-hover) var(--ease-out);
}
.arm--a::before { background: var(--amber); }
.arm--b::before { background: var(--teal); }

.arm__tag {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .15em;
  text-transform: uppercase;
}
.arm--a .arm__tag { color: var(--amber); }
.arm--b .arm__tag { color: #46B3BA; }

.arm__name {
  font-family: var(--display);
  font-size: clamp(26px, 2.8vw, 38px);
  line-height: 1.08;
  letter-spacing: -.019em;
  font-weight: 700;
}

.arm__desc {
  color: var(--on-ink-2);
  font-size: 15.5px;
  line-height: 1.55;
  max-width: 34ch;
}

.arm__go {
  margin-top: auto;
  padding-top: 18px;
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--on-ink-3);
  transition: color var(--t-hover) var(--ease-hover);
}
.arm__go::after { content: " →"; }

/* ============================================================================
   Secciones
   ========================================================================== */

.seg { padding: clamp(76px, 11vh, 136px) 0; }

.seg--dark { background: var(--ink); color: var(--on-ink); }
.seg--paper { background: var(--paper); color: var(--on-paper); }

.seg__head { max-width: 62ch; }

.seg__h {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(33px, 5vw, 64px);
  line-height: 1.05;
  letter-spacing: -.022em;
  margin: 16px 0 0;
  text-wrap: balance;
}

.seg__lede {
  margin-top: 22px;
  font-size: clamp(17px, 1.35vw, 20px);
  line-height: 1.6;
  color: var(--on-ink-2);
  text-wrap: pretty;
}
.seg--paper .seg__lede { color: var(--on-paper-2); }

/* --- Las cuatro líneas del brazo físico ----------------------------------
   Reglas horizontales en vez de tarjetas con borde. Menos cajas, más aire, y
   el número queda como marcador editorial en el margen.
   ---------------------------------------------------------------------- */

.lines { margin-top: clamp(48px, 7vw, 82px); }

.line {
  display: grid;
  grid-template-columns: minmax(56px, 84px) 1fr;
  gap: clamp(14px, 3vw, 40px);
  padding: clamp(28px, 3.4vw, 42px) 0;
  border-top: 1px solid var(--rule-ink);
}
.line:last-child { border-bottom: 1px solid var(--rule-ink); }

.line__n {
  font-family: var(--mono);
  font-size: clamp(13px, 1.1vw, 15px);
  font-weight: 700;
  letter-spacing: .06em;
  color: var(--amber);
  padding-top: .5em;
}

.line__h {
  font-family: var(--display);
  font-size: clamp(22px, 2.2vw, 30px);
  line-height: 1.14;
  letter-spacing: -.015em;
}

.line__list {
  margin-top: 18px;
  display: grid;
  gap: 9px;
  max-width: 62ch;
}
.line__list li {
  position: relative;
  padding-left: 22px;
  color: var(--on-ink-2);
  font-size: 15.5px;
  line-height: 1.5;
}
.line__list li::before {
  content: "—";
  position: absolute;
  left: 0;
  color: var(--amber-deep);
}

/* --- Clientes ------------------------------------------------------------- */

.clients { margin-top: clamp(52px, 7vw, 84px); }

.clients__list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
}
.clients__list li {
  font-family: var(--display);
  font-size: 16px;
  font-weight: 500;
  padding: 10px 18px;
  border: 1px solid var(--rule-ink);
  border-radius: 2px;
  color: var(--on-ink);
}

/* --- Servicios del Studio ------------------------------------------------- */

.svcs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: clamp(24px, 3vw, 44px);
  margin-top: clamp(48px, 7vw, 82px);
}

.svc { border-top: 2px solid var(--on-paper); padding-top: 20px; }

.svc__h {
  font-family: var(--display);
  font-size: clamp(19px, 1.75vw, 24px);
  line-height: 1.2;
  letter-spacing: -.013em;
}
.svc__p { margin-top: 10px; color: var(--on-paper-2); font-size: 15.5px; line-height: 1.55; }

/* --- Modelo de cobro ------------------------------------------------------ */

.model {
  margin-top: clamp(52px, 7vw, 88px);
  padding: clamp(28px, 3.6vw, 48px);
  background: var(--paper-2);
  border-left: 3px solid var(--teal);
}

.model__h {
  font-family: var(--display);
  font-size: clamp(24px, 3vw, 40px);
  line-height: 1.16;
  letter-spacing: -.02em;
  font-weight: 700;
  margin-top: 14px;
  text-wrap: balance;
}
.model__h span { color: var(--teal-deep); }

.model__p {
  margin-top: 14px;
  max-width: 58ch;
  color: var(--on-paper-2);
  font-size: 16px;
  line-height: 1.6;
}

/* ============================================================================
   Portafolio
   ========================================================================== */

.port { margin-top: clamp(64px, 9vw, 116px); }

.port__head { max-width: 56ch; }

.port__h {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(25px, 3.1vw, 40px);
  line-height: 1.08;
  letter-spacing: -.021em;
  margin-top: 14px;
}
.port__p { margin-top: 12px; color: var(--on-paper-2); font-size: 16.5px; }

.proj {
  position: relative;
  display: block;
  background: #fff;
  border: 1px solid var(--rule-paper);
  border-radius: 4px;
  margin-top: clamp(24px, 3vw, 38px);
  transition: transform var(--t-hover) var(--ease-out),
              border-color var(--t-hover) var(--ease-hover);
}

/* La sombra del hover, ya dibujada y en opacidad 0. Animar su opacidad corre en
   la GPU; transicionar box-shadow obliga a repintar la sombra en cada cuadro. */
.proj::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  box-shadow: 0 18px 44px -18px rgba(20, 33, 46, .34);
  opacity: 0;
  transition: opacity var(--t-hover) var(--ease-hover);
}

/* El destacado se parte en dos columnas en pantalla ancha: la captura pesa
   tanto como el texto porque es la prueba de que existe. */
.proj--lead { display: grid; grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .proj--lead { grid-template-columns: 1.25fr 1fr; align-items: stretch; }
}

.proj__shot {
  margin: 0;
  background: var(--paper-2);
  border-bottom: 1px solid var(--rule-paper);
  overflow: hidden;                 /* recorta el zoom de la imagen */
  border-radius: 3px 3px 0 0;       /* y respeta la esquina de la tarjeta */
}
/* El transform y su transición viven abajo, en «las capturas se destapan»: es
   ahí donde la imagen tiene su gesto completo. Ponerlos aquí también solo
   creaba una regla que siempre perdía. */
.proj__shot img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: top center;
}
@media (min-width: 900px) {
  .proj--lead .proj__shot {
    border-bottom: 0;
    border-right: 1px solid var(--rule-paper);
    border-radius: 3px 0 0 3px;     /* apilada arriba, al lado en ancho */
  }
  .proj--lead .proj__shot img { height: 100%; aspect-ratio: auto; }
}

.proj__body {
  padding: clamp(22px, 2.6vw, 36px);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.proj__kind {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--teal-deep);
}

.proj__name {
  font-family: var(--display);
  font-size: clamp(20px, 2vw, 27px);
  line-height: 1.16;
  letter-spacing: -.017em;
  font-weight: 700;
  color: var(--on-paper);
}

.proj__desc { color: var(--on-paper-2); font-size: 15.5px; line-height: 1.55; }

.proj__go {
  margin-top: auto;
  padding-top: 16px;
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--on-paper);
}
.proj__go::after { content: " ↗"; }

.port__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(24px, 3vw, 38px);
}
.port__grid .proj { margin-top: 0; }

/* ============================================================================
   Contacto
   ========================================================================== */

.contact__h {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(31px, 5.2vw, 68px);
  line-height: 1.05;
  letter-spacing: -.024em;
  text-wrap: balance;
}

.contact__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1px;
  margin-top: clamp(44px, 6vw, 72px);
  background: var(--rule-ink);
  border-block: 1px solid var(--rule-ink);
}
.contact__list > li {
  background: var(--ink);
  padding: clamp(24px, 3vw, 36px) clamp(20px, 2.4vw, 30px);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.contact__big {
  font-family: var(--display);
  font-size: clamp(18px, 1.75vw, 24px);
  font-weight: 600;
  letter-spacing: -.013em;
  color: var(--on-ink);
  transition: color var(--t-hover) var(--ease-hover);
  word-break: break-word;
}
.contact__big--plain { color: var(--on-ink-2); }

/* ============================================================================
   Pie
   ========================================================================== */

.ftr { background: var(--ink-3); padding: clamp(44px, 6vw, 68px) 0; }
.ftr__in {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  align-items: center;
  justify-content: space-between;
}
.ftr__legal { font-family: var(--mono); font-size: 11.5px; line-height: 1.9; color: var(--on-ink-3); }

/* ============================================================================
   Hover
   ----------------------------------------------------------------------------
   Detrás de (hover: hover) porque en pantalla táctil el hover se dispara al
   tocar y deja el elemento clavado en estado activo después del toque.
   ========================================================================== */

@media (hover: hover) and (pointer: fine) {
  .hdr__nav a:hover, .arm__go:hover { color: var(--on-ink); }
  .contact__big:hover { color: var(--amber); }

  .arm:hover { background: var(--ink-2); }
  .arm:hover::before { transform: scaleX(1); }
  .arm:hover .arm__go { color: var(--on-ink); }

  .clients__list li:hover { border-color: var(--amber); }

  /* La sombra vive en un pseudo-elemento y lo que se anima es su opacidad.
     Transicionar box-shadow directo obliga a repintar en cada cuadro. */
  .proj:hover { transform: translateY(-3px); border-color: rgba(20, 33, 46, .26); }
  .proj:hover::after { opacity: 1; }
  /* El acercamiento de la imagen en hover está abajo, con el resto de su
     gesto: aquí siempre perdía contra la regla más específica de allá. */
}

/* ============================================================================
   Pulsación
   ----------------------------------------------------------------------------
   El hundido es el acuse de recibo de la interfaz: confirma que escuchó antes
   de que pase nada más. Va solo en las superficies —tarjetas y chips—, nunca
   en enlaces de texto: escalar una palabra la desalinea de su línea base y se
   lee como un defecto de render, no como respuesta.

   Y va con tiempo propio, --t-press, más corto que el hover: la pulsación
   tiene que sentirse instantánea o deja de ser un acuse.
   ========================================================================== */

/* Las tarjetas son superficies grandes: un 0.97 en algo de 600 px se lee como
   un salto, no como una pulsación. Se hunden apenas, y la del portafolio sigue
   levantada mientras se hunde.

   Baja en --t-press (130 ms) y sube en --t-hover (160 ms): rápido al entrar
   para que se sienta instantáneo, un poco más lento al soltar para que el
   regreso no se lea como rebote. */
.arm:active, .proj:active { transition-duration: var(--t-press); }
.arm:active { transform: scale(.988); }
.proj:active { transform: translateY(-1px) scale(.994); }

/* Los chips de clientes no llevan pulsación: no son botones ni enlaces, no
   hay nada que acusar. */

/* ============================================================================
   Entrada
   ========================================================================== */

/* Portada. Se ve en cada visita, así que va corta: 420 ms y escalonado de 60 ms.
   Con los 620 ms y 90 ms anteriores, el bloque de los dos brazos —que es el
   destino principal— tardaba casi 900 ms en asentarse. */
[data-in] {
  opacity: 0;
  transform: translateY(10px);
  animation: entra var(--t-enter) var(--ease-out) forwards;
  animation-delay: calc(var(--in, 0) * 60ms);
}
@keyframes entra { to { opacity: 1; transform: none; } }

/* Aparición al scroll: decorativa y se ve muchas veces al bajar, así que 380 ms.
   Los 560 ms anteriores se leían como lentitud, no como elegancia. */
[data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 380ms var(--ease-out), transform 380ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal].is-in { opacity: 1; transform: none; }

/* ============================================================================
   Accesibilidad
   ========================================================================== */

/* Movimiento reducido no es "sin movimiento": se conservan opacidad y color,
   que ayudan a comprender, y se quita el desplazamiento, que es lo que marea. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  [data-in] { animation: none; opacity: 1; transform: none; }
  [data-reveal] { transform: none; transition: opacity 200ms ease; transition-delay: 0ms; }
  .proj:hover { transform: none; }
  .proj:hover .proj__shot img { transform: none; }
  a:active, button:active, .proj:active { transform: none; }
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
}

@media (prefers-reduced-transparency: reduce) {
  .hdr { background: var(--ink); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

@media (prefers-contrast: more) {
  :root {
    --on-ink-2: #D2E0EC;
    --on-ink-3: #AFC2D2;
    --on-paper-2: #2C3A48;
    --on-paper-3: #4A5765;
    --rule-ink: rgba(168, 188, 206, .38);
    --rule-paper: rgba(20, 33, 46, .34);
  }
}

/* El respaldo sin JavaScript está al final del archivo: tiene que ganarle en
   cascada a todo el bloque de movimiento expresivo que viene abajo. */

/* ============================================================================
   Movimiento expresivo
   ----------------------------------------------------------------------------
   Todo lo de abajo es más largo y más visible que el resto del sitio, y es
   deliberado. La regla de «nada sobre 300 ms» protege la interfaz que se usa
   muchas veces al día; una portada de estudio es lo contrario — se visita una
   vez, hay que demostrar oficio, y el movimiento ES la muestra del trabajo.
   Los estándares llaman a esto el presupuesto de deleite.

   Sigue sin animarse nada que no sea transform, opacity o clip-path.
   ========================================================================== */

/* --- Revelado por máscara ------------------------------------------------
   El texto sube desde detrás de un borde invisible en vez de aparecer. Es el
   gesto más reconocible del trabajo editorial y el que más se nota. */

/* Sin will-change: la animación corre una sola vez y el navegador ya promueve
   la capa solo mientras dura. Dejarlo puesto reserva ocho capas de GPU que no
   se liberan nunca, por un gesto que termina en el primer segundo. */
.mask { display: block; overflow: hidden; padding-bottom: .08em; }
.mask > i {
  display: block;
  font-style: normal;
  transform: translateY(105%);
}
.mask > i > b { font-weight: inherit; color: var(--teal-deep); }

/* La portada sube palabra por palabra. Cada palabra es su propia máscara en
   línea; el espacio entre ellas es el espacio del HTML, no un margen, así que
   el texto sigue partiéndose solo cuando la pantalla se angosta. */
.hero__l { display: block; }
.hero__h .mask { display: inline-block; vertical-align: top; }

/* 45 ms entre palabras, no 130 entre líneas: cuatro palabras a 130 ms tardan
   520 ms en salir y la última se lee como rezagada, no como cascada. */
.hero .mask > i {
  animation: sube 1000ms var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 90ms + 140ms);
}
@keyframes sube { to { transform: none; } }

/* En las secciones espera a que el bloque entre en pantalla. El contenedor no
   se desvanece: solo suben las líneas. */
[data-reveal].has-mask { opacity: 1; transform: none; }
.seg .mask > i, .port .mask > i, .model .mask > i, .contact .mask > i {
  transition: transform 1000ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

/* Los tres datos de contacto también suben desde su máscara. Es el último
   bloque de la página y el único que se quiere que actúes: que el correo y el
   teléfono lleguen con el mismo gesto que los títulos los pone al mismo nivel
   de importancia, en vez de dejarlos aparecer como pie de página. */
.contact__list .mask > i { display: block; }
.contact__list li:nth-child(1) .mask > i { transition-delay:  40ms; }
.contact__list li:nth-child(2) .mask > i { transition-delay:  95ms; }
.contact__list li:nth-child(3) .mask > i { transition-delay: 150ms; }
.contact__list li.is-in .mask > i { transform: none; }
[data-reveal].is-in .mask > i,
[data-reveal].is-in .mask > i { transform: none; }

/* Los títulos que viven dentro de un bloque revelado y no llevan data-reveal
   propio heredan el disparo del padre. */
.port__head.is-in .mask > i,
.model.is-in .mask > i { transform: none; }

/* --- Luz que respira en la portada ---------------------------------------
   Un resplandor que deriva muy despacio. No se mira, se siente: sin él la
   portada se ve impresa; con él se ve viva. 26 s para quedar por debajo del
   umbral que produce fatiga. */

.hero { position: relative; overflow: hidden; isolation: isolate; }
.hero::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -40%;
  left: -20%;
  width: 90%;
  height: 150%;
  background: radial-gradient(closest-side, rgba(200, 133, 46, .16), transparent 70%);
  animation: deriva 26s var(--ease-in-out) infinite alternate;
  will-change: transform;
}
.hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  top: -30%;
  right: -25%;
  width: 85%;
  height: 140%;
  background: radial-gradient(closest-side, rgba(14, 143, 151, .2), transparent 70%);
  animation: deriva 31s var(--ease-in-out) infinite alternate-reverse;
  will-change: transform;
}
@keyframes deriva {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(6%, 4%, 0) scale(1.14); }
}

/* --- Las reglas se dibujan ------------------------------------------------
   Cada línea del brazo físico traza su regla superior de izquierda a derecha.
   scaleX desde el origen izquierdo: nada de animar width. */

/* El contenedor no se desvanece. Si lo hiciera, el fade pelearía con el trazo
   de la regla y con la entrada del número: tres animaciones a la vez sobre el
   mismo elemento, cada una con su duración y su retraso, y ninguna se lee.
   Aquí el gesto es uno solo —la regla se traza y el número entra con ella— y
   el cuerpo la sigue con un fade corto. Todo asienta a los 600 ms en vez de
   los 880 ms de antes. */
.line[data-reveal] { opacity: 1; transform: none; }

.line__body {
  opacity: 0;
  transition: opacity 480ms var(--ease-out);
  transition-delay: calc(var(--reveal-delay, 0ms) + 140ms);
}
.line.is-in .line__body { opacity: 1; }

.line { position: relative; border-top-color: transparent !important; }
.line::before {
  content: "";
  position: absolute;
  top: -1px; left: 0; right: 0;
  height: 1px;
  background: var(--rule-ink);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 900ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.line.is-in::before { transform: scaleX(1); }
.line:last-child { border-bottom-color: transparent !important; }
.line:last-child::after {
  content: "";
  position: absolute;
  bottom: -1px; left: 0; right: 0;
  height: 1px;
  background: var(--rule-ink);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 900ms var(--ease-out) 160ms;
}
.line:last-child.is-in::after { transform: scaleX(1); }

/* El número entra desde la izquierda montado sobre su regla: sale apenas
   después de que el trazo arranca, no cuando ya terminó.

   Y se queda pegado mientras lees su línea. Sector Salud tiene cinco viñetas:
   para cuando llegas a la última, el 01 ya se fue de la pantalla y no hay
   forma de saber en cuál de las cuatro líneas vas. Pegado, el número contesta
   «dónde estoy» todo el tiempo que dura la línea, y de paso el margen deja de
   verse vacío. El tope es 96 px: los 63 del encabezado más aire. */
.line__n {
  position: sticky;
  top: 96px;
  align-self: start;           /* si se estira, ocupa toda la fila y no pega */
  transform: translateX(-14px);
  opacity: 0;
  transition: transform 640ms var(--ease-out), opacity 640ms ease;
  transition-delay: calc(var(--reveal-delay, 0ms) + 110ms);
}
.line.is-in .line__n { transform: none; opacity: 1; }

/* --- Las capturas del portafolio se destapan -----------------------------
   clip-path de abajo hacia arriba más un acercamiento que se relaja. La imagen
   no aparece: se descubre. */

/* Se usa `scale` —la propiedad independiente— en vez de `transform`, para
   dejarle `translate` libre al parallax de abajo. Si los dos vivieran en
   `transform` se pisarían y habría que elegir uno. */
.proj__shot img {
  clip-path: inset(0 0 100% 0);
  scale: 1.14;
  transition: clip-path 1500ms var(--ease-out), scale 1900ms var(--ease-out);
}
/* No baja a 1: se queda en 1.06 para que el parallax de ±2.5% tenga de dónde
   sacar imagen y no aparezcan franjas vacías en los bordes. */
.proj.is-in .proj__shot img { clip-path: inset(0 0 0 0); scale: 1.06; }

/* El hover conserva su tiempo corto: ahí sí manda el presupuesto de interfaz. */
@media (hover: hover) and (pointer: fine) {
  .proj.is-in:hover .proj__shot img {
    scale: 1.10;
    transition: scale 320ms var(--ease-out);
  }
}

/* --- Las capturas derivan al pasar -----------------------------------------
   La imagen se mueve más lento que su tarjeta mientras la tarjeta cruza la
   pantalla. Da profundidad —la captura se lee como algo detrás de la ventana
   de la tarjeta, no pegada a ella— y es lo único que le falta a un portafolio
   plano para que las tres piezas se sientan objetos y no estampas.

   Va con animation-timeline: view(), la línea de tiempo nativa de scroll. No
   toca JavaScript ni el hilo principal: corre en el compositor, así que no
   pierde cuadros cuando el navegador está ocupado cargando las imágenes. Donde
   no exista, simplemente no hay parallax y todo lo demás sigue igual. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .proj__shot img {
      animation: deriva-foto linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes deriva-foto {
      from { translate: 0 -2.5%; }
      to   { translate: 0 2.5%; }
    }
  }
}

/* --- Los chips de clientes se destapan de izquierda a derecha --------------
   Antes subían y crecían: seis cajas haciendo lo mismo que ya hace todo lo
   demás del sitio. Ahora se destapan con clip-path en la misma dirección en
   que se leen, como si alguien pasara la lista. El borde queda dibujado desde
   el principio, así que la fila no salta de ancho mientras entran — y no
   aparecen de la nada, que es lo que se siente falso.

   Cascada de 55 ms, el mismo escalonado que el resto del sitio. Seis chips a
   55 ms cierran en 275 ms; a 90 ms se leerían como lentitud. */

.clients__list li {
  clip-path: inset(0 100% 0 0);
  opacity: .35;
  transition: clip-path 700ms var(--ease-out), opacity 700ms var(--ease-out);
}
.clients.is-in .clients__list li { clip-path: inset(0 0 0 0); opacity: 1; }
.clients.is-in .clients__list li:nth-child(1) { transition-delay:  80ms; }
.clients.is-in .clients__list li:nth-child(2) { transition-delay: 160ms; }
.clients.is-in .clients__list li:nth-child(3) { transition-delay: 240ms; }
.clients.is-in .clients__list li:nth-child(4) { transition-delay: 320ms; }
.clients.is-in .clients__list li:nth-child(5) { transition-delay: 400ms; }
.clients.is-in .clients__list li:nth-child(6) { transition-delay: 480ms; }

/* --- Los servicios del Studio: la regla superior se traza ----------------- */

.svc { border-top-color: transparent !important; position: relative; }
.svc::before {
  content: "";
  position: absolute;
  top: -2px; left: 0; right: 0;
  height: 2px;
  background: var(--on-paper);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 800ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.svc.is-in::before { transform: scaleX(1); }

/* ============================================================================
   Las rutas del brazo físico
   ----------------------------------------------------------------------------
   Seis líneas a distintas alturas, anchos y velocidades, cada una recorrida
   por un destello ámbar. Es un tablero de movimiento — que es exactamente lo
   que hace este brazo: mover cosas de un punto a otro y entregarlas.

   Lo único que se anima es un translateX. El destello no es un elemento chico
   que viaja (eso obligaría a calcular la distancia por línea): es una banda
   del ancho completo de la línea cuyo degradado está casi todo transparente, y
   se desplaza de -100% a 100% de sí misma. Un solo keyframe sirve para las
   seis, sin importar cuánto miden.

   Muy tenue a propósito. Esto informa el fondo; lo que hay que leer es el
   texto que va encima.
   ========================================================================== */

#fisica { position: relative; isolation: isolate; overflow: hidden; }

.rutas {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

/* La caja mide 4 px de alto aunque la línea sea un pelo de 1 px. El
   overflow: hidden es obligatorio —recorta la banda que viaja para que no se
   vea correr fuera de la ruta— y en una caja de 1 px se llevaría por delante
   el nodo del extremo. Con 4 px caben los dos: la línea va centrada como
   fondo, el nodo cabe entero, y el recorte horizontal sigue haciendo su
   trabajo. */
.ruta {
  position: absolute;
  top: var(--y);
  left: var(--x);
  width: var(--w);
  height: 4px;
  overflow: hidden;
  background:
    linear-gradient(90deg, transparent, var(--rule-ink) 12%, var(--rule-ink) 88%, transparent)
    center / 100% 1px no-repeat;
}

/* El nodo del extremo: la ruta llega a algún lado. */
.ruta::before {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: 4px;
  height: 4px;
  background: var(--amber-deep);
  opacity: .5;
}

.ruta::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg,
      transparent 0%, transparent 40%,
      var(--amber) 48%,
      transparent 56%, transparent 100%)
    center / 100% 1px no-repeat;
  opacity: .62;
  animation: recorre var(--dur, 12s) linear infinite;
  animation-delay: var(--esp, 0s);
}

@keyframes recorre {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* ============================================================================
   La marca de la entrega
   ----------------------------------------------------------------------------
   Tres vías que salen del mismo origen, tres bultos que viajan casi juntos y
   un rombo al final que acusa recibo cuando llegan. El desfase entre los tres
   es de 180 ms: llegan agrupados, no en fila — eso es consolidación, y está
   escrito con esas palabras en la línea 04. El rombo es el del logo.

   Es el par de la marca viva del Studio: mismo lienzo de 120, mismos trazos,
   mismo ciclo de 3.2 s. Uno late porque sigue corriendo; éste entrega porque
   eso es lo que hace.
   ========================================================================== */

.carga {
  width: clamp(52px, 5.2vw, 68px);
  height: auto;
  margin: 20px 0 4px;
  overflow: visible;
}

.carga__vias line {
  stroke: var(--on-ink);
  stroke-width: 1.5;
  opacity: .16;
}

.carga__origen rect { fill: var(--on-ink); opacity: .3; }

.carga__bultos rect { fill: var(--amber); }

.carga__acuse {
  fill: none;
  stroke: var(--amber);
  stroke-width: 2;
  stroke-linejoin: round;
  opacity: .2;
}

/* Igual que el latido del Studio: arranca al entrar en pantalla. Una entrega
   que ya venía ocurriendo fuera de cuadro no se lee como que llegó. */
.carga.is-in .carga__bultos rect { animation: entrega 3.2s var(--ease-in-out) infinite; }
.carga.is-in .carga__acuse { animation: acusa 3.2s var(--ease-out) infinite; }

.carga.is-in .carga__bultos rect:nth-child(1) { animation-delay: 0s; }
.carga.is-in .carga__bultos rect:nth-child(2) { animation-delay: .18s; }
.carga.is-in .carga__bultos rect:nth-child(3) { animation-delay: .36s; }

/* 66 unidades es lo que hay del origen al rombo. Solo se anima translate y
   opacity: las dos que corren en la GPU. */
@keyframes entrega {
  0%        { transform: translateX(0);    opacity: 0; }
  10%       { opacity: 1; }
  46%       { opacity: 1; }
  56%, 100% { transform: translateX(66px); opacity: 0; }
}

@keyframes acusa {
  0%, 50%   { opacity: .2; }
  60%       { opacity: 1; }
  78%, 100% { opacity: .2; }
}

/* ============================================================================
   La marca viva del Studio
   ----------------------------------------------------------------------------
   El rombo del logo, abierto: un núcleo y cuatro nodos. Un pulso sale del
   centro hacia cada nodo por turnos, y el nodo se enciende cuando le llega.

   Dice la única cosa que este brazo promete y que casi nadie cumple: que las
   plataformas no se entregan y se abandonan, siguen corriendo. Es el latido de
   un sistema en producción, con el ciclo de 3.2 s de un indicador de estado —
   lento, no urgente: no está pidiendo atención, está avisando que sigue vivo.
   ========================================================================== */

.nodo {
  width: clamp(46px, 4.4vw, 60px);
  height: auto;
  margin: 20px 0 4px;
  overflow: visible;
}

.nodo__hilos line {
  stroke: var(--on-paper);
  stroke-width: 1.5;
  opacity: .18;
}

.nodo__puntos circle {
  fill: var(--teal-deep);
  opacity: .22;
  transform-box: fill-box;
  transform-origin: center;
}

.nodo__nucleo { fill: var(--teal-deep); }

/* El pulso: un anillo que sale del núcleo y se desvanece al crecer. Escala y
   opacidad, las dos propiedades que corren en la GPU. */
.nodo__pulso {
  fill: none;
  stroke: var(--teal);
  stroke-width: 1.5;
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0;
}

/* Arranca cuando el bloque entra en pantalla, no antes: un latido que ya venía
   latiendo fuera de cuadro no se lee como que arrancó, se lee como que ya
   estaba. Se quiere lo primero. */
.nodo.is-in .nodo__pulso { animation: latido 3.2s var(--ease-out) infinite; }
.nodo.is-in .nodo__puntos circle { animation: enciende 3.2s var(--ease-out) infinite; }

.nodo.is-in .nodo__puntos circle:nth-child(1) { animation-delay: .10s; }
.nodo.is-in .nodo__puntos circle:nth-child(2) { animation-delay: .28s; }
.nodo.is-in .nodo__puntos circle:nth-child(3) { animation-delay: .46s; }
.nodo.is-in .nodo__puntos circle:nth-child(4) { animation-delay: .64s; }

@keyframes latido {
  0%   { transform: scale(.55); opacity: 0; }
  12%  { opacity: .85; }
  46%  { transform: scale(3.1); opacity: 0; }
  100% { transform: scale(3.1); opacity: 0; }
}

@keyframes enciende {
  0%, 100% { opacity: .22; transform: scale(1); }
  14%      { opacity: 1;   transform: scale(1.35); }
  40%      { opacity: .22; transform: scale(1); }
}

/* --- Barra de progreso de lectura ----------------------------------------
   Dice cuánto falta. Es la única animación del sitio atada al scroll, y va sin
   transición a propósito: debe seguir el dedo, no perseguirlo. */

.hdr__bar {
  position: absolute;
  left: 0; bottom: -1px;
  height: 2px;
  width: 100%;
  transform: scaleX(var(--leido, 0));
  transform-origin: left center;
  background: linear-gradient(90deg, var(--amber), var(--teal));
}

/* --- La marca gira con la lectura ----------------------------------------
   El rombo da un cuarto de vuelta completo entre el inicio y el final de la
   página. Es el mismo dato que da la barra de progreso, dicho con la forma de
   la marca en vez de con una barra: cuando el rombo llegó a cuadrado, se
   acabó. Va sin transición porque sigue al dedo — igual que la barra. */

.brand__mark { rotate: calc(var(--leido, 0) * 90deg); }

/* El hover se suma al giro del scroll en vez de pelearse con él: uno vive en
   `rotate` y el otro en `transform`. */
.brand__mark { transition: transform 500ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .brand:hover .brand__mark { transform: rotate(45deg); }
}

/* --- Los dos brazos de la portada ----------------------------------------
   La franja de color es la fuente de luz. Al pasar el cursor, la tarjeta se
   ilumina desde su propia franja —ámbar el brazo físico, verde azulado el
   digital— a la altura del cursor. No es una luz genérica de hover: es el
   color de ese brazo entrando a la tarjeta, así que el gesto repite la
   estructura de la empresa en vez de decorarla.

   La sigue con retraso, no pegada al píxel. Amarrar un valor visual
   directamente a la posición del cursor se siente artificial porque le falta
   inercia; app.js interpola con un resorte crítico —sin rebote— y actualiza
   --my. Dos tarjetas, cuatro hijos cada una: el recálculo de la variable no
   pesa aquí. */

.arm--a { --arm-luz: rgba(200, 133, 46, .20); }
.arm--b { --arm-luz: rgba(14, 143, 151, .22); }

.arm::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;                 /* sobre el fondo de la tarjeta, bajo el texto */
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(420px 240px at 0% var(--my, 50%),
                              var(--arm-luz), transparent 72%);
  transition: opacity var(--t) var(--ease-hover);
}

.arm__name, .arm__desc, .arm__go, .arm__tag {
  transition: transform var(--t-hover) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .arm:hover::after { opacity: 1; }
  .arm:hover .arm__tag,
  .arm:hover .arm__name,
  .arm:hover .arm__desc,
  .arm:hover .arm__go { transform: translateX(6px); }
}

/* ============================================================================
   Movimiento reducido
   ----------------------------------------------------------------------------
   Aquí se apaga todo lo de arriba. Movimiento reducido no significa quitar la
   información: los textos quedan visibles y legibles, solo desaparece el
   desplazamiento, que es lo que provoca el malestar.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .mask { overflow: visible; }
  .mask > i { transform: none !important; animation: none !important; }
  .hero::before, .hero::after { animation: none; }
  .line::before, .line:last-child::after, .svc::before { transform: scaleX(1) !important; transition: none; }
  .line__n { transform: none !important; opacity: 1 !important; }
  .proj__shot img { clip-path: none !important; scale: 1 !important; translate: none !important; transition: none; animation: none !important; }
  .clients__list li { opacity: 1 !important; clip-path: none !important; transition: none; }
  .arm::after { display: none; }
  .brand__mark { rotate: none !important; }
  .brand:hover .brand__mark { transform: none; }
  .arm:hover .arm__tag, .arm:hover .arm__name,
  .arm:hover .arm__desc, .arm:hover .arm__go { transform: none; }
  .hdr__bar { transition: none; }
  .line__body { opacity: 1 !important; transition: none; }
  .hdr__nav a::after { transition: none; }
  /* La partición se queda: es estructura, no movimiento. Lo que se apaga es
     que la costura se mueva con el cursor. */
  .hero.is-split { transition: none; }
  .hero.is-split:has(.arm--a:hover),
  .hero.is-split:has(.arm--b:hover) { --costura: 50%; }
  /* Las rutas y el latido se detienen, pero se quedan dibujados: son parte de
     la lámina, no un adorno que aparece. Las rutas paran con su destello a la
     mitad; los nodos quedan encendidos. */
  .ruta::after { animation: none; opacity: .28; }
  .nodo__pulso { animation: none !important; opacity: 0; }
  .nodo__puntos circle { animation: none !important; opacity: .6; }
  /* Los bultos se quedan en su vía y el acuse queda dado: la lámina cuenta lo
     mismo sin que nada se mueva. */
  .carga__bultos rect { animation: none !important; opacity: 1; }
  .carga__acuse { animation: none !important; opacity: 1; }
}

/* ============================================================================
   Sin JavaScript
   ----------------------------------------------------------------------------
   Todo el movimiento expresivo arranca oculto y espera a que app.js ponga la
   clase .is-in. Sin JS esa clase nunca llega y la página se queda sin los seis
   títulos de sección, los cuatro números, los seis chips de clientes, las tres
   capturas del portafolio y todas las reglas horizontales — verificado en el
   navegador midiendo estilos computados.

   El bloque que había antes se escribió para [data-in] y [data-reveal], y se
   quedó corto cuando llegó el movimiento expresivo: ninguno de los selectores
   de abajo estaba cubierto. Va al final del archivo para ganarle en cascada a
   todo lo anterior.
   ========================================================================== */

@media (scripting: none) {
  [data-in], [data-reveal] { opacity: 1 !important; transform: none !important; animation: none !important; }
  .mask { overflow: visible; }
  .mask > i { transform: none !important; animation: none !important; }
  .line::before, .line:last-child::after, .svc::before { transform: scaleX(1) !important; transition: none; }
  .line__n { transform: none !important; opacity: 1 !important; }
  .line__body { opacity: 1 !important; transition: none; }
  .clients__list li { opacity: 1 !important; clip-path: none !important; transition: none; }
  .proj__shot img { clip-path: none !important; scale: 1 !important; transition: none; }
}
