/* =====================================================================
   ÍNDICE DEL ARCHIVO (para navegar rápido con Ctrl+F)
   1.  Variables globales (design tokens)
   2.  Reset & base (mobile-first)
   3.  Fondo animado global (dot-grid + blobs) — movimiento sutil
   4.  Utilidades / tipografía / botones
   5.  Navegación
   6.  Hero
   7.  Servicios
   8.  Portfolio
   9.  Por qué elegirme
   10. CTA + Formulario (incluye overlay de carga de Cal.com)
   11. Footer
   12. Luz resplandeciente que sigue al cursor
   13. Animaciones de entrada (reveal)
   14. Botón flotante de WhatsApp
   15. Selector de idioma (Google Translate)
   16. Breakpoints (tablet 768px+ / desktop 1024px+ / xl 1280px+)
   17. Lenguaje de movimiento compartido (view transitions entre páginas,
       barra de progreso de scroll, botones: sheen/press/magnético)
   18. Banner / panel de consentimiento de cookies (consent.js)
   ===================================================================== */

/* =====================================================================
   1. VARIABLES GLOBALES
   ===================================================================== */
:root{
    --bg-black: #0a0a0c;
    --bg-black-2: #050506;
    --bg-card: #121216;
    --border-soft: rgba(255,255,255,0.08);
    --border-soft-hover: rgba(255,255,255,0.16);

    --white: #f7f7f9;
    --gray-soft: rgba(247,247,249,0.65);
    --gray-mid: rgba(247,247,249,0.4);

    /* Tipografías. Se evitan a propósito las que aparecen por defecto en
       casi toda web generada con IA (Inter, Manrope, JetBrains Mono, Space
       Grotesk...): la base es Schibsted Grotesk (grotesca editorial, con
       más carácter en los pesos altos de los títulos) y el acento mono es
       Azeret Mono (más fina y geométrica, refuerza el perfil "developer"
       en etiquetas, números de servicio/métricas y tags de portfolio). */
    --font-sans: 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
    --font-mono: 'Azeret Mono', 'SFMono-Regular', Consolas, monospace;

    /* Azul como color de acento principal (reemplaza el coral) */
    --accent: #2f6fed;
    --accent-hot: #5b8dff;
    --accent-dark: #13224f;
    --accent-glow: rgba(47,111,237,0.35);

    /* Segundo tono, usado en el canvas de ondas para dar profundidad */
    --wave-teal: #1fc9c3;

    --radius-lg: 20px;
    --radius-md: 14px;
    --radius-sm: 8px;

    --container: 1180px;

    /* Paddings fluidos: chicos en mobile, crecen en desktop */
    --section-pad: 78px;
    --container-pad: 20px;

    /* Amplitud del recorrido de los blobs "bailando" (ver sección 3):
       un solo lugar para ajustar qué tanto se mueven en todos lados
       donde se usan. clamp() evita que en pantallas muy anchas el
       recorrido en vw se dispare y termine sacando el blob 100% del
       área visible; el piso en px evita que en mobile el movimiento
       se sienta demasiado chico. */
    --blob-dance-x: clamp(120px, 22vw, 300px);
    --blob-dance-y: clamp(80px, 16vh, 190px);

    /* Curvas de movimiento del sitio, en un solo lugar:
       --ease-out-expo: arranca rápido y frena de golpe ("cae en su
         lugar con peso") — entradas/reveals.
       --ease-smooth: frenado suave y parejo — hovers y micro-interacciones
         que no deben sentirse bruscas. */
    --ease-out-expo: cubic-bezier(.16,1,.3,1);
    --ease-smooth: cubic-bezier(.22,1,.36,1);
}

/* =====================================================================
   2. RESET & BASE (mobile-first)
   ===================================================================== */
*{margin:0;padding:0;box-sizing:border-box;}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%;overflow-x:hidden;width:100%;}
body{
    font-family:var(--font-sans);
    background:var(--bg-black);
    color:var(--white);
    overflow-x:hidden;      /* evita el scroll horizontal que rompía el mobile */
    position:relative;
    width:100%;
    min-height:100vh;
}

img{max-width:100%;display:block;}
a{text-decoration:none;color:inherit;transition:.3s ease;}
ul{list-style:none;}
button{font-family:inherit;}

h1,h2,h3,h4{
    font-weight:800;
    line-height:1.18;
    letter-spacing:-0.02em;
}
p{color:var(--gray-soft);line-height:1.75;}

/* Contenedor: ancho fluido con padding lateral consistente */
.container{
    width:100%;
    max-width:var(--container);
    margin:0 auto;
    padding:0 var(--container-pad);
}

.section-pad{padding:var(--section-pad) 0;}

.text-accent{color:var(--accent-hot);}

/* =====================================================================
   3. FONDO ANIMADO GLOBAL — movimiento sutil a lo largo de toda la web
   ===================================================================== */

/* Capa fija de puntos que se desplaza muy lentamente (todo el documento) */
.global-bg{
    position:fixed;
    inset:0;
    z-index:0;
    pointer-events:none;
    opacity:.5;
    background-image:radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1px);
    background-size:26px 26px;
    animation:driftGrid 40s linear infinite;
}
@keyframes driftGrid{
    0%{background-position:0 0;}
    100%{background-position:130px 130px;}
}

/* Blobs de color flotantes, reutilizables en cualquier sección */
.blob{
    position:absolute;
    border-radius:50%;
    filter:blur(70px);
    pointer-events:none;
    z-index:0;
    will-change:transform;
}
.blob-blue{background:var(--accent);opacity:.22;}
.blob-teal{background:var(--wave-teal);opacity:.16;}

/* Único movimiento de blobs del sitio: recorre un área mucho más amplia del fondo que
   floatSlow/floatSlowReverse (que solo se mueven unos px en el lugar).
   Se anima "transform" en vez de "top/left" a propósito: el navegador
   puede correrla en el compositor (GPU), sin recalcular layout en cada
   frame — importante porque estos blobs ya son costosos de pintar por
   el blur. Reutilizable en cualquier sección que quiera este efecto.
   El recorrido oscila para los DOS lados (no solo se aleja en una
   dirección y vuelve): los blobs suelen posicionarse ya pegados a un
   borde (offsets negativos, ver los "style" inline de cada uso), así
   que un desplazamiento que solo sigue alejándose terminaba sacando el
   blob 100% del área visible en algún punto del loop. Oscilando para
   ambos lados, siempre queda al menos una porción visible. La amplitud
   sale de --blob-dance-x/y (ver sección 1, con clamp() para que no se
   dispare en pantallas anchas): un solo lugar para ajustar qué tanto
   "sale" del diseño en todos lados donde se usa este efecto. */
@keyframes blobDanceA{
    0%{transform:translate(0,0);}
    25%{transform:translate(calc(var(--blob-dance-x) * -0.7), calc(var(--blob-dance-y) * 0.85));}
    50%{transform:translate(var(--blob-dance-x), calc(var(--blob-dance-y) * -0.8));}
    75%{transform:translate(calc(var(--blob-dance-x) * -0.55), var(--blob-dance-y));}
    100%{transform:translate(0,0);}
}
@keyframes blobDanceB{
    0%{transform:translate(0,0);}
    25%{transform:translate(calc(var(--blob-dance-x) * 0.7), calc(var(--blob-dance-y) * -0.85));}
    50%{transform:translate(calc(var(--blob-dance-x) * -1), calc(var(--blob-dance-y) * 0.8));}
    75%{transform:translate(calc(var(--blob-dance-x) * 0.55), calc(var(--blob-dance-y) * -1));}
    100%{transform:translate(0,0);}
}
.blob-dance-a{animation:blobDanceA 34s ease-in-out infinite;}
.blob-dance-b{animation:blobDanceB 40s ease-in-out infinite;}

/* Asegura que el contenido de cada sección quede sobre los blobs */
/* IMPORTANTE — bug de taps fantasma en mobile (filtros de portfolio, etc.):
   los blobs decorativos se posicionan a propósito con offsets negativos
   (ej. right:-100px) para que "sangren" fuera del viewport, y agrandan el
   scrollWidth de la sección más allá de su clientWidth. Con overflow-x:hidden
   la sección sigue siendo técnicamente un contenedor scrolleable (solo se
   bloquea el scroll por gesto/barra, no que sea "scrollable"): en iOS/Android,
   si el dedo se mueve unos px en horizontal al tocar un botón, el navegador
   puede interpretarlo como un intento de pan horizontal y cancela el click.
   overflow-x:clip elimina el scrollport por completo (inmune a esto) y se
   declara DESPUÉS como mejora progresiva: los navegadores que no soportan
   "clip" ignoran la línea y se quedan con "hidden" de fallback. touch-action
   refuerza lo mismo a nivel de gestos táctiles. */
section{position:relative;isolation:isolate;overflow-x:hidden;overflow-x:clip;touch-action:pan-y;}
section > .container{position:relative;z-index:2;}

/* Grano cinematográfico muy sutil sobre toda la web: aporta textura y
   profundidad al fondo oscuro sin afectar contraste ni legibilidad.
   z-index:2 lo ubica por encima del contenido de las secciones (que
   viven en su propio stacking context vía isolation:isolate) pero por
   debajo del header (z-index:1300) y de los widgets flotantes. */
.grain-overlay{
    position:fixed;
    inset:0;
    z-index:2;
    pointer-events:none;
    opacity:.035;
    mix-blend-mode:overlay;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
    background-size:180px 180px;
}

/* =====================================================================
   4. TIPOGRAFÍA / EYEBROW / BOTONES
   ===================================================================== */
.eyebrow{
    display:inline-flex;
    align-items:center;
    gap:8px;
    color:var(--accent-hot);
    font-family:var(--font-mono);
    font-weight:600;
    font-size:.76rem;
    text-transform:uppercase;
    letter-spacing:1.5px;
    margin-bottom:14px;
}

.section-header{max-width:640px;margin-bottom:46px;}
.section-header.center{margin-left:auto;margin-right:auto;text-align:center;}
.section-header h2{font-size:clamp(1.7rem,6vw,2.85rem);margin-bottom:16px;position:relative;display:inline-block;}
.section-header p{font-size:1rem;}

/* Trazo de acento bajo el titulo de cada seccion: un detalle editorial que
   ancla visualmente el heading sin agregar otro elemento al layout */
.section-header h2::after{
    content:'';
    display:block;
    width:46px;
    height:3px;
    margin-top:14px;
    border-radius:3px;
    background:linear-gradient(90deg,var(--accent),var(--wave-teal));
}

.btn{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    gap:10px;
    width:100%;
    padding:16px 28px;
    border-radius:50px;
    font-weight:700;
    font-size:.92rem;
    cursor:pointer;
    border:1px solid transparent;
    white-space:nowrap;
    transition:.35s cubic-bezier(.4,0,.2,1);
}
/* Los <button class="btn"> (p.ej. "Agendar reunión", que dispara el popup de
   Cal.com) deben verse idénticos en tamaño a los <a class="btn">: sin este
   reset, el estilo nativo del navegador para <button> (appearance + el
   borde interno oculto de Firefox) los renderiza levemente más grandes que
   un link con el mismo padding, aunque compartan exactamente las mismas
   reglas de arriba. */
button.btn{appearance:none;-webkit-appearance:none;-moz-appearance:none;}
button.btn::-moz-focus-inner{border:0;padding:0;}
/* Hover de los botones: el botón NO se mueve. En su lugar se enciende
   desde adentro (filo de luz superior + sombra que se asienta) y aparece
   un halo fino a su alrededor, como un anillo de foco. Todo es
   box-shadow con la misma cantidad de capas en reposo y en hover, así la
   transición interpola limpio en vez de saltar. El reflejo de una pasada
   (::after, abajo) completa el gesto. */
.btn-primary{
    background:linear-gradient(135deg,var(--accent) 0%,var(--accent-hot) 100%);
    color:#fff;
    box-shadow:
        0 0 0 0 rgba(91,141,255,0),
        0 10px 28px -8px var(--accent-glow),
        inset 0 1px 0 rgba(255,255,255,.14);
}
.btn-primary:hover{
    box-shadow:
        0 0 0 4px rgba(91,141,255,.16),
        0 12px 30px -10px rgba(47,111,237,.65),
        inset 0 1px 0 rgba(255,255,255,.32);
}
/* Feedback táctil al presionar: un "tick" de escala muy corto. Se declara
   con transición propia más rápida que la del hover para que responda al
   instante al click y no arrastre los .35s del resto. */
.btn:active{transform:translateY(0) scale(.97);transition-duration:.12s;}
/* Reflejo que recorre el botón primario al pasar el mouse: una franja
   de luz que cruza de izquierda a derecha una sola vez por hover (no en
   loop — el loop queda reservado a los .btn-glow/.btn-shine-border para
   no llenar la web de brillos compitiendo). Sólo transform+opacity, se
   compone en GPU. */
.btn-primary{position:relative;overflow:hidden;}
.btn-primary::after{
    content:'';
    position:absolute;
    top:0;bottom:0;
    left:0;
    width:60%;
    background:linear-gradient(105deg, transparent 0%, rgba(255,255,255,.22) 50%, transparent 100%);
    transform:translateX(-160%) skewX(-12deg);
    opacity:0;
    pointer-events:none;
}
@media (hover:hover){
    .btn-primary:hover::after{
        opacity:1;
        transform:translateX(270%) skewX(-12deg);
        transition:transform .85s var(--ease-smooth), opacity .2s ease;
    }
}
.btn-outline{
    background:transparent;
    color:var(--white);
    border-color:var(--border-soft);
    box-shadow:0 0 0 0 rgba(91,141,255,0);
}
.btn-outline:hover{
    background:rgba(47,111,237,.08);
    border-color:var(--accent-hot);
    color:var(--accent-hot);
    box-shadow:0 0 0 4px rgba(91,141,255,.12);
}
.btn-sm{width:auto;padding:11px 22px;font-size:.82rem;}

/* =====================================================================
   5. NAVEGACIÓN (mobile-first: menú full-width desplegable)
   ===================================================================== */
