/* ============================================================
   Layout, cabecera, hero, cards, pie.
   Depende de identidad.css (tokens). Cargar siempre después.
   ============================================================ */

/* ------------------------------------------------------------
   Grilla base
   ------------------------------------------------------------ */
.seccion__inner,
.hero__inner,
.cabecera__inner,
.pie__inner,
.caso__head-inner {
    width: 100%;
    max-width: var(--ancho-max);
    margin-inline: auto;
    padding-inline: var(--padding-lado);
}

/* .caso__cuerpo NO lleva esta grilla: cada bloque hijo ya la aplica por
   su cuenta con la clase .bq (ver bloques.css). Ponerla acá también
   duplicaba el padding-inline en cada bloque — el número de .bq-stat
   terminaba 76px más adentro que el borde de .bq-persona, que sólo
   recibía el padding una vez. */

.seccion { padding-block: clamp(var(--sp-16), 9vw, var(--sp-24)); }

.seccion__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-5);
    padding-bottom: var(--sp-8);
    margin-bottom: var(--sp-10);
    border-bottom: 1px solid var(--borde);
}

.saltar-al-contenido {
    position: absolute;
    left: var(--sp-4);
    top: -100px;
    z-index: 100;
    padding: var(--sp-3) var(--sp-5);
    background: var(--superficie);
    border: 1px solid var(--petroleo-400);
    border-radius: var(--r-sm);
    color: var(--hueso);
    transition: top var(--dur-rapido) var(--ease);
}
.saltar-al-contenido:focus { top: var(--sp-4); }

/* ------------------------------------------------------------
   Cabecera
   ------------------------------------------------------------ */
.cabecera {
    position: sticky;
    top: 0;
    z-index: 50;
    border-bottom: 1px solid var(--borde);
    /* Glass: acá sí, es una barra fija sobre contenido que scrollea */
    background: rgba(26, 26, 26, 0.82);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
}

.cabecera__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-8);
    padding-block: var(--sp-5);
}

.cabecera__logo {
    display: block;
    color: var(--hueso);
    line-height: 0;
}
/* .cabecera__logo/.pie__logo son <a> — sin esto, la regla genérica
   "a:hover { color: #A6DEE9 }" de identidad.css (más específica que
   .cabecera__logo sola) le gana en el hover, y ese celeste se filtra a
   la capa sólida del logo vía fill="currentColor" — un segundo cambio
   de color, aparte del efecto que sigue al cursor (ver [data-logo-hover]
   más abajo), que no tiene nada que ver con esa regla genérica de links. */
.cabecera__logo:hover,
.cabecera__logo:focus-visible,
.pie__logo:hover,
.pie__logo:focus-visible {
    color: var(--hueso);
}
.cabecera__logo svg {
    width: 190px;
    height: auto;
    display: block;
}

/* ---- Hover del logo: el color sigue al cursor, no aparece de golpe ----
   Referencia: https://codepen.io/carolineartz/pen/016f62ee... — un
   <h1> con mix-blend-mode revela círculos de color que persiguen al
   mouse por debajo. Acá no hay texto real para blendear (es un SVG),
   así que se adapta con dos capas apiladas del MISMO logo (ver
   includes/header.php: logo_svg() y logo_svg(true)):
     .logo__capa--base  → sólida, currentColor, siempre visible.
     .logo__capa--color → el degradado de 3 colores, recortada con una
                           máscara circular (mask-image) centrada en la
                           posición del cursor (--mx/--my, en px, los
                           actualiza assets/js/logo-hover.js en cada
                           pointermove). Fuera de ese círculo la máscara
                           es transparente, así que se sigue viendo la
                           capa sólida de abajo — el efecto de "se va
                           pintando por donde pasa el mouse" sin animar
                           nada más que la posición de la máscara. */
.cabecera__logo,
.pie__logo {
    position: relative;
}
/* Tamaño real del SVG lo siguen fijando .cabecera__logo svg / .pie__logo
   svg (más abajo/arriba) directamente en px — OJO con poner un ancho
   por % acá adentro: .logo__capa es un <span> de ancho automático sin
   nada que lo fuerce a los 190/240px del logo, así que un width:100%
   en el svg quedaría resolviendo contra un contenedor sin tamaño propio. */
.logo__capa { display: block; }
.logo__capa--color {
    position: absolute;
    inset: 0;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-rapido) var(--ease);
    --mx: 50%;
    --my: 50%;
    /* 100px de radio (200px de diámetro): más grande que el logo entero
       en la cabecera (190px de ancho), a propósito — así al entrar el
       círculo ya cubre TODO el logo, y el recorte real se nota en los
       bordes/esquinas al mover el mouse, no como un punto chico que
       hay que perseguir para encontrar. La pluma sola (el isotipo)
       mide ~50px de ancho ahí, así que esto es 4x ese tamaño. */
    mask-image: radial-gradient(circle 100px at var(--mx) var(--my), #000 0%, #000 30%, transparent 70%);
    -webkit-mask-image: radial-gradient(circle 100px at var(--mx) var(--my), #000 0%, #000 30%, transparent 70%);
}
[data-logo-hover].is-hover .logo__capa--color {
    opacity: 1;
}

.cabecera__nav {
    display: flex;
    gap: var(--sp-8);
}

.cabecera__link {
    position: relative;
    padding-bottom: 3px;
    color: var(--gris);
    font-size: var(--t-body);
    /* El subrayado se dibuja siempre y se revela con scaleX: así no
       hay salto de layout al pasar el mouse. */
    transition: color var(--dur-rapido) var(--ease);
}
.cabecera__link::after {
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 2px;
    background: var(--petroleo-400);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--dur-medio) var(--ease);
}
.cabecera__link:hover,
.cabecera__link.is-activo { color: var(--hueso); }
.cabecera__link:hover::after,
.cabecera__link.is-activo::after { transform: scaleX(1); }

/* Sólo aparece logueado (ver visitante_es_admin() e includes/header.php)
   — con el color de acento SIEMPRE puesto, no recién al pasar el mouse
   como el resto: es la única entrada del menú que no es una sección del
   sitio sino una salida hacia otro lado (el panel), y conviene que se
   note antes de tocarla, no después. */
.cabecera__link--admin { color: var(--petroleo-300); }
.cabecera__link--admin:hover { color: var(--hueso); }

/* Burger — sólo mobile */
.cabecera__burger {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 44px;
    height: 44px;
    padding: 0;
    background: none;
    border: 1px solid var(--borde);
    border-radius: var(--r-sm);
    cursor: pointer;
}
.cabecera__burger span {
    display: block;
    width: 18px;
    height: 1.5px;
    margin-inline: auto;
    background: var(--hueso);
    transition: transform var(--dur-medio) var(--ease),
                opacity var(--dur-rapido) var(--ease);
}
.cabecera__burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.cabecera__burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.cabecera__burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* Abre/cierra animando el ALTO real (grid-template-rows 0fr -> 1fr), no
   sólo opacidad — la primera versión (opacity+transform sobre
   display:none con @starting-style) abría bien, pero al cerrar el bloque
   seguía ocupando su lugar en el flujo mientras se desvanecía y recién
   al final de la transición saltaba a alto 0: el contenido de abajo daba
   un salto seco justo cuando el usuario ya daba la animación por
   terminada. Con el alto de la fila animándose en sincro con el fundido,
   el contenido de abajo acompaña todo el trayecto en vez de saltar al
   final — mismo resultado en las dos direcciones. Técnica de fr en vez
   de max-height: max-height necesita un valor fijo de "alto abierto" (a
   mano o por JS, y roto en cuanto cambia la cantidad de links); 0fr/1fr
   se anima igual pero el alto real lo sigue poniendo el contenido. */
.nav-mobile {
    display: grid;
    /* minmax(0, 0fr) y no 0fr a secas: un <flex> solo en la lista de
       grid-template-rows todavía respeta el mínimo automático de la
       fila (el tamaño de su contenido — en este caso el padding del
       wrapper de abajo), así que sin el minmax() la fila nunca
       bajaba de ~29px (el padding+borde de .nav-mobile__lista) por más
       que el fr pidiera 0 — se comprobó armando el panel y midiendo la
       altura real. minmax(0, …) reemplaza ese mínimo automático por un
       0 fijo, que es lo que realmente se necesita para que colapse del
       todo. */
    grid-template-rows: minmax(0, 0fr);
    overflow: hidden; /* recorta .nav-mobile__lista mientras la fila mide menos que su contenido */
    transition: grid-template-rows var(--dur-medio) var(--ease);
}
.nav-mobile.is-abierto { grid-template-rows: minmax(0, 1fr); }
/* Único hijo directo de la grilla de arriba — es lo que se recorta
   cuando la fila mide 0fr. min-height:0 porque el mínimo automático de
   un ítem de grid es el tamaño de su contenido (min-height:auto por
   default): sin este override, la fila no podría bajar de esa altura y
   el colapso se frenaría antes de llegar a 0. El borde/padding que antes
   vivían en .nav-mobile pasan a acá: puestos en el contenedor de la
   grilla se verían siempre, aun con la fila en 0fr (el borde no es parte
   de la fila que se recorta). */
.nav-mobile__lista {
    min-height: 0;
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--borde);
    padding: var(--sp-2) var(--padding-lado) var(--sp-5);
}
.nav-mobile__link {
    display: flex;
    align-items: center;
    min-height: 44px;
    color: var(--hueso);
    border-bottom: 1px solid var(--borde);
}
.nav-mobile__link:last-child { border-bottom: 0; }
/* Mismo criterio que .cabecera__link--admin: color de acento siempre
   puesto, para que se note que ésta no es una sección del sitio. */
.nav-mobile__link--admin { color: var(--petroleo-300); }

/* ------------------------------------------------------------
   Hero — dos columnas: perfil + mockup con slider.

   La entrada se disuelve con blur+opacity en CSS puro (ver más abajo,
   "Entrada del hero"): no depende de JS ni de scroll, así que no
   necesita el mismo failsafe que motion.js — es una @keyframes normal
   con fill-mode:both, y prefers-reduced-motion ya la neutraliza vía
   la regla global de identidad.css (animation-duration: 0.01ms).
   ------------------------------------------------------------ */
.hero {
    position: relative; /* ancla .hero__fondo / .hero__fondo-velo */
    /* Fondo sólido de base — se ve tal cual si no hay ningún caso
       destacado con imagen de fondo (.hero__fondo ni se renderiza). */
    background: linear-gradient(180deg, var(--negro) 0%, var(--petroleo-900) 100%);
    border-bottom: 1px solid var(--borde);
    overflow: clip; /* el blur no debe sangrar fuera del hero */
}

/* ---- Efecto "se achica y desaparece" al hacer scroll ----
   Referencia: https://codepen.io/traf/pen/xvydWW. .js-hero-fijo lo pone
   hero-scroll.js SÓLO cuando el contenido entra en la pantalla
   disponible (desktop/tablet grande) — en mobile el hero mide más que
   la pantalla y quedaría contenido recortado, así que ahí sigue con
   scroll normal. margin-top:100vh en la sección de al lado reserva
   exactamente el espacio que el hero fijo deja de ocupar en el flujo
   (mismo truco que el "top:100vh" del CodePen) y la hace pasar por
   encima al scrollear — por eso necesita fondo sólido, si no se vería
   el hero a través.
   top/height/position son inline (hero-scroll.js): dependen del alto
   real de .cabecera (sticky, arriba del hero) y de si ya se "soltó"
   del fixed — no son un valor fijo calculable acá. align-items:center
   evita que, en pantallas más altas que el contenido, éste quede
   pegado arriba con un hueco vacío gigante abajo. */
body.js-hero-fijo .hero {
    display: flex;
    align-items: center;
    left: 0;
    z-index: 1;
    width: 100%;
    border-radius: var(--r-md);
    will-change: transform;
}
/* TODO lo que viene después del hero tiene que ganarle en apilado, no
   sólo la sección de al lado. Un position:fixed con z-index:1 se pinta
   SIEMPRE por encima de un elemento estático, aunque ese elemento tenga
   fondo sólido — el fondo no alcanza, hace falta que sea "positioned"
   con su propio z-index. Al footer le faltaba eso: hay una banda de
   scroll (justo antes de que el hero se oculte) donde el footer ya
   subió al viewport y el hero todavía se está pintando, y ahí el hero
   le ganaba y lo tapaba. Va con ~ y no con + para cubrir cualquier
   sección que se agregue después, no sólo la primera. */
body.js-hero-fijo .hero ~ section,
body.js-hero-fijo .pie {
    position: relative;
    z-index: 2;
    background: var(--negro);
}
/* El margen que reserva el hueco del hero fijo va SÓLO en la primera
   (el resto ya fluye detrás de ella con normalidad). */
body.js-hero-fijo .hero + section {
    margin-top: 100vh;
    box-shadow: 0 -32px 64px rgba(0, 0, 0, 0.4);
}

/* ---- Fondo "glass": foto del caso activo, difuminada ----
   Misma mecánica de cross-fade que .hero__slide (opacity + is-activo),
   sincronizada por hero-slider.js. z-index bajo: todo el contenido del
   hero (position:relative + DOM posterior) pinta naturalmente encima. */
.hero__fondo {
    position: absolute;
    inset: 0;
    z-index: 0;
    overflow: hidden;
}
.hero__fondo-slide {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 1s var(--ease);
}
.hero__fondo-slide.is-activo { opacity: 1; }
.hero__fondo-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    filter: blur(32px) saturate(1.15) brightness(0.6);
    /* Agranda la imagen para que el blur no revele el borde recto */
    transform: scale(1.15);
}

/* Velo oscuro encima del fondo — mismo degradado que tenía .hero antes,
   ahora translúcido para que la foto se note pero el texto siga
   legible. Se queda quieto (no cross-fadea): sólo la foto de abajo
   cambia entre slides, el velo es constante. */
.hero__fondo-velo {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(180deg,
        rgba(26, 26, 26, 0.86) 0%,
        rgba(10, 42, 51, 0.9) 100%);
}