header{
    position:fixed;
    top:0;left:0;right:0;
    z-index:1300;
    border-bottom:1px solid var(--border-soft);
    transition:border-color .3s ease, box-shadow .3s ease;
}
/* Al bajar el scroll, el header suma sombra y refuerza el borde: distingue
   visualmente "flotando sobre el contenido" de su estado inicial en el hero */
header.scrolled{
    border-bottom-color:var(--border-soft-hover);
    box-shadow:0 12px 30px -18px rgba(0,0,0,.6);
}
/* Barra de progreso de lectura pegada al borde inferior del header:
   scaleX de 0 a 1 según cuánto se bajó la página. Scroll-driven animation
   nativa (corre en el compositor, sin JS); los navegadores sin
   animation-timeline caen en motion.js, que escribe --scroll-progress. */
.scroll-progress{
    position:absolute;
    left:0;right:0;
    bottom:-1px;
    height:2px;
    background:linear-gradient(90deg,var(--accent),var(--wave-teal));
    transform-origin:left;
    transform:scaleX(var(--scroll-progress,0));
    pointer-events:none;
}
@supports (animation-timeline: scroll()){
    .scroll-progress{
        transform:scaleX(0);
        animation:scrollProgress linear both;
        animation-timeline:scroll(root);
    }
}
@keyframes scrollProgress{from{transform:scaleX(0);}to{transform:scaleX(1);}}
/* El blur/fondo va en un pseudo-elemento, no en el header directamente.
   Motivo: `backdrop-filter` en un elemento lo convierte en el "contenedor de
   referencia" de sus hijos con position:fixed (spec CSS). Como el menú mobile
   (.nav-links) es fixed y vive dentro del header, si el header tuviera el
   backdrop-filter, el menú se posicionaría relativo a la altura del header
   (~68px) en vez de a toda la pantalla, quedando cortado/aplastado. */
header::before{
    content:'';
    position:absolute;
    inset:0;
    z-index:-1;
    background:rgba(10,10,12,0.85);
    backdrop-filter:blur(14px);
}
.nav-wrap{
    position:relative;
    display:flex;
    align-items:center;
    justify-content:space-between;
    height:68px;
    transition:height .45s var(--ease-smooth);
}
.logo{
    display:flex;
    align-items:center;
    gap:9px;
    font-size:1.1rem;
    font-weight:700;
    color:var(--white);
    margin-left:6px;
}
.logo-icon{
    height:38px;
    width:auto;
    flex-shrink:0;
    display:block;
}

.hamburger{
    display:flex;
    flex-direction:column;
    gap:5px;
    cursor:pointer;
    padding:8px;
    margin:-8px;
    z-index:1100;
}
.hamburger span{
    width:22px;height:2px;
    background:var(--white);
    border-radius:2px;
    transition:.3s ease;
}
.hamburger.active span:nth-child(1){transform:rotate(45deg) translate(5px,5px);}
.hamburger.active span:nth-child(2){opacity:0;}
.hamburger.active span:nth-child(3){transform:rotate(-45deg) translate(5px,-5px);}

.nav-links{
    position:fixed;
    top:68px;left:0;right:0;
    bottom:0;
    background:rgba(8,8,10,0.98);
    backdrop-filter:blur(16px);
    display:flex;
    flex-direction:column;
    align-items:stretch;
    gap:0;
    padding:12px 20px 32px;
    overflow-y:auto;
    /* Estado cerrado: invisible, ligeramente desplazado hacia arriba,
       y sin interceptar clics (pointer-events:none) */
    opacity:0;
    visibility:hidden;
    transform:translateY(-14px);
    pointer-events:none;
    transition:opacity .32s ease, transform .32s cubic-bezier(.4,0,.2,1), visibility 0s linear .32s;
}
.nav-links.active{
    opacity:1;
    visibility:visible;
    transform:translateY(0);
    pointer-events:auto;
    transition:opacity .32s ease, transform .32s cubic-bezier(.4,0,.2,1), visibility 0s linear 0s;
}
.nav-links a:not(.btn){
    font-weight:600;
    font-size:1.05rem;
    color:var(--gray-soft);
    padding:16px 4px;
    border-bottom:1px solid var(--border-soft);
}
.nav-links a.active,
.nav-links a:hover{color:var(--white);}
.nav-links a.active{color:var(--accent-hot);}
/* Los links del menú mobile entran en cascada al abrir (fade + leve
   desplazamiento desde la izquierda). En desktop (768px+) el nav es
   inline y siempre visible, así que ahí se anula (ver breakpoints). */
.nav-links > *{
    opacity:0;
    transform:translateX(-10px);
    transition:opacity .4s ease, transform .5s var(--ease-out-expo), color .3s ease, border-color .3s ease, background .3s ease, box-shadow .3s ease;
}
.nav-links.active > *{opacity:1;transform:none;}
.nav-links.active > *:nth-child(1){transition-delay:.06s;}
.nav-links.active > *:nth-child(2){transition-delay:.11s;}
.nav-links.active > *:nth-child(3){transition-delay:.16s;}
.nav-links.active > *:nth-child(4){transition-delay:.21s;}
.nav-links.active > *:nth-child(5){transition-delay:.26s;}
.nav-links.active > *:nth-child(6){transition-delay:.31s;}
.nav-links.active > *:nth-child(7){transition-delay:.36s;}
.nav-links .btn{margin-top:20px;}
.nav-links .btn + .nav-link-shine{margin-top:20px;}
.nav-links .btn-primary{
    background:linear-gradient(135deg,#173d99 0%,var(--accent) 100%);
    box-shadow:0 10px 28px -8px rgba(23,61,153,.45);
}
.nav-links .btn-primary:hover{transform:translateY(-2px);box-shadow:0 16px 34px -6px rgba(23,61,153,.6);}

/* Link "Portfolio Animado": brillo sutil tipo "shine" en el texto, más
   un subrayado animado debajo (mismo recurso que
   ".porque .section-header h2::after", @keyframes porqueLineSweep, acá
   en versión fina "de navbar"). Ambos efectos se aplican al <span>
   interno (no al <a>) para que el subrayado quede del ancho del TEXTO:
   en el menú mobile el <a> se estira a todo el ancho del panel (ver
   ".nav-links{align-items:stretch}"), y un <span> siempre se achica a
   su contenido sin importar el ancho del link que lo contiene. */
.nav-links a.nav-link-shine span{position:relative;display:inline-block;}
@supports ((background-clip:text) or (-webkit-background-clip:text)){
    .nav-links a.nav-link-shine span{
        background-image:linear-gradient(100deg,
            var(--gray-soft) 0%, var(--gray-soft) 40%,
            #fff 48%, var(--accent-hot) 52%, #fff 56%,
            var(--gray-soft) 64%, var(--gray-soft) 100%);
        background-size:220% 100%;
        background-position:200% 0;
        -webkit-background-clip:text;
        background-clip:text;
        color:transparent;
        animation:navShine 6s ease-in-out infinite;
    }
    .nav-links a.nav-link-shine.active span{
        background-image:linear-gradient(100deg,
            var(--accent-hot) 0%, var(--accent-hot) 40%,
            #fff 48%, var(--accent-hot) 52%, #fff 56%,
            var(--accent-hot) 64%, var(--accent-hot) 100%);
    }
}
@keyframes navShine{
    0%,15%{background-position:200% 0;}
    55%,100%{background-position:-120% 0;}
}

.nav-links a.nav-link-shine span::after{
    content:'';
    position:absolute;
    left:0;
    right:0;
    bottom:-6px;
    height:2px;
    border-radius:2px;
    background-image:linear-gradient(90deg,
        var(--accent) 0%, var(--accent) 32%,
        var(--wave-teal) 46%, #eafffb 50%, var(--wave-teal) 54%,
        var(--accent) 68%, var(--accent) 100%);
    background-size:300% 100%;
    background-position:200% 0;
    animation:porqueLineSweep 4.2s cubic-bezier(.83,0,.17,1) infinite;
}
@media (prefers-reduced-motion: reduce){
    .nav-link-shine span{animation:none;}
    .nav-link-shine span::after{animation:none;}
}

/* Botón "Ver portfolio" del hero: mismo lenguaje visual que el link
   "Portfolio Animado" del navbar (arriba) — brillo animado en el texto
   (@keyframes navShine) y un barrido de gradiente en el borde
   (@keyframes porqueLineSweep, el mismo del subrayado del navbar y del
   acento de "por qué elegirme"), para que este CTA llame más la atención
   sin introducir un efecto nuevo. El borde animado se logra con un
   pseudo-elemento con "mask" (técnica de "gradient border" animado): el
   gradiente se pinta como fondo completo y el mask recorta todo menos un
   anillo del grosor del padding, dejando ver solo el borde. */
.btn-shine-border{position:relative;}
.btn-shine-border::before{
    content:'';
    position:absolute;
    inset:0;
    border-radius:inherit;
    padding:1px;
    background-image:linear-gradient(90deg,
        var(--accent) 0%, var(--accent) 32%,
        var(--wave-teal) 46%, #eafffb 50%, var(--wave-teal) 54%,
        var(--accent) 68%, var(--accent) 100%);
    background-size:300% 100%;
    animation:porqueLineSweep 4.2s cubic-bezier(.83,0,.17,1) infinite;
    -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite:xor;
    mask-composite:exclude;
    pointer-events:none;
}
.shine-text{position:relative;display:inline-block;}
@supports ((background-clip:text) or (-webkit-background-clip:text)){
    .shine-text{
        background-image:linear-gradient(100deg,
            var(--gray-soft) 0%, var(--gray-soft) 40%,
            #fff 48%, var(--accent-hot) 52%, #fff 56%,
            var(--gray-soft) 64%, var(--gray-soft) 100%);
        background-size:220% 100%;
        background-position:200% 0;
        -webkit-background-clip:text;
        background-clip:text;
        color:transparent;
        animation:navShine 6s ease-in-out infinite;
    }
}
@media (prefers-reduced-motion: reduce){
    .btn-shine-border::before{animation:none;}
    .shine-text{animation:none;}
}

/* =====================================================================
   15. SELECTOR DE IDIOMA (Google Translate)
   Widget real de Google queda oculto (#google_translate_element); este
   bloque es un dropdown propio que solo dispara el cambio de idioma via
   cookie "googtrans" (ver assets/js/lang-switch.js) y oculta el chrome
   por defecto que agrega Google (banner superior, tooltip, iframe).
   ===================================================================== */
.lang-switch{
    /* Mobile: centrado absoluto respecto al header, no al espacio libre
       entre logo y hamburger (que no son del mismo ancho y lo desviarian) */
    position:absolute;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);
}
.lang-switch-btn{
    display:flex;
    align-items:center;
    gap:5px;
    height:36px;
    padding:0 10px;
    background:transparent;
    border:1px solid var(--border-soft);
    border-radius:8px;
    color:var(--gray-soft);
    font-family:var(--font-mono);
    font-size:.85rem;
    letter-spacing:.03em;
    cursor:pointer;
    transition:border-color .2s ease, color .2s ease, background .2s ease;
}
.lang-switch-btn:hover,
.lang-switch.open .lang-switch-btn{
    border-color:var(--border-soft-hover);
    color:var(--white);
    background:rgba(255,255,255,0.04);
}
.lang-switch-btn svg{transition:transform .2s ease;flex-shrink:0;}
.lang-switch.open .lang-switch-btn svg{transform:rotate(180deg);}

.lang-switch-menu{
    position:absolute;
    top:calc(100% + 8px);
    right:0;
    min-width:150px;
    background:var(--bg-card);
    border:1px solid var(--border-soft);
    border-radius:10px;
    padding:6px;
    box-shadow:0 16px 34px -10px rgba(0,0,0,.55);
    opacity:0;
    visibility:hidden;
    transform:translateY(-6px);
    transition:opacity .18s ease, transform .18s ease, visibility 0s linear .18s;
    z-index:1200;
}
.lang-switch.open .lang-switch-menu{
    opacity:1;
    visibility:visible;
    transform:translateY(0);
    transition:opacity .18s ease, transform .18s ease;
}
.lang-switch-menu li + li{margin-top:2px;}
.lang-switch-menu button{
    display:block;
    width:100%;
    text-align:left;
    padding:8px 10px;
    background:transparent;
    border:none;
    border-radius:6px;
    color:var(--gray-soft);
    font-size:.92rem;
    cursor:pointer;
    transition:background .15s ease, color .15s ease;
}
.lang-switch-menu button:hover{background:rgba(255,255,255,0.06);color:var(--white);}
.lang-switch-menu button.active{color:var(--accent-hot);}

/* Chrome que agrega Google Translate por defecto: se oculta por completo,
   el dropdown propio de arriba es la unica UI visible para el usuario */
.goog-te-banner-frame,
.skiptranslate > iframe{display:none !important;}
body{top:0 !important;}
#google_translate_element{display:none !important;}
.goog-tooltip,
.goog-tooltip:hover{display:none !important;}
.goog-text-highlight{background:none !important;box-shadow:none !important;}

/* =====================================================================
   6. HERO
   ===================================================================== */
.hero{
    min-height:118svh;
    display:flex;
    flex-direction:column;
    justify-content:center;
    text-align:center;
    padding:110px 0 60px;
    overflow:hidden;
}
.hero-content{position:relative;z-index:5;max-width:820px;margin:0 auto;padding:0 var(--container-pad);}

/* Cada pieza del hero (título, subtítulo, CTAs) va envuelta en un
   .hero-exit-* que sólo existe para la salida por scroll (ver más abajo);
   la entrada de carga sigue viviendo en el elemento real de adentro
   (h1/.hero-subtitle/.hero-cta). Dos animaciones sobre las mismas
   propiedades del mismo elemento se pisarían (mismo motivo que en
   .hero-stats, comentario más abajo), así que entrada y salida quedan en
   elementos distintos en vez de intentar combinarlas en uno solo. */
.hero-exit-title, .hero-exit-subtitle, .hero-exit-cta{display:block;}