.hero__inner {
    position: relative; /* contexto de apilado sobre .hero__fondo-velo */
    z-index: 2; /* por encima de .hero__fondo-velo (z-index:1) */
    display: grid;
    /* Antes 1.1fr minmax(300px,380px): apenas alcanzaba para el teléfono
       solo. Con la laptop+teléfono superpuestos hace falta más lugar
       real para que la composición no se vea apretada. */
    /* Un poco menos que 620px de techo: con título/bajada/métrica ahora
       debajo de la laptop+teléfono, achicar un poco la composición ayuda
       a que la columna quede mejor centrada en su conjunto. */
    /* Antes 1fr: la columna de perfil no tiene contenido que necesite
       crecer sin límite, así que en pantallas muy anchas (ancho-max es
       1600px) ese 1fr absorbía TODO el sobrante y dejaba un hueco enorme
       entre la foto/botones y el mockup — el contenido de la columna
       quedaba pegado a la izquierda mientras la columna crecía sola.
       Con un techo (420px) y justify-content:center (abajo), el sobrante
       se reparte como margen afuera de las dos columnas por igual, no
       como un hueco entre ellas. */
    /* El techo de la columna derecha (antes 540px) manda sobre el alto
       del hero: la laptop es aspect-ratio 16/10, así que cada píxel de
       ancho de más son 0.6 de alto de más. Bajarlo a 510 es imperceptible
       de ancho y es lo que termina de hacer entrar el hero en laptops de
       768px de alto, donde el efecto de scroll si no queda apagado. */
    /* Las dos columnas se ESTIRAN hasta los bordes de la grilla en vez
       de quedar centradas con aire a los costados: .hero__inner comparte
       max-width y padding-inline con .cabecera__inner (ver "Grilla base"
       arriba), así que sin justify-content:center el borde izquierdo de
       la columna de perfil cae exactamente en la misma línea vertical
       que el logo, y el derecho del mockup en la misma que el último
       ítem del nav. La alineación con la cabecera sale sola de compartir
       la grilla — no hay que replicar ningún número.
       minmax(0, 1fr) y no 1fr en la derecha: el mínimo automático de un
       track de grilla es auto (= el contenido), y el mockup tiene hijos
       absolutos anchos que lo empujarían por encima de su reparto,
       desbordando la fila en pantallas angostas. */
    grid-template-columns: minmax(280px, 400px) minmax(0, 1fr);
    gap: clamp(var(--sp-10), 5vw, var(--sp-16));
    /* Pantalla completa: el hero ocupa el viewport menos la cabecera
       sticky que tiene encima (ver --hero-alto en identidad.css). */
    min-height: var(--hero-alto);
    /* stretch (default): ahora la fila tiene el alto de la pantalla, no
       el del contenido, así que las dos columnas reciben ese alto
       completo y cada una reparte su propio contenido adentro
       (.hero__perfil lo centra, .hero__mockup-col centra el conjunto
       mockup+texto). Con `start` las dos quedaban pegadas arriba y todo
       el sobrante de la pantalla completa se acumulaba abajo como un
       hueco muerto. */
    align-items: stretch;
    /* Antes usaba --sp-18, que no existe en la escala (salta de 14 a 16 a
       20) — un var() inválido invalida todo el clamp(), y el padding
       terminaba computando 0px en los dos lados. Por eso se veía todo
       pegado al header y a la sección de abajo.
       Bajado de 8vw/9vw (80+96px de aire a 1440) a esto: el hero tiene
       que ENTRAR en una pantalla para que el efecto de scroll se
       active (ver hero-scroll.js) y con el padding viejo no entraba en
       laptops de 768px de alto, que son mayoría — el efecto quedaba
       apagado justo donde más se usa.
       Bajado otra vez (era 8/10 y 8/12) ahora que el hero ocupa la
       pantalla entera y el contenido se centra solo: el aire de arriba
       y abajo ya lo da el alto sobrante, y cada píxel de padding se lo
       come el mockup, que es lo que se quiere ver grande. */
    padding-block: clamp(var(--sp-6), 3vw, var(--sp-8));
    padding-block: clamp(var(--sp-6), 3vw, var(--sp-8));
}

/* ---- Navegación del slider ----
   Tercer intento de posición: primero en los extremos de TODO el hero
   (se asociaba con la foto de perfil, ajena al slider); después como
   overlay flotando sobre la laptop o anclado al borde del título — en
   los dos casos terminaba tapando imagen o texto. Ahora es su propia
   fila fija DEBAJO de .hero__mockup-texto (ver markup), con flechas más
   chicas y puntos para saltar directo a un caso — nunca se superpone a
   nada. */
.hero__nav {
    align-self: flex-start; /* .hero__mockup-col centra por default; esto la deja pegada al mismo borde izquierdo que el título */
    display: flex;
    align-items: center;
    gap: var(--sp-4);
}

.hero__nav-flecha {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: none;
    padding: 0; /* <button> trae padding propio del browser (~6px); sin
                   esto, justify-content:flex-start/flex-end empujaba
                   contra ese padding invisible, no contra el borde real */
    background: none;
    border: 1px solid var(--borde);
    border-radius: var(--r-full);
    color: var(--gris);
    cursor: pointer;
    transition: border-color var(--dur-rapido) var(--ease),
                color var(--dur-rapido) var(--ease),
                transform var(--dur-rapido) var(--ease);
}
.hero__nav-flecha .icono-material { font-size: 20px; }
.hero__nav-flecha:hover { border-color: var(--petroleo-400); color: var(--petroleo-300); }
.hero__nav-flecha:active { transform: scale(0.9); }

.hero__nav-puntos {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}
.hero__nav-punto {
    width: 8px;
    height: 8px;
    flex: none;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--borde);
    cursor: pointer;
    transition: background var(--dur-rapido) var(--ease),
                transform var(--dur-rapido) var(--ease);
}
.hero__nav-punto:hover { background: var(--gris); }
.hero__nav-punto.is-activo {
    background: var(--petroleo-400);
    transform: scale(1.25);
}

/* ---- Columna izquierda: perfil ---- */
.hero__perfil {
    display: flex;
    flex-direction: column;
    /* flex-start, no center: con align-items:center cada hijo (la foto
       y la fila de botones) se centraba POR SU CUENTA según su propio
       ancho — como miden distinto, sus bordes izquierdos quedaban
       desalineados entre sí (se veía roto). flex-start los alinea entre
       ellos, y sin ancho/margen propio el bloque queda pegado al borde
       izquierdo de la columna (que ahora ya tiene su propio aire vía
       el padding-inline de .hero__inner) — el mismo criterio que
       .hero__dispositivos del otro lado. */
    align-items: flex-start;
    /* La columna recibe el alto completo de la fila (stretch): el
       contenido se centra adentro para no quedar pegado arriba con el
       resto de la pantalla vacío abajo. */
    justify-content: center;
    gap: var(--sp-6);
}

/* Foto y texto lado a lado, no apilados — la foto ocupa su propio
   recuadro a la izquierda, del mismo alto que el bloque de texto. */
/* Foto + estado/rol apilados. Título/resumen/métrica (.hero__perfil-texto)
   van DEBAJO de este bloque, no al costado — se probó lado a lado
   (.hero__perfil-top, flex-row) pero con títulos de caso largos el
   layout se rompía; apilado es más robusto ante contenido de largo
   variable, a costa de una columna izquierda más alta. */
.hero__perfil-foto {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-3);
    flex: none;
}

/* Frase personal, achicada — el título grande ahora lo ocupa el caso
   destacado (ver .hero__titulo), esto queda como posicionamiento rápido
   junto a la foto en vez de desaparecer del todo. */
.hero__tagline {
    max-width: 26ch;
    font-family: var(--font-titulo);
    font-size: var(--t-h3);
    line-height: var(--lh-snug);
    color: var(--hueso);
}

.hero__avatar-marco {
    /* Recuadro con esquinas casi rectas (ver identidad.css), rectangular
       en vez de cuadrado — más parecido a una foto de perfil real
       (retrato, más alto que ancho) que a un ícono. Más grande que la
       pasada anterior (120-170px): con el título/resumen/métrica ahora
       debajo de la laptop+teléfono en vez de al lado de la foto, hay
       lugar de sobra acá arriba sin que la columna se dispare de alto. */
    flex: none;
    width: clamp(160px, 15vw, 230px);
    aspect-ratio: 4 / 5;
    border-radius: var(--r-md);
    overflow: hidden;
    border: 1px solid var(--borde);
    background: var(--superficie);
}
.hero__avatar { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Título/bajada/métrica del caso — debajo de la laptop+teléfono, no
   junto a la foto de perfil (esa es información de la columna de al
   lado, sin relación con el caso puntual que se está mostrando).
   width:100% para alinear con .hero__dispositivos (mismo ancho, mismo
   borde izquierdo) dentro de .hero__mockup-col, que centra ambos como
   bloques pero el texto adentro queda alineado a la izquierda. */
.hero__mockup-texto {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    width: 100%;
    text-align: left;
}

.hero__estado {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}
.hero__punto {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    /* Verde y no petróleo: es la única señal de "estoy disponible" del
       hero, y verde es el color que ese mensaje ya tiene en el resto
       del sitio (alertas de éxito) — no es sólo gusto. */
    background: var(--exito);
    flex: none;
    position: relative; /* ancla el ::after del latido */
}

/* Latido lento: señal de "estoy disponible", no una alarma.
   ------------------------------------------------------------
   OJO — el pulso va en este ::after y NO en un box-shadow animado del
   propio punto, como estaba antes.

   box-shadow no se puede componer en la GPU: el navegador tiene que
   REPINTAR el elemento en cada cuadro. Y como la animación es infinite,
   eso no para nunca mientras la pestaña esté abierta — mantiene ocupado
   el hilo principal y no deja que la página quede en reposo. PageSpeed
   lo marcaba como "animación no compuesta" (era la única de la página:
   las demás corren una vez y terminan).

   transform y opacity sí se componen en la GPU: se animan sin repintar.

   El resultado se ve igual: el ::after arranca calcado al punto (mismo
   tamaño, mismo color) y escala hasta 3x mientras se desvanece, que es
   exactamente el halo que dibujaba el box-shadow al abrir su spread de
   0 a 7px sobre un punto de 7px (7 + 7 + 7 = 21px = 3x). Se pinta
   ENCIMA del punto en vez de atrás, pero al ser del mismo color no se
   nota — y así se evita el z-index: -1, que acá se comería el fondo del
   .hero y dejaría el pulso invisible.

   La regla global de prefers-reduced-motion (identidad.css) alcanza a
   *::after, así que esto queda neutralizado solo. */
.hero__punto::after,
.cv-item--actual .cv-item__punto::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--latido-color, var(--exito));
    animation: latido 2.6s var(--ease) infinite;
    pointer-events: none;
}
@keyframes latido {
    0%, 100% { transform: scale(1); opacity: 0.5; }
    60%      { transform: scale(3); opacity: 0; }
}
/* Texto del aviso — clase propia y no .eyebrow--acento (que también usa
   "Hablemos" en contacto.php y el rol en sobre-mi.php): cambiarla acá
   no debe teñir esas otras dos de verde de rebote. */
.hero__estado-texto { color: var(--exito-texto); letter-spacing: var(--ls-mono-lg); }

.hero__titulo {
    /* Muestra el título del caso destacado activo (hero-slider.js lo
       reescribe al cambiar de diapositiva) — no un texto fijo, por eso
       el tamaño está pensado para acomodar títulos de largo variable,
       no ajustado a una frase puntual. Bajado un poco más: ahora vive
       debajo de la laptop+teléfono en una columna más angosta que
       antes (540px de techo en vez de 620px). */
    font-size: clamp(1.5rem, 1rem + 1.8vw, 2.3rem);
    line-height: var(--lh-tight);
    letter-spacing: var(--ls-h1);
    max-width: 26ch;
    /* SIN min-height acá: la primera línea del título tiene que quedar
       siempre en la misma posición (arriba de la caja), tenga el título
       1 o 2 líneas — eso ya lo garantiza el flujo normal solo, sin
       ayuda de CSS. El resumen/métrica de abajo tienen que "subir"
       cuando el título es corto, no quedar separados por un hueco: ver
       hero-slider.js (ajustarAltoTitulo) — el espacio que le falta al
       título para completar 2 líneas se reserva como padding-bottom en
       .hero__mockup-texto (calculado por JS, porque depende del texto
       real de cada caso), no acá adentro. Así el total de la columna
       sigue sin saltar, pero el hueco vive al final del bloque, no
       entre el título y el resumen. */
}

.hero__resumen {
    /* Sin margin-top: .hero__perfil-texto ya pone gap entre hijos */
    max-width: 46ch;
    font-size: var(--t-body);
    line-height: 1.6;
    color: var(--gris);
}
.hero__resumen:empty { display: none; }

/* Métrica del caso destacado — mismo patrón de acento que caso-card,
   pero como texto simple con la regla de color a la izquierda en vez de
   tarjeta completa, para no competir visualmente con el título. */
.hero__metrica {
    padding-left: var(--sp-4);
    border-left: 2px solid var(--acento, var(--petroleo-400));
    font-family: var(--font-mono);
    font-size: var(--t-mono);
    letter-spacing: var(--ls-mono);
    text-transform: uppercase;
    color: var(--acento, var(--petroleo-300));
}
.hero__metrica:empty { display: none; }
.hero__metrica--petroleo { --acento: var(--petroleo-300); }
.hero__metrica--bordo    { --acento: var(--bordo-300); }
.hero__metrica--tierra   { --acento: var(--tierra-300); }

.hero__acciones {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-4);
    align-items: center;
    margin-top: var(--sp-2);
}
/* El documento "Componentes UI" invierte la jerarquía global de .btn
   (primario pasa a petróleo, secundario a bordó — ver más abajo), pero
   el hero quedó EXPLÍCITAMENTE afuera de ese cambio. Como .btn--primario
   / .btn--secundario son clases compartidas por todo el sitio, sin este
   override puntual el hero heredaría el swap igual. Esto reproduce
   los valores que tenían ANTES del documento — es intencional que
   difieran del resto de los botones del sitio. */
.hero__acciones .btn--primario {
    background: var(--bordo-500);
    border-color: var(--bordo-500);
    color: var(--hueso);
}
.hero__acciones .btn--primario:hover {
    background: var(--bordo-700);
    border-color: var(--bordo-700);
}
.hero__acciones .btn--secundario {
    background: transparent;
    border-color: var(--petroleo-400);
    color: var(--petroleo-300);
}
.hero__acciones .btn--secundario:hover {
    background: var(--petroleo-500);
    border-color: var(--petroleo-500);
    color: var(--hueso);
}