/* ---- Entrada del hero (coreografía de carga) ----
   Orquestación en 4 tiempos, todos con --ease-out-expo (frena de golpe:
   cada pieza "cae en su lugar" con peso):
     1. el titular entra palabra por palabra desde abajo de una máscara
        (.hw/.hw-i, armadas por script.js) con un leve tilt;
     2. el subtítulo se "enfoca" (blur + fade) en vez de sólo deslizar;
     3. los CTAs suben;
     4. la línea de las stats se dibuja desde el centro y cada stat entra
        escalonada. Blobs y partículas hacen su propio fade largo debajo.
   Sin JS el h1 usa el mismo fade-up genérico que sus hermanos (regla de
   abajo), así que nunca depende del split para verse. */
.hero h1, .hero-subtitle, .hero-cta{
    opacity:0;
    transform:translateY(28px);
    animation:heroFadeUp 1.1s var(--ease-out-expo) forwards;
}
.hero h1{animation-delay:.08s;}
.hero-subtitle{animation-delay:.55s;}
.hero-cta{animation-delay:.74s;}
@keyframes heroFadeUp{to{opacity:1;transform:none;}}

/* Subtítulo: entra desenfocado y se asienta. El blur se anima sólo acá
   (un párrafo, una vez, al cargar) — no en .reveal, donde animar filter
   en una grilla entera de cards sería caro en mobile. */
.hero-subtitle{
    transform:translateY(18px);
    filter:blur(10px);
    animation-name:heroBlurUp;
}
@keyframes heroBlurUp{to{opacity:1;transform:none;filter:blur(0);}}

/* Titular palabra por palabra. script.js envuelve cada palabra del h1 en
   <span class="hw"><span class="hw-i">palabra</span></span> y le pone
   .hero-split al h1 (anula el fade-up genérico). .hw es la máscara
   (overflow:hidden) y .hw-i el texto que sube desde abajo. El padding +
   margen negativo de .hw dan aire para que la máscara no recorte
   descendentes ("q", "j") ni acentos sin mover la línea de base: el
   layout queda idéntico al del texto plano. Al terminar, script.js
   restaura el innerHTML original del h1 (sin wrappers). */
.hero h1.hero-split{animation:none;opacity:1;transform:none;}
.hw{
    display:inline-block;
    overflow:hidden;
    vertical-align:top;
    padding:.06em .08em .16em;
    margin:-.06em -.08em -.16em;
}
.hw-i{
    display:inline-block;
    transform:translateY(112%) rotate(3deg);
    transform-origin:0 100%;
    animation:heroWordUp 1s var(--ease-out-expo) forwards;
    animation-delay:calc(.12s + var(--i,0) * 65ms);
}
@keyframes heroWordUp{to{transform:none;}}

/* Stats: la línea superior (::before, reemplaza al border-top para poder
   animarla) se dibuja desde el centro y cada stat entra escalonada. La
   entrada va en los hijos y NO en .hero-stats: ese elemento lleva la
   animación de salida por scroll (más abajo) y dos animaciones sobre las
   mismas propiedades en el mismo elemento se pisarían. */
.hero-stats::before{
    content:'';
    position:absolute;
    top:0;left:0;right:0;
    height:1px;
    background:linear-gradient(90deg,transparent,var(--border-soft-hover) 28%,var(--border-soft-hover) 72%,transparent);
    transform:scaleX(0);
    animation:heroLineDraw 1.3s var(--ease-out-expo) .95s forwards;
}
@keyframes heroLineDraw{to{transform:scaleX(1);}}
.hero-stats .stat{
    opacity:0;
    transform:translateY(16px);
    animation:heroFadeUp .9s var(--ease-out-expo) forwards;
}
.hero-stats .stat:nth-child(1){animation-delay:1.05s;}
.hero-stats .stat:nth-child(2){animation-delay:1.15s;}
.hero-stats .stat:nth-child(3){animation-delay:1.25s;}

/* Blobs y partículas del hero: aparecen por debajo de todo, más lento.
   Los blobs ya tienen su animación infinita (blobDanceA/B, sección 3),
   así que acá se re-declara la lista completa sumando el fade: el
   keyframe sólo define "from", el "to" es el valor computado (la
   opacidad propia de .blob-blue/.blob-teal). */
.hero .blob-dance-a{animation:blobDanceA 34s ease-in-out infinite, heroSoftIn 2.4s ease-out;}
.hero .blob-dance-b{animation:blobDanceB 40s ease-in-out infinite, heroSoftIn 2.4s ease-out;}
.particle-bg{animation:heroSoftIn 2s ease-out .5s both;}
@keyframes heroSoftIn{from{opacity:0;}}

/* ---- Salida del hero por scroll ----
   El contenido no "se va" con el scroll sin más: se disuelve, se
   desenfoca, se achica apenas y queda rezagado (parallax) mientras la
   sección siguiente lo pisa. Scroll-driven animation nativa: el progreso
   lo lleva el navegador en el compositor, sin JS, y es reversible.
   OJO: scroll(root), no scroll()/scroll(nearest): <body> tiene
   overflow-x:hidden (sección 2), lo que lo convierte en scroll container
   con rango 0 — "nearest" lo elegiría y la animación quedaría clavada en
   el frame 0. Los rangos van en svh porque la sección mide 118svh: el
   contenido debe haberse desvanecido antes de que su borde salga por
   arriba.
   Escalonado a propósito, en vez de un único bloque .hero-content: en
   mobile, con el primer scrollcito para asomar las stats (11 proyectos /
   9 marcas / 3+ años) el título ya empezaba a desvanecerse — se sentía
   prematuro. Ahora el arranque se retrasa (nada pasa antes de 14svh) y
   cada elemento tiene su propio tramo, título → subtítulo → botones →
   stats, así el fade se lee como una secuencia y no como un parpadeo
   simultáneo. Los tramos están anidados (cada uno empieza mientras el
   anterior todavía se está yendo) para que la salida completa no dure
   más que antes; el final de .hero-stats (66svh) es el mismo de siempre,
   así que el resto de la sección (118svh) sigue calzando igual.
   Navegadores sin animation-timeline caen en el fallback de script.js,
   que escribe --hero-exit-title/-subtitle/-cta/-stats (0→1) en .hero; si
   cambiás los rangos acá, cambiá las fracciones del JS también. */
@keyframes heroExit{
    0%{opacity:1;transform:none;filter:blur(0);}
    30%{opacity:.86;}
    100%{opacity:0;transform:translateY(80px) scale(.93);filter:blur(10px);}
}
@supports (animation-timeline: scroll()){
    .hero-exit-title{
        animation:heroExit linear both;
        animation-timeline:scroll(root);
        animation-range:14svh 40svh;
    }
    .hero-exit-subtitle{
        animation:heroExit linear both;
        animation-timeline:scroll(root);
        animation-range:20svh 46svh;
    }
    .hero-exit-cta{
        animation:heroExit linear both;
        animation-timeline:scroll(root);
        animation-range:26svh 52svh;
    }
    .hero-stats{
        animation:heroExit linear both;
        animation-timeline:scroll(root);
        animation-range:34svh 66svh;
    }
}
@supports not (animation-timeline: scroll()){
    .hero-exit-title{
        opacity:calc(1 - var(--hero-exit-title,0));
        transform:translateY(calc(var(--hero-exit-title,0) * 80px)) scale(calc(1 - var(--hero-exit-title,0) * .07));
        filter:blur(calc(var(--hero-exit-title,0) * 10px));
    }
    .hero-exit-subtitle{
        opacity:calc(1 - var(--hero-exit-subtitle,0));
        transform:translateY(calc(var(--hero-exit-subtitle,0) * 80px)) scale(calc(1 - var(--hero-exit-subtitle,0) * .07));
        filter:blur(calc(var(--hero-exit-subtitle,0) * 10px));
    }
    .hero-exit-cta{
        opacity:calc(1 - var(--hero-exit-cta,0));
        transform:translateY(calc(var(--hero-exit-cta,0) * 80px)) scale(calc(1 - var(--hero-exit-cta,0) * .07));
        filter:blur(calc(var(--hero-exit-cta,0) * 10px));
    }
    .hero-stats{
        opacity:calc(1 - var(--hero-exit-stats,0));
        transform:translateY(calc(var(--hero-exit-stats,0) * 80px)) scale(calc(1 - var(--hero-exit-stats,0) * .07));
        filter:blur(calc(var(--hero-exit-stats,0) * 10px));
    }
}

.hero h1{
    font-size:clamp(2rem,8vw,4.6rem);
    margin-bottom:20px;
}
.hero-subtitle{
    font-size:1.02rem;
    max-width:600px;
    margin:0 auto 34px;
}
.hero-cta{
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:14px;
    margin-bottom:60px;
}
.hero-stats{
    position:relative;
    z-index:5;
    display:grid;
    grid-template-columns:repeat(3,1fr);
    gap:16px;
    max-width:640px;
    margin:0 auto;
    padding:32px var(--container-pad) 0;
    /* La línea superior vive en ::before (ver entrada del hero más arriba) */
}
.stat{text-align:center;}
.stat-number{
    font-family:var(--font-mono);
    font-size:1.9rem;
    font-weight:700;
    background:linear-gradient(135deg,var(--white) 0%,var(--accent-hot) 100%);
    -webkit-background-clip:text;
    -webkit-text-fill-color:transparent;
    background-clip:text;
    margin-bottom:4px;
}
.stat-label{font-size:.7rem;color:var(--gray-mid);line-height:1.4;}

.particle-bg{
    position:absolute;
    left:0;right:0;bottom:0;
    width:100%;
    height:280px;
    pointer-events:none;
    z-index:0;
    /* Desvanece el patrón de puntos hacia los bordes (abajo y costados) para
       que no se vea un corte duro contra la sección siguiente */
    -webkit-mask-image:
        linear-gradient(to bottom, black 45%, transparent 100%),
        linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image:
        linear-gradient(to bottom, black 45%, transparent 100%),
        linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%);
    mask-composite: intersect;
}
.particle-bg canvas{width:100%;height:100%;display:block;}

/* =====================================================================
   7. SERVICIOS
   ===================================================================== */
.servicios{background:var(--bg-black);}
.servicios-grid{
    display:grid;
    grid-template-columns:1fr;
    gap:22px;
}
/* Alto de card FIJO (no min-height): la card nunca cambia de tamaño entre
   idle y hover. El párrafo también reserva su alto siempre (ver
   ".service-card p" más abajo) — lo único que cambia con el hover es la
   posición del ícono+título dentro de ese espacio fijo (centrados en
   idle, arriba en hover) y el texto del párrafo, tipeado por JS. Los tres
   valores de "height" (acá y en los dos breakpoints de abajo) están
   medidos a mano para que en NINGÚN card, a NINGÚN ancho, el párrafo más
   largo de la lista ("Backend & Bases de Datos") se corte — si se agrega
   un servicio nuevo con un texto más largo, volver a medir. */
/* Toda la interacción de esta card comparte UNA sola curva/duración base
   (--svc-ease/--svc-dur) para que las piezas se sientan como un solo
   movimiento fluido en vez de varias animaciones sueltas con timings
   distintos. cubic-bezier(.22,1,.36,1) es una curva de frenado suave
   ("ease-out" largo, sin arranque brusco) — más lenta y pareja que la
   cubic-bezier(.16,1,.3,1) que usa el resto del sitio (esa arranca muy
   rápido y frena de golpe, que es justo lo que se sentía "tosco" acá). */
.service-card{
    --svc-ease:cubic-bezier(.22,1,.36,1);
    --svc-dur:.65s;
    background:var(--bg-card);
    border:1px solid var(--border-soft);
    border-radius:var(--radius-lg);
    padding:28px;
    transition:border-color var(--svc-dur) var(--svc-ease), transform var(--svc-dur) var(--svc-ease), box-shadow var(--svc-dur) var(--svc-ease);
    position:relative;
    overflow:hidden;
    height:310px;
}
.service-card::before{
    content:'';
    position:absolute;
    inset:0;
    background:linear-gradient(135deg,rgba(47,111,237,0.08),transparent 60%);
    opacity:0;
    transition:opacity var(--svc-dur) var(--svc-ease);
}
.service-card.reveal:hover,
.service-card.reveal.is-open{
    border-color:rgba(47,111,237,0.4);
    transform:translateY(-4px);
    box-shadow:0 16px 36px -16px var(--accent-glow);
}
.service-card:hover::before,
.service-card.is-open::before{opacity:1;}

/* Ícono + título: se posicionan como bloque absoluto para poder centrarse
   en la card COMPLETA en idle (no solo en el espacio libre arriba del
   párrafo, que quedaba visualmente "para arriba") — top:50%+translateY(-50%)
   es el truco clásico de centrado y, al ser valores en %, sí admite
   transición (justify-content/align-items no). En hover baja a
   top:<padding> (mismo valor que el padding de la card, porque un hijo
   position:absolute NO hereda el padding del contenedor solo). El párrafo
   es OTRO absoluto independiente, siempre pegado abajo con alto fijo — así
   ninguno de los dos participa de un layout en común que pudiera empujar
   al otro, y la card nunca cambia de tamaño. Ícono y título mantienen su
   propio truco de left/translateX para pasar de centrados a alineados a
   la izquierda (mismo left:28px/36px del padding en hover). */
.service-head{
    position:absolute;
    top:50%;
    left:28px;
    right:28px;
    transform:translateY(-50%);
    transition:top var(--svc-dur) var(--svc-ease), transform var(--svc-dur) var(--svc-ease);
}
.service-card:hover .service-head,
.service-card.is-open .service-head{
    top:28px;
    transform:translateY(0%);
}
.service-icon,
.service-card h3{
    position:relative;
    left:50%;
    transform:translateX(-50%) scale(1);
    width:fit-content;
    transition:left var(--svc-dur) var(--svc-ease), transform var(--svc-dur) var(--svc-ease);
}
.service-card:hover h3,
.service-card.is-open h3{
    left:0%;
    transform:translateX(0%) scale(1);
}
/* OJO: el ícono necesita SU PROPIA transición completa (left+transform+
   background-color) en una sola declaración — si quedara una segunda
   regla ".service-icon{transition:...}" separada de la de arriba, al
   tener la misma especificidad la de más abajo GANA COMPLETA y pisa el
   "left" de la regla compartida, dejando al ícono saltando de golpe en
   horizontal mientras el título se desliza suave (bug real que causaba
   parte de lo "tosco" reportado). Por eso todo el ícono vive acá, no
   separado. */