/* ------------------------------------------------------------
   Botones — jerarquía y valores del documento "Componentes UI"
   compartido: petróleo lleno = primario, bordó outline = secundario.
   OJO — esto es un CAMBIO DE JERARQUÍA respecto de lo que había antes
   (primario era bordó lleno, secundario era outline petróleo — quedó
   invertido a propósito, siguiendo el documento tal cual se pidió).
   ------------------------------------------------------------ */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 46px;
    padding: 13px var(--sp-6);
    border-radius: var(--r-xs);
    font-family: var(--font-texto);
    font-size: var(--t-body);
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    cursor: pointer;
    transition: background-color var(--dur-rapido) var(--ease),
                border-color var(--dur-rapido) var(--ease),
                color var(--dur-rapido) var(--ease),
                transform var(--dur-rapido) var(--ease);
}
.btn:active { transform: scale(0.97); }
.btn:disabled,
.btn[aria-disabled="true"] {
    cursor: not-allowed;
    pointer-events: none; /* por si es un <a>, que no ignora :disabled */
}

/* Tamaños — .btn sin modificador es el "medium" (46px) del documento */
.btn--large  { min-height: 56px; padding: 17px var(--sp-8); font-size: 17px; }
.btn--small  { min-height: 38px; padding: 10px var(--sp-4); font-size: var(--t-sm); }

.btn--primario {
    background: var(--petroleo-500);
    border: 1px solid var(--petroleo-500);
    color: var(--hueso);
}
.btn--primario:hover {
    background: var(--petroleo-600);
    border-color: var(--petroleo-600);
    color: var(--hueso);
}
.btn--primario:disabled,
.btn--primario[aria-disabled="true"] {
    background: var(--petroleo-800);
    border-color: var(--petroleo-800);
    color: var(--gris-apagado);
}

.btn--secundario {
    background: transparent;
    border: 1px solid var(--bordo-500);
    color: var(--bordo-300);
}
.btn--secundario:hover {
    /* Relleno translúcido (no sólido): a diferencia del primario, este
       botón vive sobre fondos variados (tarjetas, formularios) y un
       bordó sólido ahí compite demasiado — el documento lo pide así,
       con el texto aclarándose un escalón (bordo-300 → bordo-200,
       6.4:1) para mantener contraste sobre ese fondo más oscuro. */
    background: rgba(150, 38, 60, 0.14); /* --bordo-500 @ 14% */
    border-color: var(--bordo-400);
    color: var(--bordo-200);
}
.btn--secundario:disabled,
.btn--secundario[aria-disabled="true"] {
    background: transparent;
    border-color: var(--borde);
    color: var(--gris-oscuro);
}

/* Acento por caso — mapea metrica_color de la DB (usado en la grilla de abajo) */
.caso-card--petroleo { --acento: var(--petroleo-400); }
.caso-card--bordo    { --acento: var(--bordo-400); }
.caso-card--tierra   { --acento: var(--tierra-300); }

/* ------------------------------------------------------------
   Columna derecha: mockup de dispositivo + slider de productos
   ------------------------------------------------------------ */
.hero__mockup-col {
    display: flex;
    flex-direction: column;
    /* stretch, no center: el mockup y el bloque de texto ocupan los dos
       el ancho completo de la columna, así comparten borde izquierdo y
       derecho (el mockup se acota solo por alto, ver
       .hero__dispositivos). Con `center` cada uno se centraba por su
       cuenta según su ancho y los bordes no coincidían. */
    align-items: stretch;
    /* Centra verticalmente el conjunto mockup + texto + paginador dentro
       del alto de pantalla completa que ahora recibe la columna. */
    justify-content: center;
    /* --sp-6 y no --sp-8: son dos gaps (mockup→texto→nav), así que cada
       escalón de la escala cuesta el doble en el alto total del hero, y
       ese alto define si el efecto de scroll puede activarse o no
       (ver hero-scroll.js). */
    gap: var(--sp-6);
}

.hero__dispositivos {
    position: relative;
    /* Crece hasta ocupar todo el ancho de la columna (que ahora llega
       al margen derecho de la cabecera), pero acotado por el alto que
       queda libre en pantalla — ver --hero-mockup-ancho en
       identidad.css. Se acota por ANCHO a propósito: la laptop saca su
       alto del aspect-ratio 16/10, así que limitar el ancho la escala
       entera sin deformar nada. Limitarla por max-height la achataría,
       porque el aspect-ratio deja de mandar cuando las dos dimensiones
       están restringidas. */
    width: min(100%, var(--hero-mockup-ancho));
    /* Anclado a la IZQUIERDA, no centrado. Cuando la pantalla es baja
       (una notebook apaisada, por ejemplo) el alto disponible achica
       --hero-mockup-ancho y el mockup queda más angosto que su columna.
       Centrándolo, ese sobrante se repartía a los dos lados y el mockup
       se despegaba del borde donde arrancan el título y la bajada que
       tiene justo debajo — medido a 1400x700: la columna empieza en
       548px y la base de la laptop caía en 728px, 180px corrida.
       Con el margen sólo del lado derecho, el mockup crece y se achica
       de izquierda a derecha y su borde izquierdo no se mueve nunca.
       En pantallas altas no cambia nada: ahí el ancho ya da 100% y no
       hay sobrante que repartir. */
    margin-inline: 0 auto;
    /* El teléfono está posicionado con bottom:-9%, o sea que sobresale
       por debajo de esta caja y el flex de al lado no lo cuenta. Sin
       reservar ese alto se monta sobre el título de abajo. */
    margin-bottom: calc(var(--hero-mockup-alto) * 0.09);
}

/* Sin el efecto de hero fijo no hay ningún motivo para acotar el mockup
   por alto: el hero scrollea normal y el tope sólo lo dejaba chiquito al
   lado del bloque de texto, que ocupa el ancho completo. hero-scroll.js
   pone y saca esta clase — y el porqué de medir siempre con el tope
   puesto está explicado allá. */
.js-hero-mockup-libre .hero__dispositivos {
    width: 100%;
}

/* Mismo criterio para el teléfono solo: sin hero fijo no hay por qué
   acotarlo por alto, vuelve a su tamaño deseado. */
.js-hero-mockup-libre .hero__dispositivos--solo-telefono .hero__device {
    width: min(280px, 100%);
}

/* Debajo de 1300px el teléfono superpuesto se oculta (ver los bloques
   de tablet y notebook al final de la hoja), así que la composición
   pasa a ser sólo la laptop y su alto deja de ser el 0.608 del ancho
   que asume --hero-mockup-ancho en identidad.css.

   Sin teléfono el alto es: marco 16/10 al 89.29% del ancho
   (0.558 × ancho) + los 13px de la base. Invertido, el ancho que
   corresponde a un alto dado es (alto - 13px) / 0.558 — la cuenta
   exacta, no un factor aproximado, porque de esto depende que la
   laptop no se pase del alto disponible y apague el hero fijo.

   Va como variable en el propio elemento y no en :root para poder
   excluir --solo-telefono con un selector: ahí el mockup es un teléfono
   suelto y esta geometría no aplica. */
@media (max-width: 1299px) {
    .hero__dispositivos:not(.hero__dispositivos--solo-telefono) {
        --hero-mockup-ancho: calc((var(--hero-mockup-alto) - 13px) / 0.558);
    }
}

/* Sin ningún caso con imagen de fondo, no hay nada que mostrar en la
   laptop — vuelve al teléfono solo, centrado, como antes de sumar la
   composición. Las flechas ya no viven acá adentro (ver
   .hero__mockup-texto más abajo), así que este modo no necesita
   ocuparse de ellas. */
.hero__dispositivos--solo-telefono {
    display: flex;
    justify-content: center;
}
.hero__dispositivos--solo-telefono .hero__device {
    position: static;
    /* Los 280px son el tamaño deseado; los otros dos términos son topes.

       El tercero acota por el alto que queda libre en pantalla, igual
       que hace la laptop (ver .hero__dispositivos). Despejado: el marco
       mide 14px más que la pantalla en cada eje (6px de padding + 1px de
       borde, por lado), la pantalla es 0.45 de ancho por alto, y el 0.91
       deja afuera el 9% que el margen inferior reserva. O sea
       ancho = (0.91 × alto_disponible − 14px) × 0.45 + 14px.

       Sin esto el teléfono medía 280px SIEMPRE, sin mirar la pantalla:
       a 1400×700 el hero pedía 777px de alto contra 622 disponibles y el
       efecto de hero fijo quedaba apagado en todo caso sin imagen de
       escritorio. Pasar la pantalla a 9/20 (más alta) lo empeoraba a
       869px, así que el tope va ahora.
       El fallback del var(): si --hero-mockup-alto no resolviera, el
       calc() queda inválido, se cae toda la declaración de width, y este
       flex item pasa a width:auto — que acá NO es 100%, es el ancho del
       contenido: 14px de padding y el teléfono desaparecido.

       Con el fallback eso no pasa, PERO no salva la situación: medido,
       el teléfono queda igual en 48px, porque --hero-mockup-ancho y el
       margen inferior de .hero__dispositivos salen de la MISMA variable
       y no tienen fallback, así que el contenedor se colapsa igual y el
       100% de acá se mide contra un contenedor colapsado.

       O sea que esto es sólo higiene de la regla, no una red de
       verdad. La red real es que la variable vive en el :root de
       identidad.css: si esa hoja no carga, el sitio entero se queda sin
       diseño y el tamaño del teléfono es el menor de los problemas. */
    width: min(280px, 100%, calc((var(--hero-mockup-alto, 999px) * 0.91 - 14px) * 0.45 + 14px));
    /* Sin esto, Chromium resuelve mal el % dentro de min() como base
       flex (queda indefinido en la primera pasada del algoritmo) y
       encoge el teléfono muy por debajo de sus 280px aunque sobre
       espacio de sobra en la fila — flex-shrink:0 lo fija al ancho
       real sin importar esa resolución previa. Pasa incluso siendo el
       único hijo del flex row, no hace falta que haya hermanos. */
    flex-shrink: 0;
}

/* Con laptop: el teléfono se monta encima, superpuesto abajo a la
   derecha — misma composición que cualquier mockup de "laptop + phone"
   de referencia (Dribbble, Envato, etc.), armada acá en CSS puro. */
.hero__dispositivos:not(.hero__dispositivos--solo-telefono) .hero__device {
    position: absolute;
    /* Antes -3% (el teléfono se pasaba del borde derecho de la laptop):
       eso lo acercaba demasiado a la flecha overlay de al lado. Más
       fácil: tirar el teléfono un poco hacia adentro en vez de mover
       la flecha. */
    right: 4%;
    bottom: -9%;
    /* 37% daba un teléfono casi tan alto como la laptop entera (472px
       vs 388px de pantalla) — con bottom como ancla, eso lo disparaba
       por ARRIBA del borde superior de la laptop en vez de quedar
       apoyado en la parte de abajo. Más angosto = más bajo, proporción
       más parecida a la referencia. */
    width: 21%;
    z-index: 3;
}

.hero__device {
    width: min(280px, 100%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-5);
}

/* ---- Laptop — mismo criterio que el teléfono: marco en CSS puro,
   pantalla + "tapa" con notch arriba, base de aluminio abajo con una
   muesca chica al frente (referencia: mockups tipo MacBook). El
   disolver WebGL de acá es una SEGUNDA instancia del mismo código que
   usa el teléfono (hero-slider.js llama crearDisolverWebGL dos veces) —
   literalmente la misma animación, no una parecida. ---- */
.hero__laptop {
    position: relative;
    /* 89.29% y no 100%: la base de la laptop mide 112% y va corrida -6%
       para simular el saliente de aluminio (ver .hero__laptop-base), o
       sea que sobresale de este elemento por los dos lados. Al alinear
       el mockup con el margen de la cabecera, el elemento más ancho es
       esa base, no el marco — si el marco midiera 100%, la base se
       pasaría de la línea. 1 / 1.12 = 0.8929 deja la base midiendo
       exactamente el ancho de .hero__dispositivos, así que el borde
       visible del mockup cae justo en el margen. */
    width: 89.29%;
    margin-inline: auto;
}
.hero__laptop-marco {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 10;
    padding: 14px 14px 5px;
    background: linear-gradient(155deg, #2A2A2A 0%, #141414 100%);
    border: 1px solid #3A3A3A;
    border-radius: 14px 14px 3px 3px;
    box-shadow: var(--sombra-lg);
}
.hero__laptop-notch {
    position: absolute;
    top: 14px;
    left: 50%;
    transform: translateX(-50%);
    width: 54px;
    height: 12px;
    background: #141414;
    border-radius: 0 0 7px 7px;
    z-index: 2;
}
.hero__laptop-pantalla {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    /* Radio exterior (14px) - padding (14px) ≈ 0: casi sin aire para
       curvar, por eso casi recta — mismo criterio de anidado que el
       teléfono, ver el comentario en .hero__device-pantalla. */
    border-radius: 2px;
    background: var(--negro);
}
.hero__laptop-slides {
    list-style: none;
    margin: 0;
    padding: 0;
    position: relative;
    width: 100%;
    height: 100%;
}
.hero__laptop-slide {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 1s var(--ease);
}
.hero__laptop-slide.is-activo { opacity: 1; }
.hero__laptop-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.hero__laptop-base {
    position: relative;
    width: 112%;
    margin-left: -6%;
    height: 13px;
    background: linear-gradient(180deg, #3C3C3C 0%, #181818 100%);
    border-radius: 0 0 9px 9px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
}
.hero__laptop-muesca {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 15%;
    height: 5px;
    background: #141414;
    border-radius: 0 0 5px 5px;
}

/* Marco del teléfono — todo en CSS, ningún PNG de mockup de por medio */
.hero__device-marco {
    position: relative;
    width: 100%;
    /* Segunda pasada: 9/17 + padding 8px + radius 28px seguía viéndose
       angosto y redondeado contra la referencia (Android real, más
       rectangular, curvas discretas). Empujado más en la misma
       dirección: cuerpo más ancho, bisel más fino, esquinas casi rectas.

       El aspect-ratio YA NO VA ACÁ (antes: 9/16.5). Lo tiene la pantalla
       de adentro, y el marco saca su alto de ella más este padding.

       El motivo es que el padding es de 6px fijos, no proporcional: con
       la proporción puesta en el marco, la de la pantalla salía distinta
       según el tamaño del teléfono (a 202px de ancho daba 0.527, y en el
       modo de teléfono solo, más grande, daba otra). Una captura de
       360×800 es 0.45, así que object-fit:cover le comía una franja
       arriba y abajo, distinta en cada lugar donde aparece el mockup.
       Con la proporción en la pantalla, 360×800 entra exacto en
       cualquier tamaño. */
    padding: 6px;
    background: linear-gradient(155deg, #2A2A2A 0%, #141414 100%);
    border: 1px solid #3A3A3A;
    border-radius: 18px;
    box-shadow: var(--sombra-lg);
}
/* Botones laterales — power + volumen, apenas sobresaliendo del borde
   derecho, como en un teléfono real. No necesitan aria-hidden porque son
   pseudo-elementos sin contenido: no llegan al árbol de accesibilidad.

   OJO — acá antes decía que el aria-hidden "ya está en .hero__device".
   Es falso, y conviene que quede dicho: .hero__device es justamente el
   carrusel accesible (role="region", aria-roledescription="carrusel", y
   cada diapositiva como group — ver index.php). El que sí es
   aria-hidden es .hero__laptop, que es decorativo y duplica lo que
   muestra el teléfono. Ponerle aria-hidden al teléfono borraría el
   carrusel entero del lector de pantalla. */
.hero__device-marco::before,
.hero__device-marco::after {
    content: '';
    position: absolute;
    right: -2px;
    width: 3px;
    background: #3A3A3A;
    border-radius: 0 2px 2px 0;
}
.hero__device-marco::before { top: 21%; height: 7%; }  /* power */
.hero__device-marco::after  { top: 31%; height: 12%; } /* volumen */

/* Cámara punch-hole circular, centrada arriba — reemplaza el notch tipo
   isla anterior. Referencia: teléfono Android moderno (borde fino
   alrededor de la lente, no un rectángulo negro liso). */
.hero__device-notch {
    position: absolute;
    top: 12px;
    left: 50%;
    transform: translateX(-50%);
    width: 7px;
    height: 7px;
    background: #0A0A0A;
    border: 1px solid #2E2E2E;
    border-radius: 50%;
    z-index: 2;
}
.hero__device-pantalla {
    position: relative;
    width: 100%;
    /* 9/20 = 0.45 = 360/800, la proporción de una captura de celular
       moderno. Es la pantalla la que manda el alto del marco (ver el
       comentario de .hero__device-marco), no al revés. */
    aspect-ratio: 9 / 20;
    overflow: hidden;
    /* Antes 22px — MÁS curvo que el marco que la contiene (18px de
       radio, con sólo 6px+1px de borde de "aire" alrededor). Un radio
       interior mayor al espacio disponible no anida bien con la curva
       de afuera: en cada esquina se veía un quiebre raro donde las dos
       curvas no coinciden. Fórmula: radio_exterior - (padding + borde)
       = 18 - 7 ≈ 11px, así las dos curvas quedan concéntricas de verdad. */
    border-radius: 11px;
    background: var(--negro);
}

/* Estado vacío: ningún caso destacado con imagen todavía */
.hero__slide-vacio {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--sp-4);
    height: 100%;
    padding: var(--sp-6);
    text-align: center;
}
.hero__slide-vacio img { width: 56%; opacity: 0.5; }
.hero__slide-vacio p {
    font-size: var(--t-xs);
    line-height: 1.5;
    color: var(--gris);
}

/* Slider — las diapositivas se apilan y cruzan con opacity/transform */
.hero__slides {
    list-style: none;
    margin: 0;
    padding: 0;
    position: relative;
    width: 100%;
    height: 100%;
}
.hero__slide {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    opacity: 0;
    transform: scale(1.03);
    transition: opacity var(--dur-medio) var(--ease),
                transform var(--dur-medio) var(--ease);
    pointer-events: none;
}
.hero__slide.is-activo {
    opacity: 1;
    transform: scale(1);
    pointer-events: auto;
}
.hero__slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
/* Tinte de acento por slide, sobre el mismo SVG de relleno */
.hero__slide--petroleo img { filter: saturate(1.3) hue-rotate(0deg); }
.hero__slide--bordo    img { filter: saturate(1.2) hue-rotate(-45deg); }
.hero__slide--tierra   img { filter: saturate(1.1) hue-rotate(25deg); }

/* Canvas del disolver WebGL — se inserta por JS encima de .hero__slides
   cuando WebGL está disponible. Sin WebGL (o si falla la compilación del
   shader), el canvas nunca se agrega y queda el cross-fade CSS de arriba
   como único mecanismo — nunca hay una capa rota tapando el slider. */
.hero__device-pantalla canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}
/* Con el canvas activo, la imagen de la diapositiva se apaga (el canvas
   la reemplaza visualmente) pero el <li> sigue en el árbol de accesibilidad
   y el label de producto se mantiene visible sobre el canvas. */
.hero__device-pantalla--webgl .hero__slide img { opacity: 0; }

/* Misma mecánica en la pantalla de la laptop — segunda instancia del
   mismo disolver WebGL, ver hero-slider.js. */
.hero__laptop-pantalla canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}
.hero__laptop-pantalla--webgl .hero__laptop-slide img { opacity: 0; }

.hero__slide-label {
    position: absolute;
    left: var(--sp-4);
    bottom: var(--sp-4);
    padding: var(--sp-1) var(--sp-3);
    background: rgba(10, 10, 10, 0.72);
    backdrop-filter: blur(6px);
    border-radius: var(--r-xs);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: var(--ls-mono);
    text-transform: uppercase;
    color: var(--hueso);
}

/* Sin botón de pausa dedicado a propósito (ver ".hero__nav" más arriba
   para flechas + puntos) — decisión consciente del usuario, look
   minimalista. El autoplay se sigue pudiendo pausar pasando el mouse o
   enfocando el teléfono/la nav con teclado (hero-slider.js), sólo que
   sin un control visible dedicado para eso puntual.
*/

/* ------------------------------------------------------------
   Entrada del hero — se disuelve al cargar la página.

   Blur + opacity, sin recorte ni desplazamiento: el contenido aparece
   "enfocándose" en el lugar donde ya está, no viaja desde ningún borde.
   Es la lectura de "se disuelve" sobre la referencia del CodePen — no
   tuve acceso al código (bloquea scraping) — así que si no coincide
   con el original, pasame el HTML/CSS/JS y lo ajusto exacto.
   ------------------------------------------------------------ */
@keyframes heroDisolver {
    0%   { opacity: 0; filter: blur(16px); }
    100% { opacity: 1; filter: blur(0); }
}
.hero__perfil     { animation: heroDisolver .9s var(--ease) both; }

/* backwards y NO both, a diferencia del bloque de perfil de arriba.

   Con `both` el navegador deja aplicado el último fotograma para
   siempre, y ese fotograma incluye filter: blur(0). Un filter distinto
   de none —aunque sea un blur de CERO— saca al elemento del flujo de
   pintado normal y lo manda a su propia capa rasterizada: la laptop
   quedaba permanentemente un poco blanda, como fuera de foco, sobre
   todo en los bordes del marco y en el texto de la captura.

   `backwards` conserva la entrada (durante los .15s de retraso ya se ve
   el fotograma inicial, así que no aparece de golpe) pero al terminar
   devuelve el elemento a su estilo normal: sin filtro, sin capa, nítido.

   El bloque de perfil sigue con `both` — no se tocó a propósito, es una
   foto y ahí la diferencia no se nota. Si algún día se ve blanda, este
   es el motivo y el mismo cambio aplica. */
.hero__mockup-col { animation: heroDisolver .9s var(--ease) .15s backwards; }

/* ------------------------------------------------------------
   Grilla de casos — bento. El tamaño de cada card (chico/mediano/
   grande) la calcula PHP con las proporciones de la portada, no
   se calcula acá — ver comentario en database/schema.sql sobre por
   qué se descartó atarlo a "destacado".

   grid-auto-rows fijo (no auto): el span de "grande" necesita una
   unidad de fila estable para dar un bloque prolijo (2 filas = 2×alto
   + el gap del medio). 440px alcanza para una card "chico" completa
   con título de 2 líneas + resumen de 2 líneas (clampeado, ver
   .caso-card__resumen) sin recortarse — si en algún caso puntual no
   alcanza, el contenido de texto no crece más allá de ese clamp, así
   que el margen es real, no un cálculo optimista.
   ------------------------------------------------------------ */
/* La forma de cada card la calcula PHP a partir de las proporciones de
   la portada (caso_forma_card en includes/casos_privados.php), no un
   campo elegido a mano: una foto apaisada en una card vertical se
   recorta, y al revés queda con aire muerto arriba y abajo.

   grid-auto-rows en dos "medias filas" (210px) en vez de una fila
   entera: así una card cuadrada ocupa 2 medias filas y una horizontal
   sólo 1, y las alturas encajan sin huecos. Con filas de 440px, una
   card más baja que la fila dejaba el hueco igual. */
.casos-grid {
    display: grid;
    /* auto-fit y no auto-fill: a ciertos anchos de contenedor (738px con
       el padding/gap actuales, por ejemplo) el algoritmo de auto-fill
       calcula que "casi" entra una columna más y crea ese track de
       todos modos, vacío, con el resto de milímetro que sobra —
       aparecía como una línea angosta y suelta al borde de la grilla
       (comprobado: grid-template-columns computaba "615px 2px", una
       segunda columna fantasma de 2px). auto-fit se comporta EXACTAMENTE
       igual cuando las columnas tienen contenido; sólo difiere en que
       colapsa a 0 las que quedarían vacías, que es justo este caso. */
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    grid-auto-rows: 210px;
    gap: var(--sp-8);
    grid-auto-flow: dense; /* rellena los huecos que deja una card grande */
}

/* Las cuatro formas tienen que LEERSE distintas, no sólo llamarse
   distinto: con 210px de fila y 32px de gap, "1 columna x 2 filas" y
   "1 columna x 3 filas" dan 396x452 y 396x694 — la primera se lee
   cuadrada y la segunda claramente vertical. Si a las dos se les da el
   mismo span, la orientación automática deja de notarse. */

/* Apaisada: dos columnas de ancho por dos medias filas -> ~1.8:1. */
.caso-card--horizontal { grid-column: span 2; grid-row: span 2; }

/* Cuadrada: una columna por dos medias filas -> casi 1:1. */
.caso-card--cuadrada { grid-column: span 1; grid-row: span 2; }

/* Vertical: una columna por tres medias filas -> ~0.57:1. */
.caso-card--vertical { grid-column: span 1; grid-row: span 3; }

/* Destacada: la más grande de todas. Manda sobre la proporción de la
   imagen — destacar es una decisión editorial, no una consecuencia de
   las dimensiones del archivo. */
.caso-card--grande { grid-column: span 2; grid-row: span 3; }

/* Card = imagen a sangre; el texto vive en un overlay que aparece al
   pasar el mouse. La imagen manda, que es la idea: en un portfolio de
   UX lo que engancha es la pieza, no el copy de la tarjeta. */
.caso-card__link {
    position: relative; /* ancla .caso-card__media y __cuerpo */
    display: block;
    height: 100%;
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--r-sm);
    overflow: hidden;
    color: inherit;
    transition: border-color var(--dur-medio) var(--ease),
                transform var(--dur-medio) var(--ease);
}
.caso-card__link:hover {
    border-color: var(--acento, var(--petroleo-400));
    transform: translateY(-3px);
    color: inherit;
}

/* inset:0 en vez de aspect-ratio: el alto de la card ya lo fija la
   fila de la grilla (grid-auto-rows), así que la imagen simplemente
   llena todo. Esto además hace que "mediano" y "grande" no necesiten
   reglas propias: la imagen se adapta sola a la celda que le toque. */
.caso-card__media {
    position: absolute;
    inset: 0;
    background: var(--tierra-800);
    overflow: hidden;
}
.caso-card__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform var(--dur-medio) var(--ease);
}
.caso-card__link:hover .caso-card__media img { transform: scale(1.04); }

.caso-card__media-vacia {
    display: block;
    width: 100%;
    height: 100%;
    background: linear-gradient(135deg, var(--tierra-800) 0%, var(--petroleo-900) 100%);
}

/* Overlay de texto. Oculto por defecto, entra con el hover.
   :focus-within y no sólo :hover — si el overlay apareciera únicamente
   con el mouse, quien navega con teclado llega al link y no tiene forma
   de saber a qué caso corresponde esa imagen. */
.caso-card__cuerpo {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: var(--sp-3);
    padding: var(--sp-6);
    background: linear-gradient(180deg,
        rgba(26, 26, 26, 0.25) 0%,
        rgba(26, 26, 26, 0.88) 55%,
        rgba(10, 42, 51, 0.95) 100%);
    opacity: 0;
    transition: opacity var(--dur-medio) var(--ease);
}
.caso-card__link:hover .caso-card__cuerpo,
.caso-card__link:focus-visible .caso-card__cuerpo,
.caso-card__link:focus-within .caso-card__cuerpo {
    opacity: 1;
}

/* El contenido acompaña con un desplazamiento corto — sin esto el
   bloque entero aparece "de golpe" y se lee como un parpadeo. */
.caso-card__cuerpo > * {
    transform: translateY(8px);
    transition: transform var(--dur-medio) var(--ease);
}
.caso-card__link:hover .caso-card__cuerpo > *,
.caso-card__link:focus-within .caso-card__cuerpo > * {
    transform: none;
}

/* Sin hover (touch) no hay forma de revelar nada: las cards quedarían
   como imágenes anónimas y el sitio sería inusable en celular. Ahí el
   overlay va siempre visible — el degradado ya nace pensado para que el
   texto se lea encima de cualquier foto. */
@media (hover: none) {
    .caso-card__cuerpo { opacity: 1; }
    .caso-card__cuerpo > * { transform: none; }
}

/* Caso sin imagen de portada: no hay foto que mostrar, así que ocultar
   el texto dejaría una card literalmente en blanco (ver index.php). */