.service-icon{
    width:50px;height:50px;
    border-radius:var(--radius-sm);
    background:rgba(47,111,237,0.12);
    display:flex;align-items:center;justify-content:center;
    margin-bottom:18px;
    color:var(--accent-hot);
    transition:left var(--svc-dur) var(--svc-ease), transform var(--svc-dur) var(--svc-ease), background-color var(--svc-dur) var(--svc-ease);
}
.service-card:hover .service-icon,
.service-card.is-open .service-icon{
    left:0%;
    background:rgba(47,111,237,0.22);
    transform:translateX(0%) scale(1.08);
}
.service-icon svg{width:24px;height:24px;}
.service-card h3{
    font-size:1.12rem;
    margin-bottom:0;
    padding-bottom:4px;
}
/* Subrayado que se "dibuja" bajo el título en hover — detalle chico que
   suma sensación de pulido sin competir con el movimiento principal del
   ícono/título. Ancla al propio ancho de h3 (fit-content), así que dibuja
   exactamente hasta donde termina el texto, no el de la card. Usa
   transform:scaleX en vez de animar "width" — scaleX corre en el
   compositor (GPU) y es mucho más fluido que animar un layout property
   como width, que es parte de por qué se sentía tosco. */
.service-card h3::after{
    content:'';
    position:absolute;
    left:0;
    bottom:0;
    width:100%;
    height:2px;
    background:linear-gradient(90deg,var(--accent),var(--wave-teal));
    transform:scaleX(0);
    transform-origin:left;
    transition:transform var(--svc-dur) var(--svc-ease) .06s;
}
.service-card:hover h3::after,
.service-card.is-open h3::after{transform:scaleX(1);}

/* Párrafo: absoluto, siempre pegado abajo, alto fijo reservado SIEMPRE
   (nunca se anima ni cambia el tamaño de la card, ver nota grande al
   inicio de ".service-card"). Revelación con fade + slide-up, arrancando
   un poco después que el ícono/título (transition-delay) para que se
   sienta como una cascada — primero se reacomoda el encabezado, después
   entra el texto — en vez de que todo se mueva a la vez. Sin JS de por
   medio: el texto real siempre está en el HTML, así que no hace falta
   ningún fallback para touch/no-JS/reduced-motion aparte del que ya da
   @media (hover:none) más abajo para la posición del header. */
.service-card p{
    position:absolute;
    left:28px;
    right:28px;
    bottom:28px;
    font-size:.9rem;
    height:130px;
    margin:0;
    overflow:hidden;
    opacity:0;
    transform:translateY(14px);
    transition:opacity var(--svc-dur) var(--svc-ease), transform var(--svc-dur) var(--svc-ease);
}
.service-card:hover p,
.service-card.is-open p{
    opacity:1;
    transform:translateY(0);
    transition-delay:.1s;
}
/* Debajo de 400px (iPhone SE y similares) una sola columna angosta hace
   que el párrafo más largo envuelva a 7 líneas en vez de 5 — sin este
   ajuste se recorta. Medido a mano igual que el resto de estos valores.
   (Va DESPUÉS de la regla base de arriba a propósito: misma especificidad,
   así que el orden en el archivo es lo que decide cuál gana.) */
@media (max-width:399px){
    .service-card{height:365px;}
    .service-card p{height:185px;}
}
/* Chevron "tocá para ver más" — solo visible en touch (hover:none, ver
   script.js sección TAP-TO-EXPAND). En desktop el hover ya hace evidente
   que hay más contenido con el propio movimiento del ícono/título, así
   que ahí el chevron no aparece. Gira 180° cuando la card está abierta,
   la misma señal universal de "expandible" que un acordeón. */
.service-chevron{
    display:none;
    position:absolute;
    top:16px;
    right:16px;
    width:28px;height:28px;
    border-radius:50%;
    background:rgba(47,111,237,0.12);
    align-items:center;
    justify-content:center;
    color:var(--accent-hot);
    z-index:1;
    transition:transform var(--svc-dur) var(--svc-ease), background-color var(--svc-dur) var(--svc-ease);
}
.service-chevron svg{width:14px;height:14px;}
.service-card.is-open .service-chevron{
    transform:rotate(180deg);
    background:rgba(47,111,237,0.22);
}
/* Touch (sin hover real): acá :hover de CSS nunca se activa, así que
   script.js agrega/saca la clase ".is-open" con el tap (ver TAP-TO-EXPAND
   en script.js) — reutiliza EXACTAMENTE las mismas reglas de arriba
   (".is-open" está agregado a cada selector ":hover"), nada especial acá
   salvo mostrar el chevron que indica que es tocable. */
@media (hover:none){
    .service-chevron{display:flex;}
}

@media (prefers-reduced-motion:reduce){
    .service-head, .service-icon, .service-card h3, .service-card h3::after, .service-card p, .service-card, .service-chevron{
        transition-duration:.01ms !important;
    }
}

/* Nota breve sobre identidad de marca, debajo de la grilla de servicios */
.servicios-note{
    position:relative;
    overflow:hidden;
    display:flex;
    align-items:flex-start;
    gap:12px;
    margin-top:32px;
    padding:18px 22px;
    background:rgba(47,111,237,0.06);
    border:1px solid rgba(47,111,237,0.18);
    border-radius:var(--radius-md);
    font-size:.88rem;
    color:var(--gray-soft);
    line-height:1.6;
}
.servicios-note svg{
    width:20px;height:20px;
    flex-shrink:0;
    color:var(--accent-hot);
    margin-top:2px;
}
/* Link a /como-trabajo debajo de la nota */
.servicios-process{margin-top:20px;}
.servicios-process a{
    display:inline-flex;
    align-items:center;
    gap:10px;
    font-size:.9rem;
    font-weight:600;
    color:var(--white);
}
.servicios-process a span{
    background:linear-gradient(90deg,var(--accent-hot),var(--wave-teal)) left bottom / 100% 1px no-repeat;
    padding-bottom:3px;
}
.servicios-process a svg{
    width:16px;height:16px;
    color:var(--accent-hot);
    transition:transform .45s var(--ease-smooth);
}
.servicios-process a:hover svg{transform:translateX(4px);}
/* Mismo reflejo/sheen que .portfolio-card::after (misma animación
   @keyframes cardSheen, ver sección 8): se reutiliza acá y en los botones
   "Ver portfolio"/"Ver portfolio animado" para que sea un único lenguaje
   visual reconocible en vez de un efecto por elemento. La animación sólo
   corre mientras el elemento está en pantalla (clase ".sheen-on", la
   agrega/saca un IntersectionObserver en script.js/sobre-mi.js) — así
   corre en cualquier dispositivo, mobile incluido, pero nunca de fondo
   fuera de vista. */
.servicios-note::after,
.btn-outline.btn-shine-border::after,
.btn-glow::after{
    content:'';
    position:absolute;
    inset:0;
    pointer-events:none;
    background:linear-gradient(115deg, transparent 0%, transparent 40%, rgba(255,255,255,.14) 50%, transparent 60%, transparent 100%);
    background-size:300% 300%;
    background-position:0% 0%;
    mix-blend-mode:screen;
}
.btn-outline.btn-shine-border,
.btn-glow{
    position:relative;
    overflow:hidden;
}
.servicios-note.sheen-on::after,
.btn-outline.btn-shine-border.sheen-on::after,
.btn-glow.sheen-on::after{animation:cardSheen 5s ease-in-out infinite alternate;}
@media (prefers-reduced-motion: reduce){
    .servicios-note::after, .servicios-note.sheen-on::after,
    .btn-outline.btn-shine-border::after, .btn-outline.btn-shine-border.sheen-on::after,
    .btn-glow::after, .btn-glow.sheen-on::after{animation:none !important;}
}

/* =====================================================================
   8. PORTFOLIO
   ===================================================================== */
.portfolio{background:var(--bg-black-2);}
.portfolio-header-row{
    display:flex;
    flex-direction:column;
    gap:20px;
    margin-bottom:36px;
}
/* Agrupa el CTA al portfolio animado justo arriba de los filtros de
   categoría, alineados ambos al mismo lado (ver breakpoint 768px+). */
.portfolio-filters-col{
    display:flex;
    flex-direction:column;
    align-items:flex-start;
    gap:14px;
}
/* En mobile el CTA "Ver portfolio animado" queda centrado y con más aire
   abajo para que tenga su propio protagonismo, en vez de mezclarse pegado
   contra los chips de categoría de abajo (ver reset en el breakpoint
   768px+, donde vuelve a alinearse con los filtros como en desktop). */
.portfolio-filters-col .btn-glow{
    align-self:center;
    margin-bottom:10px;
}
.portfolio-filters{
    display:flex;
    gap:8px;
    flex-wrap:wrap;
}
.filter-btn{
    padding:9px 18px;
    border-radius:50px;
    border:1px solid var(--border-soft);
    background:transparent;
    color:var(--gray-soft);
    font-family:var(--font-sans);
    font-size:.8rem;
    font-weight:600;
    cursor:pointer;
    transition:.3s ease;
}
.filter-btn:hover{border-color:var(--accent-hot);color:var(--white);}
.filter-btn:active{transform:scale(.95);}
.filter-btn.active{
    background:var(--accent);
    border-color:var(--accent);
    color:#fff;
}