.caso-card--sin-imagen .caso-card__cuerpo { opacity: 1; }
.caso-card--sin-imagen .caso-card__cuerpo > * { transform: none; }

.caso-card__titulo {
    font-size: var(--t-h3);
    color: var(--hueso);
    /* El overlay tiene el alto de la card (fijo, ver .casos-grid) y el
       texto se apila desde abajo: sin tope de líneas, un título largo
       empuja al resto hacia arriba hasta salirse por el borde. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.caso-card__resumen {
    font-size: var(--t-sm);
    color: var(--hueso-dim);
    /* Mismo motivo que el título. 3 y no 2: acá el resumen es el cuerpo
       principal del overlay (ya no compite con el rol, que se sacó de
       la card), así que puede respirar una línea más. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.caso-card__metrica {
    font-family: var(--font-mono);
    font-size: var(--t-mono);
    letter-spacing: var(--ls-mono);
    text-transform: uppercase;
    color: var(--acento, var(--petroleo-300));
}

/* Aviso de publicación protegida — ocupa el lugar del resumen en la
   card (ver index.php). Va en el tono apagado y no en un rojo de
   alarma: no es un error, es una condición de acceso. */
.caso-card__lock {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--t-sm);
    color: var(--gris);
}
.caso-card__lock .icono-material {
    font-size: 18px;
    flex: none;
}

/* Sin margin-top:auto: el overlay ya alinea todo abajo con
   justify-content:flex-end, y un margen automático acá volvería a
   empujar el resto del texto contra el borde de arriba, separando el
   CTA del bloque al que pertenece. */
.caso-card__cta {
    padding-top: var(--sp-1);
    font-size: var(--t-sm);
    font-weight: 600;
    color: var(--petroleo-300);
}
.caso-card__flecha {
    display: inline-block;
    transition: transform var(--dur-rapido) var(--ease);
}
.caso-card__link:hover .caso-card__flecha { transform: translateX(4px); }

/* ------------------------------------------------------------
   Paginador de la grilla de casos
   ------------------------------------------------------------ */
.paginador {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    margin-top: var(--sp-10);
}

.paginador__flecha {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    flex: none;
    border: 1px solid var(--borde);
    border-radius: var(--r-full);
    color: var(--gris);
    transition: border-color var(--dur-rapido) var(--ease),
                color var(--dur-rapido) var(--ease);
}
.paginador__flecha:hover { border-color: var(--petroleo-400); color: var(--petroleo-300); }
.paginador__flecha.is-deshabilitada {
    opacity: 0.35;
    pointer-events: none;
}

.paginador__pagina {
    display: grid;
    place-items: center;
    min-width: 36px;
    height: 36px;
    padding-inline: var(--sp-2);
    border-radius: var(--r-full);
    font-family: var(--font-mono);
    font-size: var(--t-sm);
    color: var(--gris);
    transition: background var(--dur-rapido) var(--ease),
                color var(--dur-rapido) var(--ease);
}
.paginador__pagina:hover { color: var(--hueso); }
.paginador__pagina.is-activa {
    background: var(--petroleo-400);
    color: var(--negro);
    font-weight: 600;
}

/* ------------------------------------------------------------
   Página de caso
   ------------------------------------------------------------ */
/* ------------------------------------------------------------
   Filtro por categoría de la grilla (ver index.php)

   --cat-color lo inyecta PHP inline en cada chip con el color que se
   le puso a esa categoría en el panel. Todo lo demás (tamaños, estados,
   foco) sale de acá: el inline sólo aporta el tono, nunca layout.
   ------------------------------------------------------------ */
.filtros {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    margin-bottom: var(--sp-8);
}

.filtro-chip {
    --cat-color: var(--gris);
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    border: 1px solid var(--borde);
    border-radius: var(--r-full);
    color: var(--hueso-dim);
    font-size: var(--t-sm);
    text-decoration: none;
    transition: border-color var(--dur-rapido) var(--ease),
                color var(--dur-rapido) var(--ease),
                background var(--dur-rapido) var(--ease);
}
.filtro-chip:hover {
    border-color: var(--cat-color);
    color: var(--hueso);
}

/* Activo: el color de la categoría se usa translúcido de fondo y pleno
   en el borde/texto. color-mix y no un segundo hex guardado en la DB —
   el panel elige UN color y de ahí salen todos los estados. */
.filtro-chip.is-activo {
    border-color: var(--cat-color);
    color: var(--cat-color);
    background: color-mix(in srgb, var(--cat-color) 14%, transparent);
}

.filtro-chip__punto {
    width: 8px;
    height: 8px;
    flex: none;
    border-radius: 50%;
    background: var(--cat-color);
}

.filtro-chip__n {
    font-family: var(--font-mono, monospace);
    font-size: var(--t-xs);
    color: var(--gris);
}
.filtro-chip.is-activo .filtro-chip__n { color: inherit; }

/* Punto de color de la categoría en la card de la grilla y en la ficha
   del caso — misma pieza, dos contextos. */
.caso-card__eyebrow,
.caso__eyebrow {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}
.caso-card__punto,
.caso__punto {
    width: 7px;
    height: 7px;
    flex: none;
    border-radius: 50%;
    background: var(--cat-color, var(--petroleo-400));
}

/* ---- Animación al filtrar / paginar (ver filtro-casos.js) ----
   Cada card lleva un view-transition-name propio, así que el navegador
   empareja "la misma card" entre el antes y el después y la mueve a su
   nueva posición sola. Acá sólo se define cómo entran y salen las que
   NO tienen par: las que aparecen o dejan de aplicar al filtro. */
@keyframes caso-entra {
    from { opacity: 0; transform: scale(0.94); }
    to   { opacity: 1; transform: scale(1); }
}
@keyframes caso-sale {
    from { opacity: 1; transform: scale(1); }
    to   { opacity: 0; transform: scale(0.94); }
}

@keyframes vt-fade-out { from { opacity: 1; } to   { opacity: 0; } }
@keyframes vt-fade-in  { from { opacity: 0; } to   { opacity: 1; } }

/* El * alcanza a todos los nombres caso-<slug> de una, sin tener que
   emitir una regla por caso desde PHP. */
::view-transition-old(*) {
    animation: caso-sale var(--dur-medio) var(--ease) both;
}
::view-transition-new(*) {
    animation: caso-entra var(--dur-medio) var(--ease) both;
}

/* OJO: ese * también alcanza a `root`, el snapshot de TODO el resto de
   la página (cabecera, hero, chips). Sin esta excepción, el escalado de
   arriba se le aplicaría a la página entera y el filtro volvería a
   sentirse como una recarga — exactamente lo que se quiere evitar.
   Va después y con nombre explícito para ganarle al *.
   Un fade liso sí queda: el contenido de root es casi idéntico antes y
   después (lo único que cambia es el chip activo), así que se ve como
   un cambio de color suave y no como un parpadeo de página. */
::view-transition-old(root) {
    animation: vt-fade-out var(--dur-medio) var(--ease) both;
}
::view-transition-new(root) {
    animation: vt-fade-in var(--dur-medio) var(--ease) both;
}

/* Duración del desplazamiento de las cards que sí tienen par (las que
   siguen aplicando al filtro y sólo cambian de lugar). */
::view-transition-group(*) { animation-duration: var(--dur-medio); }

@media (prefers-reduced-motion: reduce) {
    ::view-transition-old(*),
    ::view-transition-new(*),
    ::view-transition-group(*) { animation: none !important; }
}

/* ------------------------------------------------------------
   Volver, arriba de un case study (ver caso.php + caso-volver.js)
   ------------------------------------------------------------ */
/* ------------------------------------------------------------
   Publicación protegida — pantalla de desbloqueo (ver caso.php)
   ------------------------------------------------------------ */
.caso-lock {
    max-width: 46ch;
    margin-inline: auto;
    padding-block: clamp(var(--sp-12), 8vw, var(--sp-20));
    text-align: center;
}

.caso-lock__icono {
    font-size: 44px;
    color: var(--petroleo-300);
}

.caso-lock__titulo {
    margin: var(--sp-2) 0 var(--sp-4);
    font-size: var(--t-h2);
    line-height: var(--lh-tight);
}

.caso-lock__texto {
    color: var(--hueso-dim);
    margin-bottom: var(--sp-8);
}

.caso-lock__form {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--sp-3);
}

.caso-lock__input {
    flex: 1 1 220px;
    min-width: 0;
    padding: var(--sp-3) var(--sp-4);
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--r-sm);
    color: var(--hueso);
    font-family: inherit;
    font-size: var(--t-body);
}
.caso-lock__input:focus {
    outline: none;
    border-color: var(--petroleo-400);
}
/* El input queda deshabilitado mientras corre un bloqueo por intentos
   fallidos: el estilo tiene que dejarlo claro, si no parece que el
   formulario está roto. */
.caso-lock__input:disabled,
.caso-lock__form .btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.caso-lock__error {
    margin-top: var(--sp-4);
    font-size: var(--t-sm);
    color: var(--bordo-300);
}

.caso-lock__volver {
    margin-top: var(--sp-10);
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--sp-3);
    font-size: var(--t-sm);
    color: var(--gris);
}

.caso__volver {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    align-self: flex-start;
    color: var(--gris);
    font-size: var(--t-sm);
    text-decoration: none;
    transition: color var(--dur-rapido) var(--ease);
}
.caso__volver:hover { color: var(--petroleo-300); }
.caso__volver .icono-material {
    font-size: 20px;
    transition: transform var(--dur-rapido) var(--ease);
}
/* La flecha se corre hacia donde lleva el link: refuerza "atrás" sin
   depender de que se lea el texto. */
.caso__volver:hover .icono-material { transform: translateX(-3px); }

@media (prefers-reduced-motion: reduce) {
    .caso__volver .icono-material { transition: none; }
    .caso__volver:hover .icono-material { transform: none; }
}

.caso__head {
    background: linear-gradient(180deg, var(--negro) 0%, var(--petroleo-900) 100%);
    border-bottom: 1px solid var(--borde);
    padding-block: clamp(var(--sp-14), 7vw, var(--sp-20)) 0;
}

.caso__head-inner {
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
    padding-bottom: clamp(var(--sp-10), 5vw, var(--sp-14));
}

.caso__titulo { max-width: 24ch; }

.caso__resumen {
    font-size: var(--t-body-lg);
    color: var(--hueso-dim);
    max-width: 62ch;
}

.caso__ficha {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-8);
    margin: var(--sp-3) 0 0;
    padding-top: var(--sp-5);
    border-top: 1px solid var(--borde);
}
.caso__ficha dt {
    font-family: var(--font-mono);
    font-size: var(--t-mono);
    letter-spacing: var(--ls-mono);
    text-transform: uppercase;
    color: var(--gris);
    margin-bottom: var(--sp-1);
}
.caso__ficha dd {
    margin: 0;
    font-size: var(--t-body);
    color: var(--hueso);
}

/* Los chips son más altos que un dato de texto y pueden ser varios, así
   que este ítem se queda con el ancho que sobre en la fila en vez de
   ajustarse a su contenido como los demás. */
.caso__ficha-cats {
    flex: 1 1 240px;
}
.caso__ficha-cats dd {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
}

.caso__portada {
    max-width: var(--ancho-max);
    margin-inline: auto;
    padding-inline: var(--padding-lado);
}
.caso__portada img {
    display: block;
    width: 100%;
    border: 1px solid var(--borde);
    border-radius: var(--r-md) var(--r-md) 0 0;
    border-bottom: 0;
}

/* El gap entre bloques es CHICO a propósito (20px en desktop, 16 en
   mobile). Antes era clamp(56px, 7vw, 88px) y a 1580px daba 88px de
   aire entre cada bloque: demasiado, cortaba la lectura del caso en
   pedazos sueltos.

   El aire grande, cuando hace falta, se pone donde hace falta: hay un
   bloque "Espaciado" en el panel (ver includes/bloques.php) que deja
   de 0 a 100px a elección. Así el espacio es una decisión editorial de
   cada caso y no un número global que sirve para unos y no para otros. */
.caso__cuerpo {
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
    padding-block: clamp(var(--sp-16), 8vw, var(--sp-24));
}

@media (max-width: 720px) {
    .caso__cuerpo { gap: var(--sp-4); }
}

.caso__cierre {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-4);
    max-width: var(--ancho-max);
    margin-inline: auto;
    padding: clamp(var(--sp-12), 6vw, var(--sp-16)) var(--padding-lado);
    border-top: 1px solid var(--borde);
}
.caso__cierre .eyebrow { flex-basis: 100%; }

/* ------------------------------------------------------------
   Estado vacío
   ------------------------------------------------------------ */
.vacio {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-4);
    padding: var(--sp-16) var(--sp-8);
    background: var(--superficie);
    border: 1px dashed var(--borde);
    border-radius: var(--r-sm);
}

/* ------------------------------------------------------------
   Pie
   ------------------------------------------------------------ */
.pie {
    border-top: 1px solid var(--borde);
    background: var(--negro);
}
.pie__inner {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--sp-10);
    padding-block: var(--sp-16) var(--sp-10);
}
.pie__marca { display: flex; flex-direction: column; gap: var(--sp-4); }
.pie__logo { display: block; color: var(--hueso); line-height: 0; }
.pie__logo svg { width: 240px; height: auto; }
/* El hover en sí (mask-image que sigue al cursor) es genérico, ver
   [data-logo-hover] arriba — mismo mecanismo para .cabecera__logo y
   .pie__logo, no hace falta repetirlo acá. */

.pie__links ul {
    list-style: none;
    margin: var(--sp-3) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    font-size: var(--t-sm);
}
.pie__link-marca {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
}
.pie__link-marca svg { flex: none; height: 16px; width: auto; }

.pie__copy {
    grid-column: 1 / -1;
    padding-top: var(--sp-8);
    border-top: 1px solid var(--borde);
}

/* ------------------------------------------------------------
   Reveal on scroll — estado inicial.
   motion.js quita [data-reveal] al animar. Si el JS no corre
   (o hay prefers-reduced-motion), el contenido queda visible:
   nunca escondemos contenido detrás de una animación.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: no-preference) {
    .js [data-reveal] {
        opacity: 0;
        transform: translateY(18px);
    }
}

/* ------------------------------------------------------------
   Responsive

   Formatos del hero, de angosto a ancho — nombres de referencia para
   hablar de esto en una conversación sin tener que repetir los números
   cada vez. Los rangos son los del ANCHO DE VIEWPORT (no del contenedor):

     · Mobile    360px - 680px    Foto+info apilada (lado a lado recién
                                   desde 640px). Mockup oculto entero.
     · Tablet    681px - 1099px   Foto+info al costado (misma técnica
                                   que mobile 640-680). Aparece el
                                   mockup, pero apilado DEBAJO y sin el
                                   teléfono superpuesto (sólo laptop).
     · Notebook  1100px - 1299px  Layout normal de escritorio (dos
                                   columnas). Único cambio: teléfono
                                   superpuesto oculto, igual que tablet.
     · Desktop   1300px en adelante   Sin overrides — el layout base de
                                   toda la hoja, con el mockup completo
                                   (laptop + teléfono).

   En los tres primeros, el teléfono NO se oculta si el caso destacado
   no tiene imagen de escritorio (hero__dispositivos--solo-telefono):
   ahí es el único mockup que existe.

   Quedan además dos ajustes angostos que no son "un formato" en sí,
   sino parches puntuales dentro del rango mobile:
     · 640px-680px      arranca el layout foto+info al costado (ver arriba).
     · 721px-763px       corrige una columna fantasma de .casos-grid (no
                          es del hero — ver el bloque, más abajo en el
                          archivo).
   ------------------------------------------------------------ */
/* Techo subido de 960 a 1099 (antes cortaba justo donde ahora empieza el
   tablet propio, ver el bloque 681-1099 más abajo): ese rango necesita
   el mockup apilado DEBAJO del perfil, no al costado como en desktop —
   sin este cambio, 961-1099px hubiera quedado con la grilla de dos
   columnas de escritorio mientras el resto de las reglas de tablet ya
   asumían todo apilado. hero-scroll.js tiene su propio número
   (ANCHO_MINIMO) que hay que mantener sincronizado con este mismo
   valor — el efecto de "hero fijo" está pensado para las dos columnas
   de escritorio, no para este apilado. */
@media (max-width: 1099px) {
    .hero__inner {
        grid-template-columns: 1fr;
        align-items: start;
        /* Apilado, el contenido mide bastante más que una pantalla y el
           hero scrollea igual (hero-scroll.js ni se activa acá, ver
           ANCHO_MINIMO): forzar el alto de pantalla sólo agregaría un
           hueco antes de la primera columna. */
        min-height: 0;
    }
    /* Sin la restricción de alto: acá el hero no tiene que entrar en una
       pantalla, así que el mockup usa todo el ancho disponible en vez de
       quedar chico por un presupuesto vertical que ya no aplica. */
    .hero__dispositivos {
        width: 100%;
        margin-bottom: calc(var(--sp-10) + 2vw);
    }
    .caso__ficha { gap: var(--sp-6); }
}

@media (max-width: 720px) {
    .cabecera__nav { display: none; }
    .cabecera__burger { display: flex; }
    .cabecera__logo svg { width: 150px; }

    /* Todo el perfil ya está apilado en desktop también — acá sólo achica
       la foto, que en el resto de los breakpoints depende de vw. */
    .hero__avatar-marco { width: 120px; max-width: none; }

    /* Una sola columna: el span de "mediano"/"grande" no tiene nada
       para ocupar a lo ancho, y el de fila (grande) sólo alargaría la
       card de forma rara sin ganar nada — todas quedan chico acá. */
    /* Una sola columna: los span de ancho no tienen a dónde crecer, y
       el de alto sólo estiraría la card sin ganar nada. Todas iguales. */
    .casos-grid { grid-template-columns: 1fr; grid-auto-rows: 420px; }
    .caso-card--horizontal,
    .caso-card--vertical,
    .caso-card--cuadrada,
    .caso-card--grande { grid-column: span 1; grid-row: span 1; }

    .pie__inner { grid-template-columns: 1fr; }
    .pie__logo svg { width: 200px; }
}

/* 721-763px: banda angosta donde .casos-grid en los hechos entra en
   UNA sola columna real (320px mínimo de auto-fit + el padding lateral
   fluido ya no dejan lugar para una segunda), pero una card horizontal
   o grande sigue pidiendo grid-column:span 2 — CSS le arma esa segunda
   columna igual, "de las sobras" (unos pocos px, no una columna real),
   y esa columna vacía se ve como una línea suelta pegada al borde
   derecho de la grilla.
   Comprobado midiendo grid-template-columns en el navegador: a 763px
   computaba "637px 2px" (la columna fantasma); a 764px ya son dos
   columnas reales, "320px 320px" — por eso el corte exacto es acá y no
   un número redondo. El breakpoint de ≤720px de arriba no alcanza a
   cubrir esto: ahí el motivo de bajar todo a 1fr es otro (nav
   hamburguesa, layout de teléfono), y de casualidad el ancho de
   columna real todavía no había cruzado a la zona de "casi entra una
   segunda" que sí aparece más arriba. */
@media (min-width: 721px) and (max-width: 763px) {
    .caso-card--horizontal,
    .caso-card--grande {
        grid-column: span 1;
    }
}

/* ------------------------------------------------------------
   Mobile (360-680px)
   Va DESPUÉS del breakpoint de 720px a propósito: en la franja
   681-720px sigue rigiendo ese (nav hamburguesa, foto a 120px), y estas
   reglas más específicas de ancho recién pisan las suyas por debajo de
   680px — mismo criterio de "el que viene después en el archivo gana"
   que ya usa el resto de la hoja entre breakpoints.
   ------------------------------------------------------------ */
@media (max-width: 680px) {
    /* Cabecera más baja: en un teléfono cada pixel vertical que se lleva
       una barra fija es pantalla que no llega a ver el contenido. */
    .cabecera__inner { padding-block: var(--sp-3); }
    .cabecera__logo svg { width: 128px; }
    .cabecera__burger { width: 40px; height: 40px; }

    /* El mockup de laptop+teléfono con el caso destacado no entra en un
       ancho de teléfono sin quedar diminuto e ilegible, y duplica lo que
       ya se ve completo más abajo en la grilla de casos (#trabajo) — acá
       sólo resta scroll antes de llegar a la foto y los botones. */
    .hero__mockup-col { display: none; }
    /* Sin la columna derecha, .hero__inner queda con una sola columna:
       el grid-template-columns:1fr del breakpoint de 960px ya alcanza,
       pero el gap entre columnas (pensado para dos) ya no tiene sentido
       si sólo queda una. */
    .hero__inner { gap: 0; }

    /* Foto de perfil más grande: a 120px (breakpoint de 720px) quedaba
       chica y perdida contra el ancho completo de un teléfono, más
       marginal que protagonista. clamp() en vez de un fijo para que siga
       creciendo un poco entre 360 y 680px sin pasarse en el extremo
       ancho de ese rango. */
    .hero__avatar-marco { width: clamp(150px, 42vw, 190px); }

    /* Los dos botones en una sola fila: apilados (el default de
       flex-wrap del desktop) se comen media pantalla de alto en un
       teléfono. flex:1 los reparte por mitades — con textos de largo
       parecido ("Ver casos de estudio" / "Descargar CV") quedan
       balanceados sin que uno le gane espacio al otro. */
    /* align-items:stretch (el default del .hero__acciones de desktop es
       center): "Ver casos de estudio" pasa a dos líneas acá y "Descargar
       CV" no, así que con center quedaban de distinta altura, uno más
       bajo que el otro — como par de botones, se leen mejor parejos. */
    .hero__acciones { flex-wrap: nowrap; align-items: stretch; gap: var(--sp-3); }
    .hero__acciones .btn {
        flex: 1 1 0;
        min-width: 0; /* si no, flex-basis:auto del <a> no deja encoger por debajo del texto */
        padding: 10px var(--sp-3);
        font-size: var(--t-sm);
        min-height: 42px;
    }
}

/* ------------------------------------------------------------
   640-680px: con el mockup oculto (regla de arriba) sobra todo el
   ancho de la ex-columna derecha — a 360px ese sobrante casi no existe
   y apilar sigue siendo lo único que entra, pero a partir de 640px ya
   hay lugar real al lado de la foto para el resto de la info en vez de
   dejarlo vacío.
   ------------------------------------------------------------ */
@media (min-width: 640px) and (max-width: 680px) {
    .hero__perfil {
        display: grid;
        grid-template-columns: auto 1fr;
        column-gap: var(--sp-6);
        row-gap: var(--sp-3);
        align-items: start;
    }
    /* .hero__perfil-foto deja de tener caja propia: display:contents
       saca su elemento del árbol de layout y promueve a sus hijos
       (foto, estado, rol, frase, marcas) a ítems DIRECTOS de la grilla
       de arriba — ahí quedan mezclados, en el mismo nivel, con
       .hero__acciones (su hermano). Sin esto, la foto sólo podría
       compartir fila con lo que ya estaba DENTRO de .hero__perfil-foto:
       los botones, que viven afuera como hermano, se quedarían abajo en
       su propia fila de todos modos. */
    .hero__perfil-foto { display: contents; }

    .hero__avatar-marco {
        grid-column: 1;
        /* 5 filas del lado derecho: estado, rol, frase, marcas y el
           renglón de botones — la foto las acompaña a lo alto de las
           cinco. Si el día de mañana se agrega o saca un elemento de
           este bloque, este número hay que ajustarlo a mano: grid no
           tiene forma de decir "todas las filas que hagan falta" sin
           que alguien más declare cuántas hay. */
        grid-row: 1 / span 5;
        align-self: start;
    }
    /* El resto de los hijos promovidos (estado, rol, frase, marcas) no
       necesita reglas propias: sin columna explícita, cada uno cae solo
       en la primera celda libre de la columna 2 (fila 1, 2, 3, 4 en ese
       orden) porque la columna 1 está ocupada por la foto en las cinco
       filas. .hero__acciones tampoco la necesita por el mismo motivo:
       es el sexto ítem en orden de documento y le toca la fila 5. */
}

/* ------------------------------------------------------------
   681-1099px: tablet. A diferencia del rango de arriba (mockup oculto
   del todo), acá sí entra la laptop — sólo el teléfono superpuesto
   queda afuera, porque a este ancho ya no queda espacio para los dos
   sin que se pisen o el teléfono tape justo la esquina de la pantalla
   de la laptop.
   ------------------------------------------------------------ */
@media (min-width: 681px) and (max-width: 1099px) {
    /* Foto + info al costado: misma técnica que el bloque de 640-680px
       (display:contents promueve los hijos de .hero__perfil-foto a
       ítems directos de esta grilla) — se repite acá en vez de ampliar
       aquel rango porque las dos franjas conviven con anchos de columna
       muy distintos y curan una prueba en cada una por separado es más
       simple que una sola regla intentando cubrir las dos. */
    .hero__perfil {
        display: grid;
        grid-template-columns: auto 1fr;
        column-gap: var(--sp-6);
        row-gap: var(--sp-3);
        align-items: start;
    }
    .hero__perfil-foto { display: contents; }
    .hero__avatar-marco {
        grid-column: 1;
        grid-row: 1 / span 5; /* estado, rol, frase, marcas, botones */
        align-self: start;
    }

    /* El teléfono superpuesto se oculta; la laptop se queda sola.
       :not(--solo-telefono) es a propósito: si el caso destacado no
       tiene imagen de portada de escritorio, el teléfono es LO ÚNICO
       que hay para mostrar (ver hero__dispositivos--solo-telefono en
       index.php) — ocultarlo ahí dejaría el mockup completamente
       vacío. */
    .hero__dispositivos:not(.hero__dispositivos--solo-telefono) .hero__device {
        display: none;
    }
    /* Ese margen reservaba el 9% que el teléfono sobresale por debajo
       de la laptop (ver .hero__dispositivos más arriba) — sin teléfono
       ya no hay nada que sobresalga, y dejarlo habría sumado un hueco
       vacío entre la laptop y el título de abajo. */
    .hero__dispositivos:not(.hero__dispositivos--solo-telefono) {
        margin-bottom: 0;
    }
}

/* ------------------------------------------------------------
   Notebook (1100-1299px): el escalón justo antes de desktop completo.
   Acá el hero ya vuelve a las dos columnas normales de escritorio (foto
   + info a la izquierda, mockup a la derecha — ninguna regla de arriba
   lo toca en este rango), así que lo único que hace falta es lo mismo
   que en tablet: ocultar el teléfono superpuesto y dejar sólo la
   laptop. Mismo criterio de :not(--solo-telefono) que el bloque de
   arriba: si el caso destacado no tiene imagen de escritorio, el
   teléfono es el único mockup que existe y no se oculta.
   ------------------------------------------------------------ */
@media (min-width: 1100px) and (max-width: 1299px) {
    .hero__dispositivos:not(.hero__dispositivos--solo-telefono) .hero__device {
        display: none;
    }
    /* Ese margen reservaba el 9% que el teléfono sobresale por debajo
       de la laptop (ver .hero__dispositivos más arriba, base de la
       hoja) — sin teléfono ya no hay nada que sobresalga, y dejarlo
       habría sumado un hueco vacío entre la laptop y el título de
       abajo. Mismo ajuste que el bloque de tablet de arriba. */
    .hero__dispositivos:not(.hero__dispositivos--solo-telefono) {
        margin-bottom: 0;
    }
}

/* ------------------------------------------------------------
   Sobre mí — CV interactivo (ver sobre-mi.php, assets/js/cv.js)
   ------------------------------------------------------------ */
.cv-head {
    background: linear-gradient(180deg, var(--negro) 0%, var(--petroleo-900) 100%);
    border-bottom: 1px solid var(--borde);
    padding-block: clamp(var(--sp-12), 7vw, var(--sp-20));
}
.cv-head__inner {
    width: 100%;
    max-width: var(--ancho-max);
    margin-inline: auto;
    padding-inline: var(--padding-lado);
    /* Texto a la izquierda, foto a la derecha. La columna de la foto es
       `auto` (su ancho real) y no una fracción: así el texto se queda
       con todo el resto en vez de que la foto crezca sola en pantallas
       anchas — el mismo problema que ya habíamos corregido en el hero. */
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: clamp(var(--sp-8), 5vw, var(--sp-16));
}
/* Páginas sin foto (contacto.php): una sola columna a ancho completo,
   nada que reservar del lado derecho. */