.portfolio-grid{
    display:grid;
    grid-template-columns:1fr;
    gap:24px;
}
.portfolio-card{
    position:relative;
    border-radius:var(--radius-lg);
    overflow:hidden;
    /* Ojo si se agregan más tags o descripciones más largas: como .pf-image
       y .pf-overlay son ambos position:absolute (ver más abajo), NINGÚN
       hijo está en flujo normal, así que esta altura NO crece sola con el
       contenido — hay que mantenerla mayor o igual a la del proyecto con
       más texto/tags (hoy, Chiron Planet con 7 tags) o el contenido se
       corta arriba en vez de desbordar visiblemente. */
    min-height:380px;
    display:flex;
    cursor:pointer;
    background:linear-gradient(160deg,#1a1a20 0%,#0c0c0f 100%);
    border:1px solid var(--border-soft);
    transition:border-color .3s ease, box-shadow .3s ease, transform .5s cubic-bezier(.4,0,.2,1);
}
.portfolio-card:hover{
    border-color:rgba(47,111,237,0.4);
    box-shadow:0 30px 60px -24px rgba(47,111,237,.35);
}
/* Reflejo/sheen: una franja diagonal de luz que recorre la tarjeta en loop
   continuo, como el brillo de una superficie de vidrio. Va en ::after (pinta
   arriba de .pf-image y .pf-overlay porque es el último en el orden del
   documento) con mix-blend-mode:screen para no lavar el texto del overlay,
   y overflow:hidden en .portfolio-card evita que se salga del borde
   redondeado de la tarjeta.
   El tile de fondo es 300% y la franja de luz vive en el 40%-60% central:
   en los extremos del recorrido (0% / 100%) la ventana visible de la card
   cae DENTRO de una zona 100% transparente del tile (ver cuenta en el
   comentario de .pa-media-frame::before, mismo criterio) — así el reflejo
   arranca y termina totalmente invisible, sin el corte diagonal duro que
   se veía antes al quedar la animación "pausada" a mitad de camino.
   La animación en sí solo corre mientras la card está en pantalla: animar
   background-position no lo acelera la GPU como transform/opacity, y con
   15 cards en la grilla más los botones/nota que comparten el efecto,
   tenerlas todas animando a la vez (incluso fuera de vista) pesa en
   repintado/batería sin aportar nada. Un IntersectionObserver en
   script.js agrega/saca la clase ".sheen-on" según cada card entra o sale
   del viewport, así en cualquier momento sólo animan las 1-3 cards que el
   usuario tiene realmente delante — funciona igual en mobile que en
   desktop. */
.portfolio-card::after{
    content:'';
    position:absolute;
    inset:0;
    pointer-events:none;
    background:linear-gradient(115deg, transparent 0%, transparent 40%, rgba(255,255,255,.14) 50%, transparent 60%, transparent 100%);
    background-size:300% 300%;
    background-position:0% 0%;
    mix-blend-mode:screen;
}
@keyframes cardSheen{
    from{background-position:0% 0%;}
    to{background-position:100% 100%;}
}
.portfolio-card.sheen-on::after{animation:cardSheen 5s ease-in-out infinite alternate;}
/* Tilt 3D sutil (ver script.js, solo desktop con mouse fino): --rx/--ry
   son seteadas por JS en cada frame de mousemove sobre la card */
.portfolio-card.has-tilt{
    --rx:0deg;--ry:0deg;
    transform:perspective(900px) rotateX(var(--rx)) rotateY(var(--ry));
}
/* Entrada al cambiar de filtro / "Mostrar más": las cards que pasan de
   display:none a visibles no aparecen de golpe sino con un fade + subida
   corta, escalonadas (--pf-i la setea renderPortfolio() en script.js).
   El keyframe sólo define "from": el "to" es el valor computado, así que
   no pisa el transform del tilt ni el opacity del reveal. Sólo aplica a
   cards que ya pasaron su reveal inicial (.show) — las demás las sigue
   revelando el observer normal. */
.portfolio-card.show.pf-enter{
    animation:pfCardIn .7s var(--ease-out-expo) both;
    animation-delay:calc(var(--pf-i,0) * 70ms);
}
@keyframes pfCardIn{from{opacity:0;transform:translateY(18px) scale(.97);}}
/* Al pasar el mouse, el texto del overlay "asoma" apenas hacia arriba (en
   cascada) mientras el logo de fondo hace su zoom — refuerza la
   profundidad entre capas sin tocar el layout. */
.pf-overlay > *{transition:transform .6s var(--ease-smooth);}
@media (hover:hover){
    .portfolio-card:hover .pf-overlay > *{transform:translateY(-4px);}
    .portfolio-card:hover .pf-overlay > .pf-category{transition-delay:0s;}
    .portfolio-card:hover .pf-overlay > h3{transition-delay:.03s;}
    .portfolio-card:hover .pf-overlay > p{transition-delay:.06s;}
    .portfolio-card:hover .pf-overlay > .pf-tags{transition-delay:.09s;}
    .portfolio-card:hover .pf-overlay > .pf-link{transition-delay:.12s;}
}

/* Logo/imagen real del proyecto: centrada, con padding, sin recorte agresivo.
   "position:absolute;inset:0" (igual que .pf-overlay) es clave: si esto fuera
   un hijo en flujo normal del flex de .portfolio-card, la altura REAL del
   logo (según su relación de aspecto propia + el padding-bottom en %% de
   pf-image-img) terminaba empujando la tarjeta mucho más alta que el
   min-height definido, sobre todo en mobile con logos verticales — la
   tarjeta quedaba gigante con el texto ocupando una porción chica. Al
   sacarlo del flujo, la altura de la tarjeta la manda 100% el min-height. */
.portfolio-card .pf-image{
    position:absolute;
    inset:0;
    display:flex;align-items:center;justify-content:center;
    font-size:3.2rem;
    transition:transform .6s cubic-bezier(.4,0,.2,1);
}
.pf-image-img{
    object-fit:contain;
    padding:10% 16% 38%;   /* deja espacio abajo para que no choque con el overlay de texto */
    filter:drop-shadow(0 8px 20px rgba(0,0,0,.4));
}
/* Logos oscuros/monocromáticos que se pierden contra el fondo negro:
   los invertimos a blanco manteniendo la transparencia del PNG original */
.pf-logo-white{
    filter:invert(1) brightness(1.4) drop-shadow(0 8px 20px rgba(0,0,0,.4));
}
/* Logo propio del sitio en la card "Este portfolio": es una marca angosta
   y vertical, necesita menos aire lateral que los logos externos */
.pf-logo-own{
    padding:6% 26% 40%;
    filter:drop-shadow(0 0 20px rgba(47,111,237,.45));
}
/* Logo de OrlandoStudio: en vez de <img>, SVG inline (coincide con como esta
   construido el sitio original) coloreado con su verde de marca */
.pf-logo-svg{
    padding:8% 12% 34%;
    color:#33b875;
    filter:drop-shadow(0 8px 24px rgba(51,184,117,.35));
}
/* Logo de Agencia Luz: sin logo definitivo todavía, se usa el signo
   "</>" (símbolo genérico de programación) en el acento azul del sitio
   como placeholder, hasta que el cliente tenga un logo propio */
.pf-logo-luz{
    padding:14% 24% 40%;
    color:var(--accent-hot);
    filter:drop-shadow(0 8px 24px var(--accent-glow));
}
.pf-logo-code{
    font-family:var(--font-mono);
    font-size:3.6rem;
    font-weight:700;
    letter-spacing:-1px;
}
/* Logo de Perfumería Fina: sin logo definitivo, se arma un wordmark en
   script (misma tipografía que usa el sitio real) en tono dorado */
.pf-logo-perfumeria{
    padding:18% 12% 42%;
    flex-direction:column;
    gap:10px;
    color:#d9b673;
    filter:drop-shadow(0 8px 24px rgba(217,182,115,.3));
}
.pf-logo-perfumeria .pf-logo-script{
    font-family:'Parisienne',cursive;
    font-size:2.2rem;
    line-height:1.1;
    text-align:center;
}
.pf-logo-perfumeria .pf-logo-rule{
    width:64px;
    height:1px;
    background:currentColor;
    opacity:.6;
}
.pf-logo-svg svg{width:100%;height:100%;}
/* Logo de Capitán Barbería: emblema prácticamente cuadrado (738x760), no
   rectangular como el resto — con el padding base (16% lateral) el
   content-box quedaba angosto y muy alto, así que object-fit:contain lo
   encogía a un cuadrado chico flotando en medio de mucho aire vertical.
   Menos padding lateral y menos padding-bottom achican esa relación de
   aspecto del content-box, acercándola a la del logo. */
.pf-logo-capitan{
    padding:10% 12% 34%;
}
.portfolio-card:hover .pf-image,
.portfolio-card:active .pf-image{transform:scale(1.04);}

.pf-overlay{
    position:absolute;inset:0;
    background:linear-gradient(180deg,transparent 8%,rgba(5,5,6,0.75) 38%,rgba(5,5,6,0.97) 100%);
    display:flex;
    flex-direction:column;
    justify-content:flex-end;
    padding:24px;
    opacity:1; /* siempre visible en mobile (sin hover) */
    transition:.4s ease;
}
.pf-category{
    color:var(--accent-hot);
    font-family:var(--font-mono);
    font-size:.68rem;
    font-weight:600;
    text-transform:uppercase;
    letter-spacing:1px;
    margin-bottom:8px;
}
.pf-overlay h3{font-size:1.1rem;margin-bottom:8px;}
.pf-overlay p{font-size:.85rem;margin-bottom:14px;line-height:1.6;}
.pf-tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px;}
.pf-tag{
    background:rgba(255,255,255,0.1);
    padding:4px 10px;
    border-radius:20px;
    font-family:var(--font-mono);
    font-size:.66rem;
    color:var(--white);
}

/* Botón "Ver sitio en vivo" dentro de cada card */
.pf-link{
    display:inline-flex;
    align-items:center;
    gap:6px;
    align-self:flex-start;
    font-size:.82rem;
    font-weight:700;
    color:var(--white);
    background:rgba(47,111,237,0.18);
    border:1px solid rgba(47,111,237,0.4);
    padding:9px 16px;
    border-radius:50px;
    transition:.3s ease;
}
.pf-link:hover{
    background:var(--accent);
    border-color:var(--accent);
    transform:translateY(-1px);
}

/* Bloque "Mostrar más": reemplaza la paginación con flechas (no funcional,
   siempre marcaba "01/14" fijo) por un control real que refleja cuántas
   cards quedan ocultas según el filtro activo. Se oculta solo (JS) cuando
   ya no hay más cards para mostrar. */
.portfolio-more{
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:22px;
    margin-top:36px;
    padding-top:28px;
    border-top:1px solid var(--border-soft);
}
.portfolio-more-inner{
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:14px;
}
.portfolio-more-count{
    font-size:.82rem;
    color:var(--gray-mid);
    font-family:var(--font-mono);
}
.portfolio-more-inner svg{width:15px;height:15px;transition:transform .3s ease;}
.portfolio-more-inner .btn:hover svg{transform:translateY(2px);}

/* CTA hacia el Portfolio Animado: mismo lenguaje de "halo de luz
   intermitente" que usa el botón de WhatsApp (ver @keyframes wappGlow), para que
   el brillo se sienta sutil y coherente con el resto del sitio en vez
   de un efecto nuevo que compita por atención. */
.btn-glow{
    display:inline-flex;
    align-items:center;
    gap:9px;
    padding:13px 26px;
    border-radius:50px;
    font-weight:700;
    font-size:.88rem;
    color:var(--white);
    background:rgba(47,111,237,0.12);
    border:1px solid rgba(47,111,237,0.4);
    animation:btnGlowPulse 3s ease-in-out infinite;
    transition:background .3s ease, border-color .3s ease, transform .3s ease;
}
.btn-glow svg{width:16px;height:16px;flex-shrink:0;}
.btn-glow:hover{
    background:var(--accent);
    border-color:var(--accent);
    transform:translateY(-2px);
}
/* Variante compacta: junto a los filtros de categoría, del mismo porte
   que los ".filter-btn" en vez del tamaño de CTA grande. */
.btn-glow-sm{padding:9px 20px;font-size:.8rem;gap:7px;}
.btn-glow-sm svg{width:14px;height:14px;}
@keyframes btnGlowPulse{
    0%,100%{box-shadow:0 8px 22px -10px rgba(47,111,237,.5), 0 0 14px 0 rgba(91,141,255,.08);}
    50%{box-shadow:0 8px 22px -10px rgba(47,111,237,.5), 0 0 22px 2px rgba(91,141,255,.38);}
}
@media (prefers-reduced-motion: reduce){
    .btn-glow{animation:none;}
}

/* =====================================================================
   9. POR QUÉ ELEGIRME
   ===================================================================== */
/* overflow-y (no shorthand "overflow"): el eje X sigue gobernado por la
   regla genérica de "section" más arriba (overflow-x:hidden/clip, clave
   para el fix de taps fantasma en mobile) — acá solo se agrega contención
   vertical para que el recorrido amplio del blob-dance no se filtre
   visualmente hacia la sección de abajo. */
.porque{background:var(--bg-black);overflow-y:hidden;}

/* Trazo de acento de esta sección: mucho más largo que el resto (ver
   regla genérica ".section-header h2::after" más arriba) y con una luz
   recorriéndolo de izquierda a derecha en loop. La curva de easing es a
   propósito muy pronunciada (lenta en los extremos, rápida en el tramo
   central) para que se sienta como un barrido premium, no un parpadeo. */
.porque .section-header h2::after{
    width:clamp(140px, 22vw, 320px);
    height:4px;
    margin-top:20px;
    background-image:linear-gradient(90deg,
        var(--accent) 0%, var(--accent) 32%,
        var(--wave-teal) 46%, #eafffb 50%, var(--wave-teal) 54%,
        var(--accent) 68%, var(--accent) 100%);
    background-size:300% 100%;
    background-position:200% 0;
    animation:porqueLineSweep 4.2s cubic-bezier(.83,0,.17,1) infinite;
}
@keyframes porqueLineSweep{
    0%{background-position:200% 0;}
    100%{background-position:-100% 0;}
}
@media (prefers-reduced-motion: reduce){
    .porque .section-header h2::after{animation:none;background-position:50% 0;}
}
.porque-grid-canvas{
    position:absolute;
    inset:0;
    z-index:1;
    pointer-events:none;
}
.porque-grid{
    display:grid;
    grid-template-columns:1fr;
    gap:36px;
}
/* Envoltorio que sólo se ocupa del fade-in (.reveal, ver script.js).
   El tilt 3D vive en el hijo ".porque-visual": ".reveal.show" define su
   propio "transform" con más especificidad (dos clases) que una regla de
   una sola clase, así que si el perspective/rotate estuviera en el MISMO
   elemento que ".reveal", quedaría pisado apenas termina el fade-in y la
   tarjeta se vería plana. Mismo motivo por el que portfolio-animado.css
   separa ".pa-media" (posición) de ".pa-media-frame" (el que rota). */
.porque-visual-wrap{display:block;}
/* Tarjeta 3D luminiscente: mismo recurso que las tarjetas de proyecto
   de portfolio-animado.css (perspective + rotateX/rotateY vía custom
   properties que mueve script.js, halo pulsante + brillo especular que
   sigue al mouse). --rx/--ry/--glow-x/--glow-y quedan neutros por
   defecto: sin JS (o en touch/reduced-motion) se ve plana y estática. */
.porque-visual{
    position:relative;
    width:100%;
    --rx:0deg;
    --ry:0deg;
    --glow-x:50%;
    --glow-y:50%;
    --card-glow-soft:rgba(97,218,251,.4);
    --glow-blur-a:26px;
    --glow-spread-a:-14px;
    --glow-blur-b:60px;
    --glow-spread-b:-30px;
    border-radius:var(--radius-lg);
    overflow:hidden;
    background:linear-gradient(145deg, #17171b, #0c0c0e);
    border:1px solid var(--border-soft);
    aspect-ratio:5/4;
    display:flex;
    align-items:center;
    justify-content:center;
    transform-style:preserve-3d;
    transform:perspective(1000px) rotateX(var(--rx)) rotateY(var(--ry));
    will-change:transform;
    box-shadow:
        0 40px 80px -30px rgba(0,0,0,.55),
        0 0 var(--glow-blur-a) var(--glow-spread-a) var(--card-glow-soft),
        0 0 var(--glow-blur-b) var(--glow-spread-b) var(--card-glow-soft);
    animation:porqueGlowPulse 5s ease-in-out infinite;
}
@keyframes porqueGlowPulse{
    0%,100%{
        box-shadow:
            0 40px 80px -30px rgba(0,0,0,.55),
            0 0 var(--glow-blur-a) var(--glow-spread-a) var(--card-glow-soft),
            0 0 var(--glow-blur-b) var(--glow-spread-b) var(--card-glow-soft);
    }
    50%{
        box-shadow:
            0 40px 80px -30px rgba(0,0,0,.55),
            0 0 calc(var(--glow-blur-a) + 8px) calc(var(--glow-spread-a) + 3px) var(--card-glow-soft),
            0 0 calc(var(--glow-blur-b) + 16px) calc(var(--glow-spread-b) + 4px) var(--card-glow-soft);
    }
}
/* Capa trasera desplazada en Z: asoma por el borde al rotar, da sensación
   de grosor real en vez de un plano chato. */
.porque-visual::before{
    content:'';
    position:absolute;
    inset:0;
    border-radius:inherit;
    background:linear-gradient(160deg, rgba(97,218,251,.35), rgba(47,111,237,.25));
    transform:translateZ(-22px);
}
/* Brillo especular que sigue al mouse (overlay glossy, no toca el ícono) */
.porque-visual::after{
    content:'';
    position:absolute;
    inset:0;
    border-radius:inherit;
    background:radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255,255,255,.18), transparent 55%);
    mix-blend-mode:screen;
    pointer-events:none;
}
.porque-visual-icon{
    position:relative;
    width:150px;
    height:150px;
    filter:drop-shadow(0 10px 30px rgba(97,218,251,.35));
    transform:translateZ(34px);
}
.react-logo{color:#61dafb;}
@media (prefers-reduced-motion: reduce){
    .porque-visual{transform:none;animation:none;}
}

.porque-list{display:flex;flex-direction:column;gap:4px;}
.porque-item{
    display:flex;
    gap:16px;
    padding:26px 0;
    border-bottom:1px solid var(--border-soft);
}
.porque-item:last-child{border-bottom:none;}
.porque-num{
    font-family:var(--font-mono);
    font-size:1.2rem;
    font-weight:700;
    color:var(--accent-hot);
    min-width:40px;
    transition:transform .5s var(--ease-smooth), color .3s ease;
}
/* Hover de cada argumento: el número se desplaza apenas a la derecha y
   el separador de abajo se enciende (ver ::after). Nada cambia de tamaño. */
.porque-item{position:relative;transition:border-color .3s ease;}
.porque-item::after{
    content:'';
    position:absolute;
    left:0;right:0;bottom:-1px;
    height:1px;
    background:linear-gradient(90deg,var(--accent),var(--wave-teal),transparent);
    transform:scaleX(0);
    transform-origin:left;
    transition:transform .6s var(--ease-smooth);
}
.porque-item:last-child::after{display:none;}
@media (hover:hover){
    .porque-item:hover .porque-num{transform:translateX(4px);color:var(--white);}
    .porque-item:hover::after{transform:scaleX(1);}
}
.porque-text h3{font-size:1.02rem;margin-bottom:6px;}
.porque-text p{font-size:.9rem;}

/* Cierre de sección: agendar llamada directo, aprovechando que ya se leyó
   todo el argumento de "por qué elegirme" (ver index.html). */
.porque-cta{
    margin-top:44px;
    padding-top:32px;
    border-top:1px solid var(--border-soft);
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:space-between;
    gap:18px;
}
.porque-cta p{font-size:1rem;color:var(--white);margin:0;}
.porque-cta .btn{width:auto;}

/* =====================================================================
   10. CTA + FORMULARIO
   ===================================================================== */
.cta-final{
    background:linear-gradient(180deg,var(--bg-black) 0%, #060a16 100%);
    overflow-y:hidden; /* contiene el recorrido amplio del blob-dance (ver sección 3) */
}
.cta-grid{
    display:grid;
    grid-template-columns:1fr;
    gap:40px;
}
.cta-left h2{font-size:clamp(1.7rem,6vw,2.8rem);margin-bottom:18px;}
.cta-left p{font-size:1rem;margin-bottom:28px;}
.cta-points{display:flex;flex-direction:column;gap:18px;}
.cta-point{display:flex;align-items:center;gap:12px;font-size:.9rem;color:var(--white);}
.cta-points + .cta-book-btn{margin-top:32px;}
.cta-point .check{
    width:24px;height:24px;
    border-radius:50%;
    background:rgba(47,111,237,0.15);
    color:var(--accent-hot);
    display:flex;align-items:center;justify-content:center;
    font-size:.75rem;
    flex-shrink:0;
}
/* Los tildes "aparecen" uno a uno con un rebote corto cuando el bloque
   .cta-left termina su reveal (la animación arranca con la clase .show
   del propio contenedor, sin observer extra). */
.cta-left.reveal .cta-point .check{opacity:0;transform:scale(.4);}
.cta-left.reveal.show .cta-point .check{
    animation:checkPop .6s cubic-bezier(.34,1.56,.64,1) forwards;
}
.cta-left.reveal.show .cta-point:nth-child(1) .check{animation-delay:.45s;}
.cta-left.reveal.show .cta-point:nth-child(2) .check{animation-delay:.6s;}
.cta-left.reveal.show .cta-point:nth-child(3) .check{animation-delay:.75s;}
@keyframes checkPop{to{opacity:1;transform:scale(1);}}
@media (prefers-reduced-motion: reduce){
    .cta-left.reveal .cta-point .check{opacity:1;transform:none;animation:none !important;}
}

.form-card{
    position:relative;
    overflow:hidden;
    background-color:var(--bg-card);
    border:1px solid var(--border-soft);
    border-radius:var(--radius-lg);
    padding:26px;
}
/* Marca de agua: el isotipo del logo, muy tenue, centrado detrás de los
   campos del formulario. Solo dentro del modal (".modal-panel .form-card")
   — el mismo .form-card también vive inline en #contacto cuando el modal
   está cerrado, y ahí NO debe llevar watermark. ::before pinta antes que
   el <form> (que también es "position:relative" para volverse un elemento
   posicionado y quedar por delante en el orden de pintado), así que el
   watermark queda siempre detrás de los inputs sin taparlos. */
.modal-panel .form-card::before{
    content:'';
    position:absolute;
    inset:0;
    background:url('../img/logo-icon.webp') no-repeat center center;
    background-size:min(65%, 300px);
    opacity:.05;
    pointer-events:none;
}
.modal-panel .form-card form{position:relative;}
.form-row{display:grid;grid-template-columns:1fr;gap:16px;}
.form-group{margin-bottom:18px;}
.form-group label{
    display:block;
    font-size:.82rem;
    font-weight:700;
    margin-bottom:7px;
    color:var(--gray-soft);
    transition:color .25s ease;
}
.form-group:focus-within label{color:var(--accent-hot);}
.form-group input,
.form-group select,
.form-group textarea{
    width:100%;
    padding:13px 15px;
    background:rgba(255,255,255,0.04);
    border:1px solid var(--border-soft);
    border-radius:var(--radius-sm);
    color:var(--white);
    font-family:var(--font-sans);
    font-size:.9rem;
    transition:.3s ease;
}
.form-group select option{background:var(--bg-card);}
.form-group input::placeholder,
.form-group textarea::placeholder{color:var(--gray-mid);}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus{
    outline:none;
    border-color:var(--accent-hot);
    background:rgba(255,255,255,0.06);
    box-shadow:0 0 0 4px rgba(47,111,237,0.12);
}
.form-group textarea{min-height:110px;resize:vertical;}
.form-submit{
    width:100%;
    border:none;
    cursor:pointer;
    padding:15px;
    border-radius:50px;
    background:linear-gradient(135deg,var(--accent),var(--accent-hot));
    color:#fff;
    font-family:var(--font-sans);
    font-weight:700;
    font-size:.92rem;
    margin-top:4px;
    transition:.35s ease;
    box-shadow:0 10px 26px -8px var(--accent-glow);
}
.form-submit:hover{transform:translateY(-2px);box-shadow:0 14px 30px -6px rgba(47,111,237,.55);}
.form-submit:active{transform:translateY(0) scale(.98);transition-duration:.12s;}
/* Aviso informativo bajo el botón de envío (primera capa de información
   que exige el RGPD en el punto donde se recogen los datos). */
.form-legal{font-size:.74rem;line-height:1.6;color:var(--gray-mid);margin-top:14px;text-align:center;}
.form-legal a{color:var(--gray-soft);text-decoration:underline;text-decoration-color:rgba(255,255,255,.25);text-underline-offset:3px;}
.form-legal a:hover{color:var(--accent-hot);text-decoration-color:currentColor;}

/* Modal de contacto: "Pedir presupuesto"/"Solicitar presupuesto" abren esto
   en vez de saltar a #contacto (ver script.js). El .form-card real se
   mueve para adentro del .modal-panel al abrir y vuelve a su lugar al
   cerrar, así que no necesita estilos propios acá — ya trae los suyos. */
.modal-overlay{
    position:fixed;
    inset:0;
    z-index:2000;
    display:flex;
    align-items:center;
    justify-content:center;
    padding:24px;
    opacity:0;
    visibility:hidden;
    transition:opacity .35s ease, visibility 0s linear .35s;
}
.modal-overlay.active{
    opacity:1;
    visibility:visible;
    transition:opacity .35s ease;
}
.modal-backdrop{
    position:absolute;
    inset:0;
    background:rgba(5,6,10,.6);
    backdrop-filter:blur(2px);
}
.modal-panel{
    position:relative;
    width:100%;
    max-width:600px;
    max-height:88vh;
    overflow-y:auto;
    transform:scale(.94) translateY(16px);
    opacity:0;
    transition:transform .4s cubic-bezier(.4,0,.2,1), opacity .3s ease;
}
.modal-overlay.active .modal-panel{
    transform:scale(1) translateY(0);
    opacity:1;
}
.modal-panel .form-card{margin:0;}
/* Alternativa a completar el form: agendar la llamada de 30 min directo
   desde Cal.com (botón [data-cal-link], ver index.html). Va antes del
   .form-card en el DOM así queda arriba, sin desordenar el appendChild
   que hace script.js al abrir el modal. */
.modal-cal-cta{
    padding:56px 26px 24px;
    text-align:center;
}
.modal-cal-cta p{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:center;
    gap:10px;
    font-size:.85rem;
    color:var(--gray-soft);
    margin:0;
}
.modal-cal-cta .btn{width:auto;}
.modal-close{
    position:absolute;
    top:14px;
    right:14px;
    width:38px;
    height:38px;
    border-radius:50%;
    display:flex;
    align-items:center;
    justify-content:center;
    background:rgba(255,255,255,0.08);
    border:1px solid var(--border-soft);
    color:var(--white);
    cursor:pointer;
    z-index:1;
    transition:.25s ease;
}
.modal-close svg{width:18px;height:18px;}
.modal-close:hover{background:var(--accent);border-color:var(--accent);}
/* El resto de la página queda "detrás de vidrio" mientras el modal está
   abierto: blur en todos los hijos directos de <body> salvo el modal
   mismo (así funciona sin envolver todo el contenido existente en un
   wrapper nuevo). pointer-events:none evita que, con el mouse, se pueda
   interactuar con lo que queda debajo del blur. Se excluye también
   <cal-modal-box> (el popup de reserva de Cal.com, que se abre encima del
   modal desde el botón "Agendá 30 min gratis" de ahí adentro): Cal lo
   inyecta como hijo directo de <body>, así que sin esta excepción quedaba
   tan blureado e inclickeable como el resto del fondo. */
body.modal-open{overflow:hidden;}
body.modal-open > *:not(.modal-overlay):not(cal-modal-box){
    filter:blur(10px);
    pointer-events:none;
    user-select:none;
    transition:filter .35s ease;
}
@media (prefers-reduced-motion: reduce){
    .modal-overlay, .modal-panel{transition:none;}
    body.modal-open > *:not(.modal-overlay):not(cal-modal-box){transition:none;}
}

/* Overlay de carga de Cal.com: al hacer click en "Agendar reunión"
   ([data-cal-link], ver index.html/sobre-mi.html), Cal.com abre su propio
   popup (<cal-modal-box>, inyectado como hijo directo de <body>) con su
   spinner genérico mientras carga el iframe de reserva — a veces con un
   delay notorio. Este overlay se muestra encima de ESE popup (mismo
   z-index al tope; ver JS: se reinserta como último hijo de <body> cada
   vez que Cal agrega/actualiza el suyo, porque a igual z-index gana el
   último en el DOM) mostrando mensajes de marca en vez del spinner de Cal,
   y se saca solo cuando Cal dispara su evento "linkReady" (calendario ya
   renderizado adentro del iframe) — ver JS para el detalle. */
.cal-loading-overlay{
    position:fixed;
    inset:0;
    z-index:2147483647;
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    gap:22px;
    background:rgba(5,6,10,.82);
    backdrop-filter:blur(6px);
    -webkit-backdrop-filter:blur(6px);
    opacity:0;
    visibility:hidden;
    transition:opacity .3s ease, visibility 0s linear .3s;
}
.cal-loading-overlay.active{
    opacity:1;
    visibility:visible;
    transition:opacity .3s ease;
}
.cal-loading-spinner{
    width:52px;
    height:52px;
    border-radius:50%;
    border:3px solid rgba(255,255,255,.12);
    border-top-color:var(--accent-hot);
    border-right-color:var(--accent);
    animation:calLoadingSpin .85s linear infinite;
    box-shadow:0 0 24px var(--accent-glow);
}
@keyframes calLoadingSpin{to{transform:rotate(360deg);}}
.cal-loading-text{
    margin:0;
    font-size:.95rem;
    font-weight:500;
    color:var(--gray-soft);
    letter-spacing:.2px;
    opacity:0;
    transform:translateY(6px);
}
.cal-loading-text.cal-loading-text-in{
    animation:calLoadingTextIn .45s ease forwards;
}
@keyframes calLoadingTextIn{
    to{opacity:1;transform:translateY(0);}
}
@media (prefers-reduced-motion: reduce){
    .cal-loading-overlay{transition:none;}
    .cal-loading-spinner{animation:none;}
    .cal-loading-text.cal-loading-text-in{animation:none;opacity:1;transform:none;}
}

/* =====================================================================
   11. FOOTER
   ===================================================================== */
footer{
    background:var(--bg-black-2);
    border-top:1px solid var(--border-soft);
    padding:48px 0 24px;
}
.footer-grid{
    display:grid;
    grid-template-columns:1fr;
    gap:32px;
    margin-bottom:36px;
}
/* En mobile el logo del footer se agranda (3x) y se centra para que
   cierre la página con más presencia; en tablet+ vuelve a su tamaño
   normal alineado a la izquierda (ver breakpoint 768px). */
.footer-brand{text-align:center;}
.footer-brand .logo{justify-content:center;margin-left:0;}
.footer-brand .logo-icon{height:128px;}
.footer-brand p{font-size:.88rem;margin:16px auto 20px;max-width:320px;}
.footer-social{display:flex;gap:10px;justify-content:center;}
.social-icon{
    width:38px;height:38px;
    border-radius:50%;
    border:1px solid var(--border-soft);
    display:flex;align-items:center;justify-content:center;
    color:var(--gray-soft);
    transition:.3s ease;
}
.social-icon svg{display:block;}
.social-icon:hover{background:var(--accent);border-color:var(--accent);color:var(--white);transform:translateY(-3px);}
.footer-col h4{font-size:.9rem;margin-bottom:16px;}
.footer-col a{
    display:block;
    width:fit-content;
    color:var(--gray-soft);
    font-size:.88rem;
    margin-bottom:12px;
    transition:color .3s ease, transform .4s var(--ease-smooth);
}
.footer-col a:hover{color:var(--accent-hot);transform:translateX(4px);}
.footer-col .footer-link-btn{
    display:block;
    background:none;
    border:0;
    padding:0;
    font:inherit;
    font-size:.88rem;
    color:var(--gray-soft);
    margin-bottom:12px;
    cursor:pointer;
    text-align:left;
    transition:color .3s ease, transform .4s var(--ease-smooth);
}
.footer-col .footer-link-btn:hover{color:var(--accent-hot);transform:translateX(4px);}
/* Columna "Legal": cada link lleva un punto que se enciende en hover,
   además del mismo gesto que el resto de links del footer. */
.footer-legal a,
.footer-legal .footer-link-btn{display:flex;align-items:center;gap:10px;}
.footer-legal a::before,
.footer-legal .footer-link-btn::before{
    content:'';
    width:5px;height:5px;
    border-radius:50%;
    background:var(--border-soft-hover);
    transition:background .3s ease, box-shadow .3s ease;
    flex-shrink:0;
}
.footer-legal a:hover::before,
.footer-legal .footer-link-btn:hover::before{background:var(--accent-hot);box-shadow:0 0 10px var(--accent-hot);}
.footer-location{
    display:block;
    color:var(--gray-soft);
    font-size:.88rem;
    margin-bottom:12px;
}
.footer-bottom{
    border-top:1px solid var(--border-soft);
    padding-top:22px;
    display:flex;
    flex-direction:column;
    gap:8px;
    font-size:.78rem;
    color:var(--gray-mid);
}

/* =====================================================================
   12. LUZ RESPLANDECIENTE QUE SIGUE AL CURSOR (solo desktop/mouse)
   ===================================================================== */
.cursor-glow{
    position:fixed;
    top:0;left:0;
    width:440px;height:440px;
    border-radius:50%;
    pointer-events:none;
    /* z-index:2, igual que ".grain-overlay" más abajo: por encima de
       ".pa-track" (portfolio-animado.css, z-index:1) para que el brillo
       no quede tapado por los paneles negros de esa página, y por debajo
       del contenido normal de las secciones (que vive a z-index:2 sobre
       su propio stacking context vía isolation:isolate, y gana el empate
       por venir después en el HTML), así sigue sintiéndose de fondo. */
    z-index:2;
    transform:translate(-50%,-50%);
    will-change:transform;
    opacity:0;
    transition:opacity .4s ease;
}
.cursor-glow.active{opacity:1;}

/* Cada click cicla el tono entre azul (default) y el mismo verde-azulado
   (--wave-teal) que ya aparece en el fondo de otras secciones (ver el
   listener de "click" en script.js / sobre-mi.js / portfolio-animado.js).
   Se hace con capas superpuestas que cruzan opacidad en vez de animar el
   color del gradiente directamente: un custom property no-registrado (sin
   @property) no interpola de forma fluida entre colores en todos los
   navegadores, mientras que animar opacity sí es fluido y consistente en
   cualquiera. La transición es larga (2.2s) a propósito, para que el
   cambio de tono se sienta como una respiración lenta, no como un flash. */
.cursor-glow-layer{
    position:absolute;
    inset:0;
    border-radius:50%;
    transition:opacity 2.2s ease;
}
.cursor-glow-blue{
    background:radial-gradient(circle, rgba(47,111,237,0.10) 0%, transparent 70%);
    opacity:1;
}
.cursor-glow-green{
    background:radial-gradient(circle, rgba(31,201,195,0.14) 0%, transparent 70%);
    opacity:0;
}
.cursor-glow[data-tone="green"] .cursor-glow-blue{opacity:0;}
.cursor-glow[data-tone="green"] .cursor-glow-green{opacity:1;}

/* =====================================================================
   13. ANIMACIONES DE ENTRADA (reveal on scroll)
   ===================================================================== */
/* Las reglas de reveal sólo aplican hasta que el JS agrega ".reveal-done"
   (al terminar la transición de entrada). Antes, ".reveal.show" dejaba
   para siempre "transform:translateY(0)" y su propia "transition" sobre
   el elemento, y como esta sección viene después en el archivo, pisaba
   con igual especificidad el transform del tilt 3D de las cards de
   portfolio (.has-tilt — quedaba anulado) y las transiciones de hover
   propias de cards y botones. Con ":not(.reveal-done)", una vez revelado
   el elemento queda 100% "nativo": sus propios transforms y transiciones. */
.reveal:not(.reveal-done){
    opacity:0;
    transform:translateY(26px);
    transition:opacity .9s var(--ease-out-expo), transform .9s var(--ease-out-expo);
    /* Cascada automática: script.js/sobre-mi.js asignan --reveal-i (0,1,2..)
       a cada elemento según el orden en que entra al viewport en el mismo
       frame (una grilla de cards, una lista de items), para que aparezcan
       uno detrás del otro sin declarar delays por grilla. Los hijos de
       ".reveal-stagger" traen su delay fijo (abajo) y ganan por
       especificidad. */
    transition-delay:calc(var(--reveal-i,0) * 90ms);
}
.reveal.show:not(.reveal-done){opacity:1;transform:translateY(0);}

/* Trazo bajo el título de sección: se "dibuja" de izquierda a derecha
   apenas el header aparece (scaleX, compositor), un poco después del
   fade del bloque para que se lea como un remate y no como parte del
   mismo movimiento. Headers que no son .reveal quedan con el trazo
   completo desde el inicio. */
.section-header.reveal h2::after{
    transform:scaleX(0);
    transform-origin:left;
    transition:transform 1s var(--ease-out-expo) .4s;
}
.section-header.center.reveal h2::after{transform-origin:center;}
.section-header.show h2::after{transform:scaleX(1);}

/* Cascada de entrada: dentro de un contenedor ".reveal-stagger", cada hijo
   directo con ".reveal" recibe una demora creciente para que aparezcan
   uno detrás del otro (título, párrafos, botón...) en vez de todos a la
   vez de golpe. El reduced-motion de más abajo ya deja ".reveal" sin
   transición, así que ahí la demora queda sin efecto automáticamente. */
.reveal-stagger > .reveal:nth-child(1){transition-delay:.05s;}
.reveal-stagger > .reveal:nth-child(2){transition-delay:.15s;}
.reveal-stagger > .reveal:nth-child(3){transition-delay:.25s;}
.reveal-stagger > .reveal:nth-child(4){transition-delay:.35s;}
.reveal-stagger > .reveal:nth-child(5){transition-delay:.45s;}
.reveal-stagger > .reveal:nth-child(6){transition-delay:.55s;}
.reveal-stagger > .reveal:nth-child(7){transition-delay:.65s;}

/* En touch (mobile/tablet) el desplazamiento vertical del reveal puede dejar
   un botón moviéndose justo cuando el usuario lo toca: el dedo queda quieto
   pero el elemento se desplaza por debajo entre el touchstart y el touchend,
   así que el navegador no dispara el click. Esto es la causa de los botones
   que "a veces andan y a veces no" en mobile — no es aleatorio, depende de
   si se toca mientras el elemento todavía está animando su entrada. En touch
   se deja solo el fade de opacidad, sin mover nada, así el botón nunca
   cambia de posición mientras se lo está tocando. */
@media (hover:none), (pointer:coarse){
    .reveal:not(.reveal-done){transform:none;transition:opacity .5s ease;transition-delay:calc(var(--reveal-i,0) * 90ms);}
    .reveal.show:not(.reveal-done){transform:none;}
    /* Mismo criterio para el botón flotante de WhatsApp: también tiene una
       animación de entrada con transform (scale+translateY) al cargar la
       página, y un tap justo en ese instante puede fallar igual que con
       .reveal. En touch aparece con opacidad nada más. */
    .whatsapp-float{transform:none;transition:opacity .4s ease;}
    .whatsapp-float.wa-visible{transform:none;}
}

@media (prefers-reduced-motion: reduce){
    .global-bg, .blob, .hero-tag .dot{animation:none !important;}
    .reveal:not(.reveal-done){transition:none;opacity:1;transform:none;}
    .section-header.reveal h2::after{transition:none;transform:scaleX(1);}
    .cursor-glow{display:none;}
    .nav-links{transition:opacity .15s ease, visibility 0s linear;transform:none;}
    .nav-links > *{transition:none;opacity:1;transform:none;}
    .nav-wrap{transition:none;}
    .scroll-progress{display:none;}
    .btn-primary::after{display:none;}
    .hero-content, .hero-exit-title, .hero-exit-subtitle, .hero-exit-cta, .hero h1, .hero-subtitle, .hero-cta, .hero-stats, .hero-stats .stat, .hw-i, .hero .blob, .particle-bg{animation:none !important;opacity:1;transform:none;filter:none;}
    .hero-stats::before{animation:none;transform:none;}
    .portfolio-card{transition:border-color .3s ease, box-shadow .3s ease;}
    .portfolio-card.has-tilt{transform:none;}
    .portfolio-card::after,
    .portfolio-card.sheen-on::after{animation:none !important;background-position:0% 0%;}
}

/* =====================================================================
   14. BOTÓN FLOTANTE DE WHATSAPP
   ===================================================================== */
.whatsapp-float{
    position:fixed;
    right:18px;
    bottom:18px;
    z-index:1200;
    display:flex;
    align-items:center;
    gap:10px;
    opacity:0;
    transform:scale(.6) translateY(20px);
    transition:opacity .5s cubic-bezier(.34,1.56,.64,1), transform .5s cubic-bezier(.34,1.56,.64,1);
}
.whatsapp-float.wa-visible{
    opacity:1;
    transform:scale(1) translateY(0);
}
@media (prefers-reduced-motion: reduce){
    .whatsapp-float{transition:opacity .3s ease;transform:none;}
}

.whatsapp-btn{
    position:relative;
    width:58px;height:58px;
    border-radius:50%;
    background:#25D366;
    display:flex;
    align-items:center;
    justify-content:center;
    box-shadow:0 10px 26px -6px rgba(37,211,102,.55);
    flex-shrink:0;
    transition:transform .35s var(--ease-smooth);
}
/* Halo de luz intermitente: un resplandor difuso detrás del botón que se
   enciende y se apaga despacio (sólo opacity, sin escalar), en vez del
   anillo que se expande hacia afuera. z-index:-1 lo deja detrás del fondo
   verde del botón (.whatsapp-float, con transform, es el contexto de
   apilamiento), así sólo se ve la luz que desborda los bordes. */
.whatsapp-btn::before{
    content:'';
    position:absolute;
    inset:-7px;
    border-radius:50%;
    background:radial-gradient(circle, rgba(37,211,102,.75) 0%, rgba(37,211,102,.35) 45%, rgba(37,211,102,0) 72%);
    filter:blur(8px);
    z-index:-1;
    opacity:.3;
    animation:wappGlow 3.2s ease-in-out infinite;
    pointer-events:none;
}
.whatsapp-btn:hover{transform:scale(1.06);}
.whatsapp-btn:hover::before{animation:none;opacity:1;transition:opacity .3s ease;}
.whatsapp-btn svg{width:30px;height:30px;}

@keyframes wappGlow{
    0%,100%{opacity:.3;}
    50%{opacity:.95;}
}

@media (prefers-reduced-motion: reduce){
    .whatsapp-btn::before{animation:none;opacity:.6;}
}

@media (min-width:768px){
    .whatsapp-float{right:28px;bottom:28px;}
}

/* =====================================================================
   BOTÓN VOLVER ARRIBA — esquina inferior izquierda, sutil, sobre el
   asistente IA. Aparece recién al bajar un poco la página.
   ===================================================================== */
.back-to-top{
    position:fixed;
    left:18px;
    bottom:100px;
    z-index:1150;
    width:48px;height:48px;
    border-radius:50%;
    border:1px solid var(--border-soft);
    background:rgba(255,255,255,.05);
    backdrop-filter:blur(10px);
    -webkit-backdrop-filter:blur(10px);
    color:var(--white);
    display:flex;
    align-items:center;
    justify-content:center;
    cursor:pointer;
    opacity:0;
    visibility:hidden;
    pointer-events:none;
    transform:translateY(10px);
    transition:opacity .35s ease, transform .35s ease, visibility .35s, background .25s ease, border-color .25s ease;
}
.back-to-top svg{width:20px;height:20px;}
.back-to-top.show{
    opacity:1;
    visibility:visible;
    pointer-events:auto;
    transform:translateY(0);
}
.back-to-top:hover{
    background:var(--accent);
    border-color:var(--accent);
    transform:translateY(-2px);
}
@media (prefers-reduced-motion: reduce){
    .back-to-top{transition:opacity .25s ease, visibility .25s;}
    .back-to-top.show{transform:none;}
    .back-to-top:hover{transform:none;}
}
@media (min-width:768px){
    .back-to-top{left:28px;bottom:112px;}
}

/* =====================================================================
   16. BREAKPOINTS — mobile-first: agregamos complejidad hacia arriba
   ===================================================================== */

/* ---------- Tablet: 768px+ ---------- */
@media (min-width:768px){
    :root{
        --section-pad:96px;
        --container-pad:32px;
    }

    .nav-links{
        position:static;
        display:flex;
        flex-direction:row;
        align-items:center;
        gap:32px;
        background:transparent;
        backdrop-filter:none;
        padding:0;
        width:auto;
        overflow:visible;
        /* En desktop el menú es inline y siempre visible: anulamos los
           estados de animación de apertura/cierre usados en mobile */
        opacity:1;
        visibility:visible;
        transform:none;
        pointer-events:auto;
        transition:none;
    }
    /* Sin cascada de entrada en desktop: los links están siempre a la vista */
    .nav-links > *{opacity:1;transform:none;transition-delay:0s !important;}
    .nav-links a:not(.btn){
        padding:0;
        border-bottom:none;
        font-size:.95rem;
        position:relative;
    }
    /* Subrayado que crece desde la izquierda al pasar el mouse (y queda
       fijo en el link activo). Sólo desktop: en mobile los links ya
       tienen su propio separador de lista. */
    .nav-links a:not(.btn)::after{
        content:'';
        position:absolute;
        left:0;right:0;
        bottom:-6px;
        height:1.5px;
        border-radius:2px;
        background:linear-gradient(90deg,var(--accent),var(--wave-teal));
        transform:scaleX(0);
        transform-origin:right;
        transition:transform .45s var(--ease-smooth);
    }
    .nav-links a:not(.btn):hover::after,
    .nav-links a:not(.btn).active::after{transform:scaleX(1);transform-origin:left;}
    /* El link "Portfolio Animado" ya trae su propio subrayado animado */
    .nav-links a.nav-link-shine::after{display:none;}
    /* Header más compacto una vez que se bajó del hero */
    header.scrolled .nav-wrap{height:66px;}
    .nav-links .btn{margin-top:0;}
    .nav-links .btn + .nav-link-shine{margin-top:0;}
    .hamburger{display:none;}
    .nav-wrap{height:78px;}
    .logo{margin-left:10px;}
    .logo-icon{height:44px;}
    .lang-switch{
        /* Desktop/tablet: vuelve al flujo normal, pegado a los botones del nav */
        position:relative;
        left:auto;
        top:auto;
        transform:none;
        margin-left:10px;
    }

    .hero{padding:150px 0 0;min-height:100vh;}
    .hero-cta{flex-direction:row;justify-content:center;flex-wrap:wrap;}
    .hero-cta .btn{width:auto;}
    .hero-stats{gap:40px;}
    .stat-number{font-size:2.3rem;}
    .stat-label{font-size:.8rem;}
    .particle-bg{height:300px;}

    .servicios-grid{grid-template-columns:repeat(2,1fr);gap:22px;}
    .portfolio-grid{grid-template-columns:repeat(2,1fr);}
    .portfolio-header-row{flex-direction:row;justify-content:space-between;align-items:flex-end;}
    .portfolio-header-row .section-header{margin-bottom:0;}
    .portfolio-filters-col{align-items:flex-end;}
    .portfolio-filters-col .btn-glow{align-self:auto;margin-bottom:0;}

    .porque-grid{grid-template-columns:0.9fr 1.1fr;gap:56px;align-items:center;}
    .cta-grid{grid-template-columns:1fr;gap:50px;}
    .form-row{grid-template-columns:1fr 1fr;}
    .footer-grid{grid-template-columns:1fr 1fr;}
    .footer-grid > .footer-brand{grid-column:1 / -1;}
    .footer-bottom{flex-direction:row;justify-content:space-between;}
    .footer-brand{text-align:left;}
    .footer-brand .logo{justify-content:flex-start;margin-left:6px;}
    .footer-brand .logo-icon{height:60px;}
    .footer-brand p{margin:16px 0 20px;}
    .footer-social{justify-content:flex-start;}
}

/* ---------- Desktop: 1024px+ ---------- */
@media (min-width:1024px){
    :root{--section-pad:130px;}

    .logo{margin-left:16px;}
    .logo-icon{height:50px;}

    .hero-cta{margin-bottom:90px;}
    .servicios-grid{grid-template-columns:repeat(3,1fr);}
    .portfolio-grid{grid-template-columns:repeat(3,1fr);}
    .portfolio-card{min-height:400px;}
    .cta-grid{grid-template-columns:0.85fr 1.15fr;align-items:flex-start;}
    .footer-grid{grid-template-columns:1.7fr 1fr 1.1fr;}
    .footer-grid > .footer-brand{grid-column:auto;}
    .footer-brand .logo-icon{height:68px;}
    .form-card{padding:40px;}
    .modal-cal-cta{padding:56px 40px 24px;}
    .service-card{padding:36px;height:325px;}
    .service-head{left:36px;right:36px;}
    .service-card:hover .service-head,
    .service-card.is-open .service-head{top:36px;}
    .service-card p{left:36px;right:36px;bottom:36px;}
}

/* ---------- Nav compacto: 768px–1099px ----------
   Con 5 links de texto + 2 botones, el nav inline no entra en anchos
   intermedios (antes ya desbordaba a ~800px y aplastaba el logo a 0px).
   Acá se achican gaps, texto y botones, y se oculta "Inicio" (el primer
   link en todas las páginas): el logo ya lleva al inicio. */
@media (min-width:768px) and (max-width:1099px){
    .nav-links{gap:14px;}
    .nav-links a:not(.btn){font-size:.84rem;}
    .nav-links > a:first-child{display:none;}
    .nav-links .btn-sm{padding:9px 14px;font-size:.76rem;}
    .logo{margin-left:0;flex-shrink:0;}
    .lang-switch{margin-left:6px;}
}

/* ---------- XL: 1280px+ (afinado tipográfico) ---------- */
@media (min-width:1280px){
    .hero h1{font-size:4.8rem;}
}

/* =====================================================================
   17. LENGUAJE DE MOVIMIENTO COMPARTIDO
   ===================================================================== */

/* Transición entre páginas (index / sobre-mi / portfolio-animado): en vez
   del "flash" blanco-negro del cambio de documento, un crossfade corto del
   root. View Transitions cross-document: cero JS, sólo aplica cuando
   ambas páginas lo declaran (las 3 cargan este archivo) y los navegadores
   sin soporte simplemente navegan como siempre. */
@view-transition{navigation:auto;}
::view-transition-old(root){animation-duration:.28s;animation-timing-function:ease-out;}
::view-transition-new(root){animation-duration:.38s;animation-timing-function:ease-in;}
@media (prefers-reduced-motion: reduce){
    @view-transition{navigation:none;}
}

/* =====================================================================
   18. BANNER / PANEL DE CONSENTIMIENTO DE COOKIES
   Lo inyecta assets/js/consent.js en todas las páginas. Tarjeta flotante
   (no barra a todo el ancho) que entra desde abajo pasando de desenfocada
   a nítida. "Rechazar" y "Aceptar" tienen el mismo tamaño y peso a
   propósito: las guías europeas (EDPB/AEPD) exigen que rechazar sea tan
   fácil y visible como aceptar.
   ===================================================================== */
.cc{
    position:fixed;
    left:12px;right:12px;bottom:12px;
    z-index:1400;
    pointer-events:none;
    visibility:hidden;
}
.cc.is-visible{visibility:visible;pointer-events:auto;}
.cc.is-leaving{visibility:visible;animation:ccHideLater 0s linear .6s forwards;}
@keyframes ccHideLater{to{visibility:hidden;}}
.cc-card{
    position:relative;
    overflow:hidden;
    max-width:440px;
    margin:0 auto;
    padding:22px 22px 18px;
    border-radius:var(--radius-lg);
    background:rgba(18,18,22,.88);
    backdrop-filter:blur(18px) saturate(140%);
    -webkit-backdrop-filter:blur(18px) saturate(140%);
    border:1px solid var(--border-soft-hover);
    box-shadow:0 30px 60px -20px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.06);
    opacity:0;
    transform:translateY(28px) scale(.97);
    filter:blur(6px);
    transition:opacity .5s var(--ease-out-expo), transform .7s var(--ease-out-expo), filter .5s ease;
}
.cc.is-visible .cc-card{opacity:1;transform:none;filter:none;}
/* Filo de luz que recorre el borde de la tarjeta una sola vez al aparecer */
.cc-card::before{
    content:'';
    position:absolute;inset:0;
    border-radius:inherit;
    padding:1px;
    background:linear-gradient(120deg, transparent 20%, var(--accent-hot) 45%, var(--wave-teal) 55%, transparent 80%) 0 0 / 250% 100%;
    -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite:xor;
    mask-composite:exclude;
    opacity:0;
    pointer-events:none;
}
.cc.is-visible .cc-card::before{animation:ccBorderSweep 1.8s var(--ease-smooth) .35s both;}
@keyframes ccBorderSweep{
    0%{opacity:0;background-position:110% 0;}
    20%{opacity:1;}
    100%{opacity:0;background-position:-40% 0;}
}
.cc-glow{
    position:absolute;
    width:220px;height:220px;
    top:-120px;right:-80px;
    border-radius:50%;
    background:radial-gradient(circle, rgba(47,111,237,.35), transparent 70%);
    filter:blur(10px);
    pointer-events:none;
}
.cc-head{display:flex;align-items:center;gap:12px;margin-bottom:10px;position:relative;}
.cc-icon{
    width:40px;height:40px;
    border-radius:12px;
    display:grid;place-items:center;
    color:var(--accent-hot);
    background:rgba(47,111,237,.12);
    border:1px solid rgba(91,141,255,.25);
    flex-shrink:0;
}
.cc-icon svg{width:24px;height:24px;overflow:visible;}
/* La galletita se "dibuja" y después caen las chispas */
.cc-icon-body{stroke-dasharray:90;stroke-dashoffset:90;}
.cc.is-visible .cc-icon-body{animation:ccDraw 1.2s var(--ease-out-expo) .25s forwards;}
.cc-chip{opacity:0;transform-box:fill-box;transform-origin:center;transform:scale(0);}
.cc.is-visible .cc-chip{animation:ccChip .5s cubic-bezier(.34,1.56,.64,1) forwards;}
.cc.is-visible .cc-chip:nth-of-type(1){animation-delay:.75s;}
.cc.is-visible .cc-chip:nth-of-type(2){animation-delay:.87s;}
.cc.is-visible .cc-chip:nth-of-type(3){animation-delay:.99s;}
@keyframes ccDraw{to{stroke-dashoffset:0;}}
@keyframes ccChip{to{opacity:1;transform:scale(1);}}
.cc-title{font-size:1.02rem;letter-spacing:-.01em;}
.cc-desc{font-size:.84rem;line-height:1.65;position:relative;}
.cc-desc a{color:var(--white);text-decoration:underline;text-decoration-color:rgba(91,141,255,.6);text-underline-offset:3px;}
.cc-desc a:hover{color:var(--accent-hot);}

.cc-prefs{display:grid;gap:8px;margin-top:14px;position:relative;}
.cc-prefs[hidden]{display:none;}
.cc.is-config .cc-prefs{animation:ccPrefsIn .5s var(--ease-out-expo);}
@keyframes ccPrefsIn{from{opacity:0;transform:translateY(-6px);}to{opacity:1;transform:none;}}
.cc-pref{
    display:flex;align-items:center;justify-content:space-between;gap:14px;
    padding:12px 14px;
    border-radius:var(--radius-md);
    background:rgba(255,255,255,.03);
    border:1px solid var(--border-soft);
    cursor:pointer;
    transition:border-color .3s ease, background .3s ease;
}
.cc-pref:hover{border-color:var(--border-soft-hover);}
.cc-pref strong{display:block;font-size:.84rem;margin-bottom:2px;}
.cc-pref div > span{font-size:.74rem;color:var(--gray-mid);line-height:1.5;display:block;}

/* Interruptor (lo usa también el panel embebido de /cookies) */
.cc-switch{position:relative;width:44px;height:26px;flex-shrink:0;display:inline-block;}
.cc-switch input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer;z-index:1;}
.cc-switch > span{
    position:absolute;inset:0;
    border-radius:50px;
    background:rgba(255,255,255,.12);
    border:1px solid var(--border-soft-hover);
    transition:background .35s var(--ease-smooth), border-color .35s ease;
}
.cc-switch > span::after{
    content:'';
    position:absolute;
    top:3px;left:3px;
    width:18px;height:18px;
    border-radius:50%;
    background:var(--white);
    box-shadow:0 2px 6px rgba(0,0,0,.4);
    transition:transform .45s cubic-bezier(.34,1.56,.64,1);
}
.cc-switch input:checked + span,
.cc-switch.is-locked > span{background:linear-gradient(135deg,var(--accent),var(--wave-teal));border-color:transparent;}
.cc-switch input:checked + span::after,
.cc-switch.is-locked > span::after{transform:translateX(18px);}
.cc-switch.is-locked{opacity:.55;}
.cc-switch input:focus-visible + span{outline:2px solid var(--accent-hot);outline-offset:3px;}