.cv-head__inner--sin-foto { grid-template-columns: 1fr; }
.cv-head__texto {
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
}

/* Igual que el avatar del hero pero más grande (ahí es 160-230px):
   acá la foto es protagonista de la página, no un dato al costado. */
.cv-head__foto {
    flex: none;
    width: clamp(200px, 24vw, 320px);
    aspect-ratio: 4 / 5;
    border-radius: var(--r-md);
    overflow: hidden;
    border: 1px solid var(--borde);
    background: var(--superficie);
}
.cv-head__foto img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.cv-head__titulo {
    font-size: var(--t-display);
    letter-spacing: var(--ls-display);
    line-height: var(--lh-tight);
}
.cv-head__perfil {
    max-width: 60ch;
    font-size: var(--t-body-lg);
    color: var(--hueso-dim);
}
.cv-head__acciones {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-4);
    margin-top: var(--sp-2);
}

/* ---- Filtros ---- */
.cv-filtros {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-4);
    margin-bottom: var(--sp-8);
}
.cv-filtros[hidden] { display: none; } /* gana sobre el display:flex de arriba */
.cv-filtros__grupo {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
}
.cv-filtro {
    padding: var(--sp-2) var(--sp-4);
    background: none;
    border: 1px solid var(--borde);
    border-radius: var(--r-full);
    color: var(--gris);
    font-family: var(--font-mono);
    font-size: var(--t-mono);
    letter-spacing: var(--ls-mono);
    text-transform: uppercase;
    cursor: pointer;
    transition: border-color var(--dur-rapido) var(--ease),
                color var(--dur-rapido) var(--ease),
                background var(--dur-rapido) var(--ease);
}
.cv-filtro:hover { border-color: var(--petroleo-400); color: var(--petroleo-300); }
.cv-filtro.is-activo {
    background: var(--petroleo-400);
    border-color: var(--petroleo-400);
    color: var(--negro);
}

/* Atenuado, NO oculto: el bloque filtrado sigue en la página (se puede
   scrollear y leer), sólo baja de jerarquía. Ocultarlo dejaría al
   usuario sin forma de llegar al contenido si el filtro no es lo que
   esperaba. */
.cv-bloque {
    transition: opacity var(--dur-medio) var(--ease);
}
.cv-bloque.is-atenuado { opacity: 0.28; }

.cv-bloque + .cv-bloque { margin-top: var(--sp-14); }
.cv-bloque__titulo {
    font-size: var(--t-h2);
    letter-spacing: var(--ls-h2);
    margin-bottom: var(--sp-8);
}

/* ---- Línea de tiempo ---- */
.cv-timeline {
    position: relative;
    list-style: none;
    margin: 0;
    padding: 0 0 0 var(--sp-8);
}
/* La línea es un pseudo-elemento y no el border-left del <ol>: un
   border arranca en el borde de la caja (y = 0), pero el primer punto
   está centrado más abajo (y = 12.5px, ver .cv-item__punto), así que el
   border dejaba un tramo de línea asomando por encima del punto. El
   pseudo-elemento arranca justo en el centro del primer punto. */
.cv-timeline::before {
    content: '';
    position: absolute;
    top: 12.5px;
    bottom: 0;
    left: 0;
    width: 1px;
    background: var(--borde);
}
.cv-item { position: relative; }
.cv-item + .cv-item { margin-top: var(--sp-10); }

.cv-item__punto {
    position: absolute;
    /* Centrado sobre la línea (left: 0 del <ol>, ver ::before arriba):
       la mitad del padding-left (32px) más la mitad del propio punto
       (9px + 2px de borde a cada lado = 13px). */
    left: calc(-1 * var(--sp-8) - 6px);
    top: 6px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    /* --cv-punto lo fija el server por ítem (ver sobre-mi.php): un color
       distinto por época, no sólo blanco/apagado. */
    background: var(--cv-punto, var(--borde));
    border: 2px solid var(--negro);
    box-sizing: content-box;
}
.cv-item--actual .cv-item__punto {
    /* Misma señal que el hero — el pulso en sí lo dibuja el ::after,
       declarado junto al del hero (buscar "@keyframes latido").
       Acá sólo se le dice de qué color: el mismo que el punto, que lo
       fija el server por ítem. Si se dejara el verde de --exito, el
       pulso —que se pinta encima— teñiría el punto de otro color. */
    --latido-color: var(--cv-punto, var(--exito));
}

.cv-item__eyebrow {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}
.cv-item__fechas { font-size: var(--t-mono); letter-spacing: var(--ls-mono); color: var(--gris); }
.cv-item__badge {
    padding: 2px var(--sp-2);
    border-radius: var(--r-full);
    background: var(--petroleo-800);
    color: var(--petroleo-300);
    font-family: var(--font-mono);
    font-size: var(--t-mono);
    letter-spacing: var(--ls-mono);
    text-transform: uppercase;
}

.cv-item__puesto {
    margin: 0;
    font-family: var(--font-titulo);
    font-size: var(--t-h3);
    color: var(--hueso);
}

.cv-item__meta {
    margin: var(--sp-1) 0 0;
    font-size: var(--t-sm);
    color: var(--gris);
}

.cv-item__resumen {
    margin: var(--sp-3) 0 0;
    max-width: 72ch;
    font-size: var(--t-sm);
    color: var(--hueso-dim);
}

/* Experiencia laboral: lista de responsabilidades (ver sobre-mi.php,
   $item['resumen'] como array). Mismo tono/ancho que .cv-item__resumen
   — son la misma pieza de contenido, sólo que estructurada en bullets
   en vez de un párrafo, así que tienen que leerse como parte del mismo
   bloque y no como un elemento distinto. */
.cv-item__bullets {
    margin: var(--sp-3) 0 0;
    padding: 0;
    max-width: 72ch;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    list-style: none;
    font-size: var(--t-sm);
    color: var(--hueso-dim);
}
.cv-item__bullets li {
    position: relative;
    padding-left: var(--sp-4);
}
/* Punto propio en vez de list-style:disc: así se puede alinear con la
   primera línea de texto con precisión (0.65em desde arriba), en vez
   de depender de cómo cada navegador posiciona el marcador nativo. */
.cv-item__bullets li::before {
    content: '';
    position: absolute;
    left: 2px;
    top: 0.65em;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--gris-apagado);
}
.cv-item__bullets strong { color: var(--hueso); font-weight: 600; }

.cv-item__tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin: var(--sp-4) 0 0;
    padding: 0;
    list-style: none;
}
.cv-item__tag {
    padding: 2px var(--sp-2);
    border: 1px solid var(--borde);
    border-radius: var(--r-xs);
    font-family: var(--font-mono);
    font-size: var(--t-mono);
    letter-spacing: var(--ls-mono);
    color: var(--gris);
}

/* ---- Herramientas ---- */
.cv-skills {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--sp-8);
}
.cv-skills__titulo { margin-bottom: var(--sp-1); }
.cv-skills__desc {
    margin: 0 0 var(--sp-4);
    max-width: 40ch;
    font-size: var(--t-sm);
    color: var(--gris);
}
.cv-skills__lista {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
}
.cv-skill {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--r-full);
    font-size: var(--t-sm);
    color: var(--hueso-dim);
}
/* Los SVG de Simple Icons vienen en 1em y con fill:currentColor, así
   que el tamaño sale del font-size del chip y el color lo hereda solo.
   Sin hover a propósito: son un listado de herramientas, no controles
   interactivos — nada pasa al hacer click, así que reaccionar al mouse
   sugería una interacción que no existe. */
.cv-skill__icono {
    flex: none;
    font-size: 1.05em; /* aplica también al fallback de Material Symbols */
    color: var(--gris);
}

/* Entrada escalonada de las herramientas. El estado FINAL es el de
   arriba (visible): la animación sólo se aplica cuando cv.js marca el
   grupo con .is-visible al entrar en pantalla. Si el JS no corre o
   IntersectionObserver no existe, las herramientas ya están visibles y
   no pasa nada — nunca quedan en cero. */
.js .cv-skills__grupo.is-visible .cv-skill {
    animation: cvSkillEntra .4s var(--ease) both;
    animation-delay: calc(var(--i, 0) * 45ms);
}
@keyframes cvSkillEntra {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

/* 860px y no 720: el corte no lo manda el breakpoint general del sitio
   sino cuándo dejan de convivir la foto (hasta 320px) y el texto sin
   que este último quede en una columna ilegible de pocas palabras. */
@media (max-width: 860px) {
    /* flex y no grid acá: apilado hace falta `order` para subir la foto,
       y en grid habría que reordenar filas explícitas — con flex es una
       sola declaración. */
    .cv-head__inner {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
    }
    /* La foto pasa ARRIBA del texto al apilarse: si quedara al final,
       en mobile habría que scrollear todo el bloque para llegar a ella
       y la página abriría sin ninguna imagen a la vista. El orden del
       DOM se mantiene (texto primero) para que un lector de pantalla
       escuche el nombre antes que la foto, que es decorativa. */
    .cv-head__foto { order: -1; }
}

@media (max-width: 720px) {
    .cv-timeline { padding-left: var(--sp-6); }
    .cv-item__punto { left: calc(-1 * var(--sp-6) - 6px); }
    .cv-bloque + .cv-bloque { margin-top: var(--sp-10); }
}

/* Tira de logos de marca bajo el tagline del hero (ver index.php).
   Sin nombres: sólo el símbolo, a color original. */
.hero__marcas {
    list-style: none;
    margin: var(--sp-1) 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-5);
}
.hero__marca {
    display: flex;
    align-items: center;
}
/* Se normaliza por ALTO y no por ancho: los cinco logos tienen
   proporciones distintas (Figma es 2:3, HTML5 casi 1:1.4, Claude y
   WordPress cuadrados). Fijar el ancho los dejaría ópticamente
   despatarrados — con alto común y ancho automático quedan alineados
   como una tira pareja. */
.hero__marca svg {
    height: 22px;
    width: auto;
    display: block;
    /* A color pleno siempre (antes: atenuados por default, color pleno
       sólo al pasar el mouse — sin hover ese estado activo pasa a ser
       el único, así que queda puesto de forma permanente). */
}

@media (max-width: 720px) {
    .hero__marcas { gap: var(--sp-4); }
    .hero__marca svg { height: 19px; }
}

/* ------------------------------------------------------------
   Contacto — formulario público (ver contacto.php)
   ------------------------------------------------------------ */
.form { display: flex; flex-direction: column; gap: var(--sp-6); }

.campo { display: flex; flex-direction: column; gap: var(--sp-2); flex: 1; min-width: 0; }
.campo label {
    font-size: var(--t-sm);
    font-weight: 600;
    color: var(--hueso);
}
.campo-fila {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-5);
}
.campo-fila .campo { min-width: 220px; }

input[type="text"],
input[type="email"],
input[type="tel"],
textarea {
    width: 100%;
    min-height: 46px;
    padding: 13px 15px;
    background: var(--negro);
    border: 1px solid var(--borde);
    border-radius: var(--r-xs);
    color: var(--hueso);
    font-family: var(--font-texto);
    font-size: var(--t-body);
    line-height: 1.5;
    transition: border-color var(--dur-rapido) var(--ease),
                box-shadow var(--dur-rapido) var(--ease);
}
input::placeholder, textarea::placeholder { color: var(--gris-apagado); }
input:hover, textarea:hover { border-color: var(--petroleo-700); }
/* El anillo (box-shadow) es lo que faltaba antes: sólo el cambio de
   borde es un indicio de foco muy débil — el documento pide un halo
   visible de 3px alrededor, no sólo el borde más claro. */
input:focus, textarea:focus {
    border-color: var(--petroleo-400);
    outline: none; /* reemplazado por el box-shadow de abajo, no se pierde el indicio de foco */
    box-shadow: 0 0 0 3px rgba(58, 157, 178, 0.18); /* --petroleo-400 @ 18% */
}
textarea { resize: vertical; min-height: 140px; }
input:disabled, textarea:disabled {
    color: var(--gris-apagado);
    border-color: #2A2826;
    cursor: not-allowed;
}

/* Estado de error/éxito por campo — antes sólo había una alerta
   general arriba del form; esto además marca CUÁL campo falló. */
.campo--error input,
.campo--error textarea {
    border-color: var(--error);
}
.campo--exito input,
.campo--exito textarea {
    border-color: var(--exito);
}
.campo__error {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--t-xs);
    color: var(--error-texto);
}
.campo__error::before {
    content: '';
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--error);
    flex: none;
}

.form__acciones { padding-top: var(--sp-2); }

.alerta {
    padding: var(--sp-4) var(--sp-5);
    border-radius: var(--r-xs);
    font-size: var(--t-sm);
}
.alerta--exito { background: var(--exito-fondo); color: var(--exito-texto); }
.alerta--error { background: var(--error-fondo); color: var(--error-texto); }

/* Honeypot: invisible para una persona, presente para un bot que
   autocompleta cada <input> que encuentra en el DOM. display:none y no
   sólo "movido fuera de pantalla" — algunos bots sí notan y evitan un
   campo simplemente corrido con position/left, pero uno con
   display:none no se renderiza ni se calcula, así que es una señal más
   dura de "esto no está pensado para completarse". tabindex="-1" en el
   input (ver contacto.php) además lo saca del recorrido con teclado, por
   si algún día se decide mostrarlo condicionalmente. */
.campo--trampa { display: none; }

.contacto-layout {
    display: grid;
    grid-template-columns: minmax(0, 640px) auto;
    gap: clamp(var(--sp-10), 6vw, var(--sp-20));
    align-items: start;
}

.contacto-directo {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    padding: var(--sp-6);
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--r-sm);
}
.contacto-directo__link {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    color: var(--hueso-dim);
    transition: color var(--dur-rapido) var(--ease);
}
.contacto-directo__link:hover { color: var(--tierra-300); }
.contacto-directo__link .icono-material { font-size: 20px; color: var(--petroleo-400); }
/* El logo de LinkedIn es un SVG a color real, no un ícono de la fuente
   (ver .icono-material arriba) — se normaliza por alto para quedar del
   mismo porte visual que el ícono de mail de al lado. */