.cc-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:16px;position:relative;}
.cc-actions [data-cc="config"],
.cc-actions [data-cc="save"]{grid-column:1 / -1;order:3;}
.cc-btn{
    appearance:none;
    -webkit-appearance:none;
    padding:12px 16px;
    border-radius:50px;
    font-family:var(--font-sans);
    font-weight:700;
    font-size:.84rem;
    cursor:pointer;
    border:1px solid var(--border-soft-hover);
    color:var(--white);
    background:rgba(255,255,255,.04);
    white-space:nowrap;
    transition:background .3s ease, border-color .3s ease, box-shadow .3s ease, color .3s ease;
}
.cc-btn:hover{border-color:var(--accent-hot);box-shadow:0 0 0 4px rgba(91,141,255,.12);}
.cc-btn:active{transform:scale(.97);transition-duration:.12s;}
.cc-btn:focus-visible{outline:2px solid var(--accent-hot);outline-offset:3px;}
.cc-btn-solid{
    background:linear-gradient(135deg,var(--accent),var(--accent-hot));
    border-color:transparent;
    box-shadow:0 0 0 0 rgba(91,141,255,0), 0 10px 24px -10px var(--accent-glow);
}
.cc-btn-solid:hover{border-color:transparent;box-shadow:0 0 0 4px rgba(91,141,255,.16), 0 12px 28px -10px rgba(47,111,237,.65);}
.cc-actions [data-cc="config"]{border-color:transparent;background:none;color:var(--gray-soft);padding:8px;}
.cc-actions [data-cc="config"]:hover{color:var(--accent-hot);box-shadow:none;}
.cc-actions [data-cc="save"]{border-color:rgba(91,141,255,.45);color:var(--accent-hot);}

@media (min-width:768px){
    .cc{left:28px;right:auto;bottom:28px;}
    .cc-card{width:440px;margin:0;padding:24px 24px 20px;}
    .cc-actions{grid-template-columns:auto 1fr 1fr;}
    .cc-actions [data-cc="config"],
    .cc-actions [data-cc="save"]{grid-column:auto;order:0;}
    .cc.is-config .cc-actions{grid-template-columns:1fr 1fr;}
    .cc.is-config .cc-actions [data-cc="save"]{grid-column:1 / -1;order:3;}
}
@media (prefers-reduced-motion: reduce){
    .cc-card{transition:opacity .2s ease;transform:none;filter:none;}
    .cc.is-visible .cc-card::before{animation:none;}
    .cc-icon-body{stroke-dashoffset:0;animation:none !important;}
    .cc-chip{opacity:1;transform:none;animation:none !important;}
    .cc.is-config .cc-prefs{animation:none;}
}