.contacto-directo__icono { flex: none; height: 20px; width: auto; }

@media (max-width: 860px) {
    .contacto-layout { grid-template-columns: 1fr; }
}

/* ------------------------------------------------------------
   Carrusel de casos relacionados (ver caso.php + rel-carrusel.js)
   ------------------------------------------------------------ */
.rel {
    border-top: 1px solid var(--borde);
    padding-block: clamp(var(--sp-12), 8vw, var(--sp-20));
}
.rel__inner {
    width: 100%;
    max-width: var(--ancho-max);
    margin-inline: auto;
    padding-inline: var(--padding-lado);
}

.rel__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    /* sp-8 menos el padding-top que le agrega .rel__pista (sp-2): el
       espacio visible entre el título y la fila de cards queda igual
       que antes de sumarle ese padding para el hover. */
    margin-bottom: var(--sp-6);
}
.rel__titulo {
    font-family: var(--font-titulo);
    font-size: var(--t-h2);
    line-height: var(--lh-tight);
}

.rel__carrusel { position: relative; }

/* El desplazamiento es scroll nativo, no un translate por JS: se
   mantiene la inercia del sistema, el arrastre táctil y la navegación
   por teclado sin reimplementar nada. */
.rel__pista {
    list-style: none;
    margin: 0;
    /* padding-top: por spec, `overflow-x: auto` fuerza a que `overflow-y`
       compute también a `auto` (no se puede dejar uno en `visible` con el
       otro scrolleable) — así que este contenedor SÍ recorta en Y, aunque
       nunca se proponga scrollear verticalmente. Sin aire arriba, el
       translateY(-3px) del hover de .rel__card se comía contra ese borde
       y la card se veía cortada. El margin-bottom de .rel__head se acorta
       en la misma medida para que el espacio visible total no cambie. */
    padding: var(--sp-2) 0 var(--sp-4);
    display: flex;
    gap: var(--sp-5);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* Sin esto, al hacer Tab a una card que está fuera de vista el
       navegador salta de golpe; con scroll-behavior acompaña. */
    scroll-behavior: smooth;
    /* La barra nativa sobre fondo oscuro queda fea y ocupa alto; el
       desplazamiento ya se comunica con las flechas y el corte visual
       de la card siguiente. */
    scrollbar-width: none;
}
.rel__pista::-webkit-scrollbar { display: none; }

.rel__item {
    flex: 0 0 auto;
    /* min() y no un ancho fijo: en mobile una card de 340px se saldría
       de pantalla y obligaría a scrollear de más para leer una sola. */
    width: min(340px, 78vw);
    scroll-snap-align: start;
}

.rel__card {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    height: 100%;
    min-height: 220px;
    padding: var(--sp-6);
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--r-sm);
    text-decoration: none;
    transition: border-color var(--dur-rapido) var(--ease),
                transform var(--dur-rapido) var(--ease);
}
.rel__card:hover {
    border-color: var(--petroleo-400);
    transform: translateY(-3px);
}

.rel__card-cat {
    --cat-color: var(--gris);
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    color: var(--cat-color);
}
.rel__punto {
    width: 7px;
    height: 7px;
    flex: none;
    border-radius: 50%;
    background: var(--cat-color);
}

.rel__card-titulo {
    margin: 0;
    font-family: var(--font-titulo);
    font-size: var(--t-h3);
    line-height: var(--lh-snug);
    color: var(--hueso);
}

/* margin-top:auto para que el CTA quede pegado abajo aunque los
   títulos tengan largos distintos: si no, cada card lo pone a distinta
   altura y la fila se ve desprolija. */
.rel__card-cta {
    margin-top: auto;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--t-sm);
    font-weight: 600;
    color: var(--petroleo-300);
}
.rel__card-cta .icono-material { font-size: 16px; }

.rel__flecha {
    margin-left: auto;
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border: 1px solid var(--borde);
    border-radius: var(--r-full);
    transition: border-color var(--dur-rapido) var(--ease),
                transform var(--dur-rapido) var(--ease);
}
.rel__card:hover .rel__flecha {
    border-color: var(--petroleo-400);
    transform: translateX(3px);
}

.rel__nav {
    display: flex;
    gap: var(--sp-3);
    margin-top: var(--sp-6);
}
.rel__nav-btn {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    background: none;
    border: 1px solid var(--borde);
    border-radius: var(--r-full);
    color: var(--gris);
    cursor: pointer;
    transition: border-color var(--dur-rapido) var(--ease),
                color var(--dur-rapido) var(--ease);
}
.rel__nav-btn:hover:not(:disabled) {
    border-color: var(--petroleo-400);
    color: var(--petroleo-300);
}
.rel__nav-btn:disabled { opacity: 0.35; cursor: not-allowed; }

@media (prefers-reduced-motion: reduce) {
    .rel__pista { scroll-behavior: auto; }
    .rel__card:hover { transform: none; }
    .rel__card:hover .rel__flecha { transform: none; }
}

/* ------------------------------------------------------------
   Aviso flotante ("Email copiado") — ver assets/js/sitio.js.

   Mismos nombres de clase y misma mecánica que el toast del panel
   (admin.css): son dos hojas separadas y no comparten código, pero que
   se llamen igual evita que el día de mañana alguien toque uno creyendo
   que toca los dos, o invente un tercer nombre para lo mismo.

   pointer-events: none en el contenedor — es un aviso, no algo con lo
   que se interactúe, y si tapara un click sobre el pie sería peor el
   remedio que la enfermedad.

   La animación va con transform/opacity (no con top/height): son las
   dos propiedades que el navegador compone en la GPU sin repintar, el
   mismo criterio que el latido del punto de disponibilidad.

   prefers-reduced-motion ya lo neutraliza vía la regla global de
   identidad.css.
   ------------------------------------------------------------ */
.toasts {
    position: fixed;
    /* Arriba y al centro, justo por debajo de la cabecera.
       Los 65px son el alto real de .cabecera (medido, y es el mismo en
       mobile que en escritorio: lo fija el logo + su padding). Si algún
       día cambia ese alto, este numero va con él o el aviso queda
       montado sobre la barra.

       left/right en 0 + align-items:center centra sin usar
       translateX: el transform queda libre para la animación de
       entrada del propio aviso, que si no se pisarían entre sí. */
    top: calc(65px + var(--sp-4));
    left: 0;
    right: 0;
    z-index: 200;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-2);
    pointer-events: none;
}
.toast {
    padding: var(--sp-4) var(--sp-5);
    border-radius: var(--r-sm);
    background: var(--superficie);
    border: 1px solid var(--exito);
    box-shadow: var(--sombra-md);
    font-size: var(--t-sm);
    color: var(--hueso);
    opacity: 0;
    transform: translateY(-10px);
    transition: opacity var(--dur-medio) var(--ease),
                transform var(--dur-medio) var(--ease);
}
.toast.is-visible {
    opacity: 1;
    transform: translateY(0);
}

/* En pantallas chicas el aviso no puede quedar más ancho que la
   pantalla: se le deja un margen a cada lado y el texto centrado por si
   llega a partirse en dos líneas. */
@media (max-width: 520px) {
    .toasts { padding-inline: var(--sp-4); }
    .toast { text-align: center; }
}

/* El aviso de respaldo (cuando el portapapeles falla) muestra la
   dirección para copiarla a mano, así que ese sí tiene que poder
   recibir el mouse — el contenedor lo desactiva para todos. */
.toast--seleccionable {
    pointer-events: auto;
    user-select: all;
    cursor: text;
}

/* ------------------------------------------------------------
   Link que cubre la pantalla del mockup (teléfono y laptop).

   Es una capa transparente encima de las diapositivas, no un <a>
   envolviéndolas: el disolver WebGL del slider lee esos <img> para
   armar sus texturas y meterle un elemento en el medio a esa
   estructura lo rompe.

   z-index 2: por encima de las diapositivas (que se apilan con
   position:absolute) pero por debajo de nada más — dentro de la
   pantalla no hay otra cosa que competir.
   ------------------------------------------------------------ */
.hero__slide-link {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: block;
    /* Sin fondo: el mockup se ve igual. El cursor es la única señal
       de que se puede clickear, igual que en las cards de la grilla. */
    border-radius: inherit;
}

/* El foco tiene que verse SÍ o SÍ: es un link sin texto visible, así
   que sin esto quien navega por teclado no tiene forma de saber dónde
   está parado. */
.hero__slide-link:focus-visible {
    outline: 2px solid var(--petroleo-300);
    outline-offset: -4px;
}

/* ------------------------------------------------------------
   Contacto directo en el hero (mail + LinkedIn), debajo de los
   logos de herramientas.

   flex-wrap es todo el mecanismo del "uno al lado del otro, y si no
   entran, uno abajo del otro": no hace falta un breakpoint. El mail
   va primero en el HTML, así que cuando envuelve queda arriba y
   LinkedIn abajo, que es el orden pedido. Un media query acá sería
   peor: el punto donde dejan de entrar depende del largo de la
   dirección, no del ancho de la pantalla.
   ------------------------------------------------------------ */
.hero__contacto {
    list-style: none;
    margin: var(--sp-4) 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2) var(--sp-5);
}

.hero__contacto-link {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--t-sm);
    color: var(--hueso-dim);
    text-decoration: none;
    /* El área de click llega a 44px de alto contando este padding:
       es el mínimo cómodo para el dedo en mobile, donde estos dos
       links quedan uno abajo del otro y pegados. */
    padding-block: var(--sp-2);
}

/* Sin subrayado: el aviso de hover es un cambio de color, igual que
   los links del pie. El subrayado además daba problema acá — con el
   <a> en flex, la línea pasaba por debajo del SVG de LinkedIn y
   quedaba un rayón suelto al costado del logo.
   La transición de color la hereda de la regla base de <a> en
   identidad.css, no hace falta repetirla. */
.hero__contacto-link:hover,
.hero__contacto-link:focus-visible {
    color: var(--petroleo-300);
}

.hero__contacto-link svg,
.hero__contacto-link .icono-material {
    width: 1em;
    height: 1em;
    font-size: 1.15em;
    flex: none;
    color: inherit;
}

/* El SVG de LinkedIn trae fill="#0a66c2" (el azul de la marca) como
   atributo de presentación dentro del propio archivo, así que
   color:inherit no lo toca: hay que pisar el fill.
   Ojo: esto va sólo acá, en el hero, porque el hero es oscuro y el
   azul de marca se pierde contra el fondo. En el pie el logo sigue
   con su color de marca — no mover esta regla a un selector más
   general. */
.hero__contacto-link svg,
.hero__contacto-link svg path {
    fill: currentColor;
}

/* ============================================================
   Visor de imágenes de los casos — ver assets/js/lightbox.js

   El overlay se monta al final del <body> (lo crea el JS), así que no
   hereda nada del caso: los colores van explícitos.
   ============================================================ */

/* Bloquea el scroll del fondo mientras el visor está abierto. Sin
   esto, la rueda del mouse mueve el caso por detrás de la imagen.
   overflow en <html> y no sólo en <body> porque en iOS el que
   scrollea es el documento. */
.js-sin-scroll,
.js-sin-scroll body { overflow: hidden; }

/* La imagen ampliable avisa que se puede tocar. El outline del foco no
   se toca: es un control más y tiene que verse al tabular. */
.js-ampliable {
    cursor: zoom-in;
}

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 300;   /* por encima de la cabecera (50) y de los toasts (200) */
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-4);
    padding: var(--sp-6);
    background: rgba(10, 10, 10, 0.92);
    /* opacity y no display: el display no se puede animar. El [hidden]
       del JS es el que lo saca del árbol cuando termina. */
    opacity: 0;
    transition: opacity var(--dur-medio) var(--ease);
}
.lightbox.is-visible { opacity: 1; }
.lightbox[hidden] { display: none; }

.lightbox__figura {
    margin: 0;
    /* min-width:0 — sin esto, un ítem flex no baja de su tamaño
       intrínseco y una imagen ancha empuja las flechas fuera de
       pantalla en vez de achicarse. */
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-4);
}

.lightbox__img {
    display: block;
    max-width: 100%;
    /* El alto se acota contra la pantalla, no contra el contenido: una
       captura vertical larga tiene que entrar entera, no obligar a
       scrollear adentro del visor. */
    max-height: calc(100vh - var(--sp-22));
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--r-xs);
}

.lightbox__pie {
    max-width: 70ch;
    color: var(--hueso-dim);
    font-size: var(--t-sm);
    line-height: var(--lh-body);
    text-align: center;
}
.lightbox__pie[hidden] { display: none; }

.lightbox__cerrar,
.lightbox__nav {
    flex: none;
    display: grid;
    place-items: center;
    width: 44px;          /* 44px: el mínimo cómodo para el dedo */
    height: 44px;
    padding: 0;
    color: var(--hueso);
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid var(--borde);
    border-radius: 50%;
    cursor: pointer;
    transition: background var(--dur-rapido) var(--ease),
                border-color var(--dur-rapido) var(--ease);
}
.lightbox__cerrar:hover,
.lightbox__nav:hover {
    background: rgba(255, 255, 255, 0.16);
    border-color: var(--petroleo-400);
}
.lightbox__nav[hidden],
.lightbox__contador[hidden] { display: none; }

.lightbox__cerrar {
    position: absolute;
    top: var(--sp-5);
    right: var(--sp-5);
    font-size: 24px;
    line-height: 1;
}

.lightbox__contador {
    position: absolute;
    bottom: var(--sp-5);
    left: 50%;
    transform: translateX(-50%);
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--t-xs);
    color: var(--gris);
}

/* En pantallas chicas las flechas al costado le comen el ancho a la
   imagen, que es lo único que importa acá: se van abajo, sobre el
   fondo, donde además caen cerca del pulgar. */
@media (max-width: 720px) {
    .lightbox { padding: var(--sp-4); }
    .lightbox__nav {
        position: absolute;
        bottom: var(--sp-6);
    }
    .lightbox__nav--prev { left: var(--sp-6); }
    .lightbox__nav--next { right: var(--sp-6); }
    .lightbox__contador { bottom: calc(var(--sp-6) + 12px); }
    .lightbox__img { max-height: calc(100vh - var(--sp-22) - var(--sp-8)); }
}
