/* ==========================================================================
   Luis Oses — versión "cosmic"

   Mismo contenido que el sitio principal, otro registro visual: oscuro fijo,
   pastilla flotante, grilla bento y azul frío en vez de brasa naranja.

   El tema oscuro aquí NO es una preferencia: es el diseño. No hay conmutador,
   así que tampoco hay dos paletas que puedan desincronizarse.

   Orden del archivo:
     1. Tipografías autoalojadas
     2. Fichas (tokens)
     3. Base y utilidades
     4. Pantalla de carga
     5. Capa WebGL
     6. Navegación
     7. Portada
     8. Encabezados de sección y botones
     9. Bento de proyectos
    10. Índice en lista
    11. Enfoque con paralaje
    12. Cifras
    13. Trayectoria
    14. Contacto y pie
    15. Movimiento reducido
   ========================================================================== */

/* ═══════════════════════════ 1. TIPOGRAFÍAS ═══════════════════════════ */
/* Instrument Serif e IBM Plex Mono viajan con el sitio, autoalojadas. Inter
   se retiró: era una hoja de estilo de Google Fonts que bloqueaba el render
   para el texto de apoyo. Ese papel lo hace ahora la pila del sistema. */

@font-face {
  font-family: 'Instrument Serif';
  src: url('fonts/instrument-serif-400i-latin.woff2') format('woff2');
  font-weight: 400; font-style: italic; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+2122, U+2191, U+2193, U+2197;
}
@font-face {
  font-family: 'Instrument Serif';
  src: url('fonts/instrument-serif-400i-latin-ext.woff2') format('woff2');
  font-weight: 400; font-style: italic; font-display: swap;
  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB;
}
@font-face {
  font-family: 'Instrument Serif';
  src: url('fonts/instrument-serif-400-latin.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+2000-206F;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+2000-206F, U+2191, U+2193, U+2197;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
  unicode-range: U+0100-024F, U+1E00-1EFF;
}

/* IBM Plex Sans: titulares de seccion y cuerpo. Sustituye a system-ui, que
   cambiaba de forma segun el sistema operativo del visitante. Es hermana de
   la mono que ya se usa, asi que la pareja es coherente por construccion.
   Solo dos pesos, 400 y 600: cada subset latino pesa 44 KB y un tercer peso
   no justificaba el coste. */
@font-face{font-family:'IBM Plex Sans';src:url('fonts/ibm-plex-sans-400-latin-ext.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap;unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'IBM Plex Sans';src:url('fonts/ibm-plex-sans-400-latin.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap;unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'IBM Plex Sans';src:url('fonts/ibm-plex-sans-600-latin-ext.woff2') format('woff2');font-weight:600;font-style:normal;font-display:swap;unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'IBM Plex Sans';src:url('fonts/ibm-plex-sans-600-latin.woff2') format('woff2');font-weight:600;font-style:normal;font-display:swap;unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}

/* ═════════════════════════════ 2. FICHAS ═════════════════════════════ */

:root {
  --bg:        hsl(0 0% 4%);
  --superficie:hsl(0 0% 8%);
  --texto:     hsl(0 0% 96%);
  --apagado:   hsl(0 0% 53%);
  --trazo:     hsl(0 0% 12%);

  /* El degradado de acento del template. gl.js lee --acento y --acento-claro
     por getComputedStyle, así que la capa WebGL se tiñe sola con estos dos:
     no hay ningún color repetido dentro del shader. */
  --acento:       #4E85BF;
  --acento-claro: #89AACC;
  --degradado: linear-gradient(90deg, var(--acento-claro) 0%, var(--acento) 100%);

  /* Sin Inter: venía de Google Fonts, que es una hoja de estilo de terceros
     que bloquea el render. El carácter tipográfico lo ponen la serif de
     display y la monoespaciada, ambas autoalojadas; para el texto de apoyo
     la pila del sistema se ve prácticamente igual y no cuesta ni un byte. */
  --fuente-cuerpo: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --fuente-titulo: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  --fuente-mono:   'IBM Plex Mono', ui-monospace, 'SF Mono', Consolas, monospace;

  --ancho: 1200px;
  --lado: clamp(1.25rem, 5vw, 4rem);
  --radio: 24px;

  color-scheme: dark;
}

/* ══════════════════════ 3. BASE Y UTILIDADES ══════════════════════ */

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

html {
  scroll-behavior: smooth;
  /* La pastilla flota arriba: sin este margen, un salto de ancla deja el
     título justo debajo de ella y parece cortado. */
  scroll-padding-top: 6.5rem;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--texto);
  font-family: var(--fuente-cuerpo);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Mientras la pantalla de carga está puesta no se puede hacer scroll: si se
   pudiera, la portada llegaría ya recorrida y la animación de entrada se
   ejecutaría fuera de la vista. */
body.esta-cargando { overflow: hidden; }

img { max-width: 100%; display: block; }

a { color: inherit; text-decoration: none; }

h1, h2, h3 { margin: 0; font-weight: 400; letter-spacing: -0.02em; }

p { margin: 0 0 1rem; }

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

:focus-visible {
  outline: 2px solid var(--acento-claro);
  outline-offset: 3px;
  border-radius: 4px;
}

.saltar {
  position: absolute; left: -9999px; top: 0; z-index: 10000;
  background: var(--texto); color: var(--bg);
  padding: .75rem 1.25rem; border-radius: 0 0 12px 0;
}
.saltar:focus { left: 0; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Entrada al hacer scroll. El estado inicial vive aquí y no en JS para que,
   si el script falla, el salvavidas de app.js solo tenga que poner el
   atributo — y si ni eso corre, el contenido igual queda legible. */
[data-entra] {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity .9s cubic-bezier(.25,.1,.25,1),
              transform .9s cubic-bezier(.25,.1,.25,1);
}
[data-entra][data-dentro] { opacity: 1; transform: none; }

/* ═══════════════════════ 4. PANTALLA DE CARGA ═══════════════════════ */

.carga {
  position: fixed; inset: 0; z-index: 9999;
  background: var(--bg);
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: var(--lado);
  transition: opacity .6s ease, visibility .6s ease;
}
.carga[data-fuera] { opacity: 0; visibility: hidden; }

.carga__etiqueta {
  font-family: var(--fuente-mono);
  font-size: .7rem; text-transform: uppercase; letter-spacing: .3em;
  color: var(--apagado);
  animation: carga-baja .8s cubic-bezier(.25,.1,.25,1) both;
}

.carga__centro { display: grid; place-items: center; }

.carga__palabra {
  font-family: var(--fuente-titulo);
  font-style: italic;
  font-size: clamp(2.5rem, 8vw, 5rem);
  color: hsl(0 0% 96% / .8);
  line-height: 1;
}
/* La clave del <span> cambia en JS; esta animación se reinicia sola porque
   el script quita y vuelve a poner el atributo. */
.carga__palabra[data-anima] { animation: carga-palabra .5s ease-out both; }

.carga__cuenta {
  justify-self: end;
  font-family: var(--fuente-titulo);
  font-size: clamp(3.5rem, 13vw, 8rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  /* tabular-nums evita que el ancho baile al pasar de 111 a 100. */
}

.carga__barra {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 3px; background: hsl(0 0% 12% / .5);
}
.carga__barra i {
  display: block; height: 100%;
  background: var(--degradado);
  transform: scaleX(0); transform-origin: left;
  box-shadow: 0 0 8px hsl(0 0% 100% / .18);
}

@keyframes carga-baja  { from { opacity: 0; transform: translateY(-20px); } }
@keyframes carga-palabra {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}

/* ══════════════════════════ 5. CAPA WEBGL ══════════════════════════ */
/* Altura reservada en CSS: la capa existe con su tamaño final antes de que
   Three.js llegue, así que su aparición no puede mover nada (CLS = 0). */

.brasas {
  position: fixed; inset: 0;
  height: 100vh; height: 100svh;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.4s ease;
}
.brasas[data-listo] { opacity: .85; }

/* Halo frío detrás de todo: el sitio no se ve plano aunque WebGL no arranque. */
body::before {
  content: '';
  position: fixed; inset: 0; z-index: -1;
  background:
    radial-gradient(70% 55% at 50% 0%, hsl(211 40% 40% / .22), transparent 70%),
    radial-gradient(50% 40% at 85% 75%, hsl(211 45% 45% / .10), transparent 70%);
  pointer-events: none;
}

/* ═════════════════════════ 6. NAVEGACIÓN ═════════════════════════ */

.nav {
  /* Por encima del panel del menú (9000): el botón de cerrar vive en la
     pastilla, y si quedara por debajo, en un móvil sin tecla Escape la
     única forma de cerrar sería tocar un enlace. */
  position: fixed; top: 0; left: 0; right: 0; z-index: 9500;
  display: flex; justify-content: center;
  padding: 1rem var(--lado);
  pointer-events: none;
}

.pastilla {
  pointer-events: auto;
  display: inline-flex; align-items: center; gap: .25rem;
  /* Mas aire a los lados que arriba y abajo: con .5rem parejo, la firma y el
     boton de «Escribeme» quedaban pegados al borde curvo. En una pildora el
     radio se come el espacio de las esquinas, asi que los extremos necesitan
     mas margen que el centro para verse igual de holgados. */
  padding: .5rem 1.15rem;
  border-radius: 999px;
  border: 1px solid hsl(0 0% 100% / .1);
  background: hsl(0 0% 8% / .72);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  transition: box-shadow .3s ease, background .3s ease;
}
.pastilla[data-flota] {
  box-shadow: 0 10px 30px hsl(0 0% 0% / .45);
  background: hsl(0 0% 8% / .9);
}

.pastilla__sep { width: 1px; height: 20px; background: var(--trazo); margin: 0 .25rem; }


/* La firma sustituye a la foto y al círculo. Va inlineada y con
   currentColor: hereda el blanco de la barra y serviría igual sobre fondo
   claro, sin duplicar el archivo en dos colores. */
.marca__logo {
  display: block;
  /* El trazo es más alto que ancho (333,74 x 382): se fija el ALTO y el ancho
     lo deduce el viewBox. Fijando el ancho quedaría distinto en cada sitio
     donde se use. */
  height: 26px;
  width: auto;
  fill: currentColor;
  color: var(--texto);
  transition: opacity .3s ease, transform .3s ease;
}
.marca:hover .marca__logo { opacity: .75; transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
  .marca:hover .marca__logo { transform: none; }
}

.pastilla__nav { display: flex; gap: .125rem; }
.pastilla__nav a {
  font-size: .8125rem;
  padding: .5rem .9rem;
  border-radius: 999px;
  color: var(--apagado);
  transition: color .25s ease, background .25s ease;
  white-space: nowrap;
}
.pastilla__nav a:hover { color: var(--texto); background: hsl(0 0% 12% / .6); }
.pastilla__nav a.es-activo { color: var(--texto); background: hsl(0 0% 12% / .8); }

/* El aro de degradado vive en un span aparte, un par de píxeles por fuera:
   así el borde parece del degradado sin tener que pintar el borde con
   background-clip, que rompe el redondeo en varios navegadores. */

/* En móvil la pastilla no cabe con todo: se queda la marca y el CTA, que es
   lo único accionable de verdad ahí. */
@media (max-width: 720px) {
  .pastilla__nav, .pastilla__sep { display: none; }
  .pastilla { gap: .75rem; }
}

/* ══════════════════════════ 7. PORTADA ══════════════════════════ */

/* Portada a dos mitades: texto a la izquierda sobre negro, fotografía del
   puesto de trabajo entrando por la derecha. La foto no es decoración — se
   ven Penpot y el editor de código a la vez, que es literalmente lo que dice
   el titular. */

.portada {
  position: relative; z-index: 1;
  min-height: 100vh; min-height: 100svh;
  display: grid;
  align-content: center;
  padding: 9rem var(--lado) 7rem;
  text-align: left;
  overflow: hidden;
}

/* --- Fondo fotográfico ------------------------------------------------
   Se ancla a la derecha y se desvanece hacia la izquierda con una máscara,
   para que el texto caiga siempre sobre negro puro y no sobre la foto. Se
   usa mask-image y no un degradado encima: un velo superpuesto apagaría
   también los brillos de las pantallas, que son lo único que da vida a la
   imagen. */
.portada__fondo {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: min(72%, 1250px);
  z-index: 0;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, transparent 6%, #000 46%, #000 100%);
          mask-image: linear-gradient(to right, transparent 0%, transparent 6%, #000 46%, #000 100%);
}
.portada__fondo img {
  width: 100%; height: 100%;
  /* height:auto anularía el 100%: aquí la imagen debe llenar la caja, así que
     el tamaño lo fija object-fit y los atributos width/height solo sirven
     para reservar la proporción antes de que descargue. */
  object-fit: cover;
  /* MEDIDO sobre el original: el centro del rostro está en x=67,9%,
     y=24,4% de la imagen. Con object-fit:cover, object-position hace
     coincidir ESE punto de la imagen con el mismo punto del contenedor,
     así que fijándolo ahí la cara sobrevive a cualquier recorte, sea
     cual sea la proporción de la ventana. Antes decía `center right`
     (=100% 50%): anclaba el borde derecho y centraba en vertical, que
     es justo por qué se cortaba la cabeza.
     El 55% en X (y no el 67,9% del rostro) está calculado: la pastilla va
     centrada arriba y con 68% su borde derecho pisaba la cabeza entre 1024 y
     1440 px. Bajar la X corre la cara hacia la derecha y la libera, sin
     sacarla del cuadro. */
  object-position: 55% 24%;
  opacity: .92;
}

/* Degradado al pie: la portada se funde con la sección siguiente en vez de
   terminar con un corte recto. Va por encima de la foto. */
.portada::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0; height: 14rem;
  background: linear-gradient(to top, var(--bg), transparent);
  pointer-events: none;
  z-index: 1;
}

.portada__centro {
  position: relative; z-index: 2;
  max-width: 40rem;
}

/* Filete vertical a la izquierda, como en el boceto: ancla la columna de
   texto y le da un borde al bloque sin necesidad de una caja. */
.portada__centro::before {
  content: '';
  position: absolute;
  left: -1.75rem; top: .4rem; bottom: .4rem;
  width: 1px;
  background: linear-gradient(to bottom, var(--trazo), transparent);
}

.portada__ceja {
  display: flex; align-items: center; gap: 1.25rem;
  font-family: var(--fuente-mono);
  font-size: .7rem; text-transform: uppercase; letter-spacing: .3em;
  color: var(--apagado);
  margin-bottom: 1.75rem;
}
.portada__ceja i {
  flex: 0 0 3.5rem; height: 1px;
  background: var(--trazo);
}

.portada__nombre {
  font-family: var(--fuente-titulo);
  font-style: normal;
  font-size: clamp(3.25rem, 9.5vw, 7rem);
  line-height: .95;
  letter-spacing: -0.015em;
  margin-bottom: .25rem;
}

.portada__claim {
  font-family: var(--fuente-titulo);
  /* Más contenido que en el boceto (dos líneas de frase, no dos palabras),
     así que la escala baja para que no invada el marcador de cifras. */
  font-size: clamp(1.55rem, 4.1vw, 2.85rem);
  line-height: 1.12;
  letter-spacing: -0.015em;
  color: var(--texto);
  margin-bottom: 1.75rem;
  /* Reparte las lineas en vez de llenar la primera y dejar huerfana la
     ultima. Medido antes: la ultima linea ocupaba el 24% del ancho y
     «mantiene.» colgaba sola en todos los tamanos de escritorio. */
  max-width: 22ch;
}
/* El rol va en su PROPIA linea. La reserva de ancho (min-width) que evita el
   salto de layout al rotar la palabra deja un hueco del tamano de la palabra
   mas larga; al final de la linea es invisible, pero a mitad de frase se leia
   como un error de maquetacion. */
.claim__rol { display: block; }
/* Solo la cola se equilibra: asi no queda una ultima linea huerfana. */
.claim__cola { display: block; text-wrap: balance; }

.portada__claim em {
  font-style: italic;
  /* El degradado de acento del sitio, recortado sobre el texto. */
  background: var(--degradado);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;

  /* La palabra ROTA cada 2 s (Diseñador / Frontend / Full-stack / Ingeniero).
     Sin un ancho reservado, cada cambio refluiría la línea y generaría un
     salto de layout cada dos segundos — CLS acumulándose toda la visita, sin
     que ninguna interacción lo justifique. El ancho lo fija la palabra más
     larga, «Full-stack». */
  display: inline-block;
  /* Medido: la palabra mas ancha es «Diseñador» (164 px), no «Full-stack»
     (160 px) — la eñe y el acento pesan mas que un guion. 8ch cubre las
     cuatro. En ch y no en px porque el tamaño de la frase es un clamp()
     y la reserva tiene que escalar con el. */
  min-width: 8ch;
  animation: rol-entra .4s ease-out;
}
@keyframes rol-entra {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.portada__lede {
  max-width: 30rem; margin: 0 0 2.5rem;
  font-size: .95rem; color: var(--apagado);
}

.portada__botones { display: inline-flex; flex-wrap: wrap; gap: 1rem; }

/* --- Enlace con flecha (CTA de la portada) --- */
.enlace-flecha {
  display: inline-flex; align-items: center; gap: 1rem;
  padding-bottom: .7rem;
  border-bottom: 1px solid var(--trazo);
  font-family: var(--fuente-mono);
  font-size: .78rem; text-transform: uppercase; letter-spacing: .25em;
  transition: border-color .35s ease, color .35s ease;
}
.enlace-flecha span { transition: transform .45s cubic-bezier(.16,1,.3,1); }
.enlace-flecha:hover { border-color: var(--acento-claro); }
.enlace-flecha:hover span { transform: translateX(7px); }

/* --- Marcador de cifras --- */
.marcador {
  display: flex; flex-wrap: wrap; gap: 2.75rem;
  list-style: none; margin: 3.5rem 0 0; padding: 0;
}
.marcador li { display: grid; gap: .45rem; }
.marcador b {
  font-family: var(--fuente-titulo);
  font-size: clamp(1.6rem, 3.4vw, 2.35rem);
  font-weight: 400; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.marcador span {
  font-family: var(--fuente-mono);
  font-size: .62rem; text-transform: uppercase; letter-spacing: .18em;
  line-height: 1.6; color: var(--apagado);
}

/* --- Pie de la portada --- */
.portada__pieton {
  position: absolute; left: var(--lado); bottom: 2.25rem; z-index: 2;
  margin: 0;
  font-family: var(--fuente-mono);
  font-size: .62rem; text-transform: uppercase; letter-spacing: .22em;
  color: var(--apagado);
}

/* 1150 y no 900: entre 950 y 1100 px NO existe ningún object-position que
   libere la pastilla sin cortar la cabeza (el contenedor es demasiado
   estrecho para las dos condiciones a la vez). Esas anchuras usan el
   diseño apilado, que no tiene el problema. */
@media (max-width: 1150px) {
  /* En móvil la portada deja de ser dos columnas y pasa a ser una banda de
     foto arriba con el texto debajo. Como telón de fondo no funcionaba: a
     este ancho la cara caía justo detrás del titular, y un retrato con el
     nombre cruzado por los ojos parece un accidente, no una decisión.
     Aquí la foto se ve entera y el texto va sobre negro limpio. */
  .portada {
    padding-top: calc(40svh + 4.5rem);
    align-content: start;
  }
  .portada__fondo {
    width: 100%;
    /* La banda arranca POR DEBAJO de la pastilla, no en el borde superior.
       Medido: la pastilla acaba en y=74 y la cabeza empezaba en y=17, o sea
       57 px de cara tapados por el menú.
       Y no se arregla con object-position: a 390x388 la proporción del
       contenedor (1,005) es menor que la de la imagen (1,777), así que cover
       escala por ALTURA y la foto se ve entera en vertical. Sin recorte
       vertical, el eje Y de object-position no tiene ningún efecto. Lo único
       que mueve la cara hacia abajo es bajar la banda entera. */
    top: 5rem;
    height: 44svh;
    bottom: auto;
    /* Se funde hacia abajo, no hacia la izquierda. */
    -webkit-mask-image: linear-gradient(to bottom, #000 46%, transparent 96%);
            mask-image: linear-gradient(to bottom, #000 46%, transparent 96%);
  }
  .portada__fondo img {
    opacity: 1;
    /* La Y baja a 8%, y esto SÍ depende del layout.
       En un móvil estrecho la banda es casi cuadrada, cover escala por
       anchura... perdón, por ALTURA, y no hay recorte vertical: la Y da
       igual. Pero a 1024x768 la misma banda queda ancha y baja (1024x338),
       cover pasa a escalar por ANCHURA y aparecen 238 px de recorte
       vertical. Con 24% ese recorte se comía la parte alta de la cabeza.
       Al 8% la cabeza entra entera en todo el rango apilado. */
    object-position: 68% 8%;
  }
  .portada__centro::before { display: none; }

  /* Dos columnas: en fila las cuatro cifras dejaban «Cafés tomados» solo en
     una línea nueva, como un huérfano. */
  .marcador {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.75rem 1.5rem;
  }
  .portada__pieton { display: none; }
}

/* Estado de partida de la entrada con GSAP. Si GSAP no carga, app.js las
   revela igual: nunca se queda una sección invisible por un CDN caído. */
/* REGLA 1 — el estado base es VISIBLE.
   Estos elementos solo se ocultan cuando el script ya está vivo y ha marcado
   <html data-armado>. Antes esta regla era incondicional, y si app.js no
   llegaba (bloqueador, proxy, fallo de red) la portada se quedaba en blanco
   detrás de la pantalla de carga para siempre — también para Google. */
[data-armado] .difuminar { opacity: 0; }

/* En móvil el elemento LCP no es el nombre sino el párrafo de la portada:
   es el bloque de texto más grande del primer viewport. Si arranca oculto,
   arrastra la métrica igual que lo haría el titular (medido: 2,96 s). Se le
   quita el ocultamiento y entra solo con transform, como el nombre. */
@media (max-width: 900px) {
  [data-armado] .portada .difuminar { opacity: 1; }
  .portada .difuminar { animation: entrar-nombre 0.9s cubic-bezier(0.16, 1, 0.3, 1) both; }
  /* Cascada corta, para que el bloque no entre como un ladrillo. */
  .portada .difuminar:nth-of-type(2) { animation-delay: 0.06s; }
  .portada .difuminar:nth-of-type(3) { animation-delay: 0.12s; }
}

/* El NOMBRE queda fuera de esa regla: es el elemento LCP y NUNCA arranca
   oculto. Un elemento que empieza en opacity 0 no se cuenta como pintado, y
   la métrica se retrasa lo que dure la animación. Su entrada es solo
   transform, que el navegador sí ignora para el LCP. */
.revelar-nombre[data-nombre-entra] {
  animation: entrar-nombre 1s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes entrar-nombre {
  from { transform: translate3d(0, 0.16em, 0); }
  to   { transform: translate3d(0, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .revelar-nombre[data-nombre-entra] { animation: none; }
}

.bajar {
  /* A la izquierda, no centrado: en el centro caia justo sobre el portatil
     de la fotografia. */
  position: absolute; left: var(--lado); bottom: 2rem; z-index: 2;
  display: grid; justify-items: center; gap: .75rem;
  font-family: var(--fuente-mono);
  font-size: .65rem; text-transform: uppercase; letter-spacing: .2em;
  color: var(--apagado);
}
.bajar__linea { position: relative; display: block; width: 1px; height: 40px; background: var(--trazo); overflow: hidden; }
.bajar__linea b {
  position: absolute; inset: 0;
  background: var(--acento-claro);
  animation: bajar 1.5s ease-in-out infinite;
}
@keyframes bajar {
  from { transform: translateY(-100%); }
  to   { transform: translateY(200%); }
}

/* ═══════════════ 8. SECCIONES, ENCABEZADOS Y BOTONES ═══════════════ */

.seccion {
  position: relative; z-index: 1;
  max-width: var(--ancho);
  margin-inline: auto;
  padding: clamp(4rem, 9vw, 7rem) var(--lado);
}

.cab { margin-bottom: 3rem; }

.cab__ceja {
  display: flex; align-items: center; gap: .75rem;
  font-family: var(--fuente-mono);
  font-size: .7rem; text-transform: uppercase; letter-spacing: .3em;
  color: var(--apagado);
  margin-bottom: 1rem;
}
.cab__ceja i { width: 2rem; height: 1px; background: var(--trazo); }

.cab__titulo {
  font-family: var(--fuente-cuerpo);
  font-weight: 600;
  font-size: clamp(2rem, 5.5vw, 3.4rem);
  line-height: 1.08;
  /* Plex Sans en tamaño grande abre demasiado; el tracking negativo la
     compacta y le da el aire de titular en vez de párrafo agrandado. */
  letter-spacing: -0.03em;
  margin-bottom: .75rem;
}
.cab__titulo em { font-family: var(--fuente-titulo); font-style: italic; }

.cab__sub { color: var(--apagado); max-width: 34rem; font-size: .95rem; }

.cab__accion { margin-top: 1.5rem; }

.boton {
  position: relative;
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .85rem 1.6rem;
  border-radius: 999px;
  font-size: .875rem;
  font-family: inherit;
  cursor: pointer;
  border: 2px solid transparent;
  transition: transform .25s ease, background .25s ease, color .25s ease, border-color .25s ease;
}
.boton:hover { transform: scale(1.04); }

.boton--solido { background: var(--texto); color: var(--bg); }
.boton--solido:hover { background: var(--acento-claro); color: var(--bg); }

.boton--linea { border-color: var(--trazo); background: var(--bg); color: var(--texto); }
.boton--linea:hover { border-color: var(--acento); }

.boton--fantasma {
  border-color: var(--trazo);
  background: transparent;
  color: var(--apagado);
  padding: .7rem 1.25rem;
}
.boton--fantasma:hover { color: var(--texto); border-color: var(--acento); }

/* ═══════════════════════ 9. BENTO DE PROYECTOS ═══════════════════════ */

.bento {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 2.5rem 1.75rem;
}
/* Mitades iguales, no 7/5. La asimetría del bento funcionaba cuando el texto
   iba encima de la imagen; con el pie debajo, dos tarjetas de distinta altura
   dejan los nombres desalineados y un hueco muerto bajo la más corta. */
.tarjeta--7, .tarjeta--5 { grid-column: span 6; }

@media (max-width: 860px) {
  .tarjeta--7, .tarjeta--5 { grid-column: span 12; }
}

.tarjeta { margin: 0; }

/* La ficha es imagen ARRIBA y texto DEBAJO, no texto encima de la imagen.
   Antes el pie flotaba sobre la captura con un degradado de 6rem: tapaba la
   interfaz que se supone que se está enseñando y, sobre capturas claras, el
   texto perdía contraste justo donde más importa. Un pie de foto debajo se
   lee siempre. */
.tarjeta a,
.tarjeta > div {
  display: grid;
  grid-template-rows: auto auto;
  gap: 1rem;
}

.tarjeta__marco {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-radius: var(--radio);
  border: 1px solid var(--trazo);
  background: var(--superficie);
}

.tarjeta img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  transition: transform .8s cubic-bezier(.25,.1,.25,1), filter .5s ease;
  /* Las capturas vienen de sitios claros y muy distintos entre sí; se apagan
     un poco para que se lean como una serie sobre fondo oscuro. Antes estaban
     a brightness .62, que las dejaba grises y sin vida: el objetivo es
     unificarlas, no apagarlas. */
  filter: saturate(1.2) brightness(1);
}
/* El hover ya no cambia el filtro: en reposo la imagen va a saturate(1.2),
   asi que un filter:none aqui la APAGARIA al pasar el cursor, que es justo lo
   contrario de lo que se busca. Solo escala. */
.tarjeta a:hover img { transform: scale(1.04); }

/* Un velo apenas perceptible en reposo, para asentar la captura en el fondo.
   Sustituye a la trama de semitono, que a 4px se leía como un mosquitero
   encima de la imagen. */
.tarjeta__velo {
  position: absolute; inset: 0;
  background: linear-gradient(to top, hsl(0 0% 3% / .35), transparent 55%);
  opacity: 1;
  transition: opacity .4s ease;
  pointer-events: none;
}
.tarjeta a:hover .tarjeta__velo { opacity: 0; }

/* Chapa de «Ver» al pasar el cursor. */
.tarjeta__placa {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(.94);
  padding: .7rem 1.4rem;
  border-radius: 999px;
  background: var(--texto); color: var(--bg);
  font-size: .875rem;
  white-space: nowrap;
  opacity: 0;
  transition: opacity .35s ease, transform .35s cubic-bezier(.25,.1,.25,1);
}
.tarjeta__placa em { font-family: var(--fuente-titulo); font-style: italic; }
.tarjeta a:hover .tarjeta__placa { opacity: 1; transform: translate(-50%, -50%) scale(1); }

/* --- Pie, ya fuera de la imagen --- */
.tarjeta__pie {
  display: grid; gap: .3rem;
  padding: 0 .25rem;
}
.tarjeta__pie b {
  font-family: var(--fuente-titulo);
  font-size: clamp(1.25rem, 2vw, 1.6rem);
  font-weight: 400;
  letter-spacing: -0.015em;
  line-height: 1.15;
  transition: color .3s ease;
}
.tarjeta a:hover .tarjeta__pie b { color: var(--acento-claro); }
.tarjeta__pie span {
  font-size: .875rem;
  line-height: 1.5;
  color: var(--apagado);
}
.tarjeta__pie i {
  font-family: var(--fuente-mono); font-style: normal;
  font-size: .68rem; letter-spacing: .06em; color: var(--apagado);
  margin-top: .25rem;
  opacity: .8;
}

/* Etiqueta para los proyectos internos, que no llevan enlace. */
.tarjeta--interno .tarjeta__marco::after {
  content: "Interno";
  position: absolute; top: .9rem; right: .9rem;
  padding: .25rem .6rem;
  border-radius: 999px;
  border: 1px solid hsl(0 0% 100% / .25);
  background: hsl(0 0% 4% / .7);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  font-family: var(--fuente-mono);
  font-size: .6rem; letter-spacing: .12em; text-transform: uppercase;
  color: hsl(0 0% 88%);
}

/* ═══════════════════════ 10. ÍNDICE EN LISTA ═══════════════════════ */

.lista { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }

.lista > li > a,
.lista > li > div {
  display: grid;
  grid-template-columns: 84px 1fr auto;
  align-items: center;
  gap: 1.5rem;
  padding: 1rem;
  border-radius: 40px;
  border: 1px solid var(--trazo);
  background: hsl(0 0% 8% / .35);
  transition: background .3s ease, border-color .3s ease, transform .3s ease;
}
.lista > li > a:hover {
  background: var(--superficie);
  border-color: hsl(211 40% 45% / .5);
  transform: translateX(6px);
}

.lista__mini {
  display: block;
  width: 84px; height: 60px;
  border-radius: 28px;
  overflow: hidden;
  background: var(--superficie);
}
.lista__mini img { width: 100%; height: 100%; object-fit: cover; object-position: top center; filter: saturate(.8) brightness(.9); }

/* Los cuatro internos no tienen captura publicable: en su lugar, la sigla
   sobre el color de la marca. Es una decisión visible, no un hueco. */
.lista__mini--sigla {
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--tono) 28%, var(--superficie));
  border: 1px solid color-mix(in srgb, var(--tono) 45%, transparent);
  font-family: var(--fuente-titulo); font-style: italic;
  font-size: 1.35rem;
  color: color-mix(in srgb, var(--tono) 55%, white);
}
.lista__mini--sigla::after { content: attr(data-sigla); }

.lista__txt { display: grid; gap: .15rem; min-width: 0; }
.lista__txt b { font-size: 1.05rem; font-weight: 500; }
.lista__txt span { font-size: .85rem; color: var(--apagado); }

.lista__meta { display: grid; gap: .2rem; justify-items: end; text-align: right; }
.lista__meta i {
  font-family: var(--fuente-mono); font-style: normal;
  font-size: .68rem; letter-spacing: .05em; color: var(--apagado);
}
.lista__meta em {
  font-style: normal; font-size: .8rem;
  color: var(--acento-claro);
}
.es-interno .lista__meta em {
  color: var(--apagado);
  border: 1px solid var(--trazo);
  border-radius: 999px;
  padding: .1rem .6rem;
  font-size: .7rem;
}

@media (max-width: 780px) {
  .lista > li > a,
  .lista > li > div {
    grid-template-columns: 60px 1fr;
    border-radius: 28px;
  }
  .lista__mini { width: 60px; height: 60px; border-radius: 20px; }
  .lista__meta { grid-column: 1 / -1; justify-items: start; text-align: left; }
}

/* ══════════════════ 11. ENFOQUE CON PARALAJE ══════════════════ */
/* Tres pantallas de alto: espacio para que el bloque central quede fijo
   mientras las dos columnas lo cruzan a velocidades distintas. */

.explora {
  position: relative; z-index: 1;
  min-height: 300vh;
}

.explora__fijo {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  display: grid; place-items: center;
  padding-inline: var(--lado);
  z-index: 1;
  pointer-events: none;
}
.explora__centro {
  pointer-events: auto;
  max-width: 30rem; text-align: center;
}
.explora__grande {
  font-size: clamp(1.05rem, 2.2vw, 1.25rem);
  color: var(--apagado);
  margin-bottom: 2rem;
}
.explora__grande em { color: var(--texto); font-style: normal; }

/* El hueco central es el que protege al texto fijo: las tarjetas viajan por
   los bordes y nunca lo cruzan. Por eso el gap es enorme y las columnas se
   alinean hacia fuera, no hacia el centro. */
.explora__cols {
  position: absolute; inset: 0;
  z-index: 2;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, 32vw, 36rem);
  max-width: 1500px; margin-inline: auto;
  /* inset:0 alinea con la caja de relleno de .explora, así que el padding
     lateral de la sección no protege a las tarjetas: hay que repetirlo aquí
     o el primer y el último bloque se salen por los bordes. */
  padding: 12vh var(--lado);
  pointer-events: none;
}
.explora__col { display: grid; gap: clamp(4rem, 14vh, 10rem); align-content: start; justify-items: start; }
.explora__col:last-child { justify-items: end; }
/* Columna derecha desfasada: sin esto las seis tarjetas forman dos filas
   perfectamente alineadas y el paralaje casi no se nota. */
.explora__col:last-child { padding-top: 26vh; }

.capa {
  pointer-events: auto;
  padding: 1.75rem;
  border-radius: var(--radio);
  border: 1px solid var(--trazo);
  background: hsl(0 0% 8% / .82);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  max-width: 21rem;
}
.capa__n {
  display: inline-block;
  font-family: var(--fuente-titulo); font-style: italic;
  font-size: 1.5rem; color: var(--acento-claro);
  margin-bottom: .5rem;
}
.capa h3 { font-size: 1.25rem; margin-bottom: .6rem; }
.capa p { font-size: .875rem; color: var(--apagado); margin-bottom: 1rem; }

.capa--cita { background: hsl(211 40% 45% / .12); border-color: hsl(211 40% 45% / .3); }
.capa--cita p {
  font-family: var(--fuente-titulo); font-style: italic;
  font-size: 1.2rem; color: hsl(0 0% 85%); margin: 0; line-height: 1.4;
}
.capa--cita em { color: var(--texto); }

.tags { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; padding: 0; }
.tags li {
  font-family: var(--fuente-mono);
  font-size: .65rem; letter-spacing: .05em;
  padding: .25rem .6rem;
  border: 1px solid var(--trazo);
  border-radius: 999px;
  color: var(--apagado);
}

/* Por debajo de 900px el paralaje se apaga y las tarjetas se leen en una
   sola columna: en móvil el pinning pelea con la barra del navegador. */
@media (max-width: 900px) {
  .explora { min-height: 0; padding: clamp(4rem, 9vw, 7rem) var(--lado); }
  .explora__fijo { position: static; height: auto; margin-bottom: 3rem; }
  .explora__cols {
    position: static;
    grid-template-columns: 1fr;
    gap: 1rem;
    padding: 0;
    transform: none !important;
  }
  .explora__col { gap: 1rem; padding-top: 0 !important; transform: none !important; }
  .explora__col:last-child { padding-top: 0; }
  .capa { max-width: none; }
}

/* ═══════════════════════════ 12. CIFRAS ═══════════════════════════ */

.cifras-seccion { border-block: 1px solid var(--trazo); }

.cifras {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 2rem;
}
.cifras li { display: grid; gap: .35rem; }
.cifras b {
  font-family: var(--fuente-titulo); font-style: italic; font-weight: 400;
  font-size: clamp(2.5rem, 6vw, 4rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  background: var(--degradado);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.cifras span { font-size: .8125rem; color: var(--apagado); }

@media (max-width: 760px) { .cifras { grid-template-columns: repeat(2, 1fr); } }

/* ════════════════════════ 13. TRAYECTORIA ════════════════════════ */

.linea-t { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.linea-t li {
  display: grid;
  grid-template-columns: 10rem 1fr;
  gap: 2rem;
  padding: 2rem 0;
  border-top: 1px solid var(--trazo);
}
.linea-t li:last-child { border-bottom: 1px solid var(--trazo); }
.linea-t__anio {
  font-family: var(--fuente-mono);
  font-size: .75rem; letter-spacing: .05em;
  color: var(--acento-claro);
  padding-top: .25rem;
}
.linea-t h3 { font-size: 1.15rem; margin-bottom: .2rem; }
.linea-t__org { font-size: .8125rem; color: var(--acento-claro); margin-bottom: .6rem; }
.linea-t p:last-child { font-size: .9rem; color: var(--apagado); margin: 0; }

.estudios {
  display: grid; gap: .35rem;
  margin-top: 2rem;
  font-size: .9rem; color: var(--apagado);
}
.estudios span {
  font-family: var(--fuente-mono);
  font-size: .7rem; text-transform: uppercase; letter-spacing: .2em;
  color: var(--texto);
}

@media (max-width: 700px) {
  .linea-t li { grid-template-columns: 1fr; gap: .5rem; }
}

/* ════════════════════════ 14. CONTACTO Y PIE ════════════════════════ */

.pie {
  position: relative; z-index: 1;
  border-top: 1px solid var(--trazo);
  overflow: hidden;
  background:
    radial-gradient(60% 50% at 50% 100%, hsl(211 45% 40% / .18), transparent 70%),
    var(--bg);
}

.marquesina { overflow: hidden; padding: 3rem 0; border-bottom: 1px solid var(--trazo); }
.marquesina__cinta { display: flex; width: max-content; will-change: transform; }
.marquesina__cinta span {
  font-family: var(--fuente-titulo); font-style: italic;
  font-size: clamp(2rem, 6vw, 4.5rem);
  white-space: nowrap;
  padding-right: 1.5rem;
  color: hsl(0 0% 96% / .9);
}
.marquesina__cinta i { font-style: normal; color: var(--acento); }

.contacto {
  max-width: var(--ancho); margin-inline: auto;
  padding: clamp(3rem, 8vw, 5rem) var(--lado);
  display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}
@media (max-width: 860px) { .contacto { grid-template-columns: 1fr; } }

.contacto__grande {
  font-size: clamp(1.75rem, 4.5vw, 2.75rem);
  line-height: 1.15;
  margin-bottom: 2rem;
}
.contacto__grande em { font-family: var(--fuente-titulo); font-style: italic; }

.contacto__cta { margin-bottom: 2.5rem; }

.enlaces { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.enlaces li {
  display: grid; grid-template-columns: 7rem 1fr;
  gap: 1rem;
  padding: .6rem 0;
  border-top: 1px solid var(--trazo);
  font-size: .875rem;
}
.enlaces span { font-family: var(--fuente-mono); font-size: .7rem; text-transform: uppercase; letter-spacing: .15em; color: var(--apagado); padding-top: .15rem; }
.enlaces a { transition: color .25s ease; }
.enlaces a:hover { color: var(--acento-claro); }

.forma { display: grid; gap: 1rem; }
.campo { display: grid; gap: .4rem; }
.campo label {
  font-family: var(--fuente-mono);
  font-size: .7rem; text-transform: uppercase; letter-spacing: .15em;
  color: var(--apagado);
}
.campo input, .campo textarea {
  font-family: inherit; font-size: .9rem;
  color: var(--texto);
  background: hsl(0 0% 8% / .6);
  border: 1px solid var(--trazo);
  border-radius: 14px;
  padding: .8rem 1rem;
  transition: border-color .25s ease, background .25s ease;
  resize: vertical;
}
.campo input:focus, .campo textarea:focus {
  outline: none;
  border-color: var(--acento);
  background: var(--superficie);
}
.campo input[aria-invalid="true"], .campo textarea[aria-invalid="true"] { border-color: #c85a4a; }

.forma .boton { justify-self: start; }

.forma__aviso { font-size: .8125rem; color: var(--apagado); margin: 0; min-height: 1.2em; }
.forma__aviso[data-error] { color: #e08a7c; }
.forma__aviso[data-ok] { color: var(--acento-claro); }

/* Trampa de miel: fuera de pantalla, no display:none — algunos robots
   descartan los campos ocultos con display y esto los deja pasar. */
.miel { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.pie__barra {
  max-width: var(--ancho); margin-inline: auto;
  padding: 1.5rem var(--lado) 2.5rem;
  border-top: 1px solid var(--trazo);
  display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;
  font-size: .8125rem; color: var(--apagado);
}
.pie__barra p { margin: 0; }
.pie__estado { display: inline-flex; align-items: center; gap: .5rem; }
.pie__estado i {
  width: 8px; height: 8px; border-radius: 50%;
  background: #4ade80;
  box-shadow: 0 0 0 0 hsl(142 70% 55% / .6);
  animation: latido 2s ease-out infinite;
}
@keyframes latido {
  to { box-shadow: 0 0 0 10px hsl(142 70% 55% / 0); }
}
.pie__barra a:hover { color: var(--texto); }

/* ═══════════════════ 15. MOVIMIENTO REDUCIDO ═══════════════════ */
/* Quien lo pide no quiere "menos" movimiento: quiere ninguno. El paralaje
   también se apaga desde app.js, que no llega a registrar los ScrollTrigger. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  [data-entra] { opacity: 1; transform: none; }
  [data-armado] .difuminar { opacity: 1; }
  .explora { min-height: 0; padding-block: 5rem; }
  .explora__fijo { position: static; height: auto; margin-bottom: 3rem; }
  .explora__cols { position: static; grid-template-columns: 1fr; padding: 0; gap: 1rem; }
  .explora__col { gap: 1rem; padding-top: 0 !important; }
}

/* ─────────────────────────── Retrato de contacto ───────────────────────── */
/* Va en el bloque de contacto y no en la portada a propósito: una cara sirve
   justo en el momento en que se invita a escribir, y en la portada competiría
   con el titular, que es el elemento LCP. */

.retrato {
  width: 190px;
  margin: 0 0 2rem;
  border-radius: 14px;
  overflow: hidden;
  border: 1px solid var(--trazo);
  /* El mismo tratamiento frío del resto del sitio, para que la foto no
     parezca pegada encima de la paleta. */
  background: var(--fondo-2, #0d1117);
}
.retrato img {
  display: block;
  width: 100%;
  height: auto;          /* imprescindible junto a los atributos width/height */
  aspect-ratio: 1 / 1;
  object-fit: cover;
  filter: saturate(0.92) contrast(1.03);
}

@media (max-width: 700px) {
  .retrato { width: min(84vw, 260px); margin-bottom: 1.5rem; }
}

/* ═══════════════════ MENÚ MÓVIL A PANTALLA COMPLETA ═══════════════════
   Antes, bajo 720 px, los cuatro enlaces de sección eran simplemente
   display:none. En una página de 12,7 pantallas de scroll eso deja a quien
   entra desde el teléfono sin ninguna forma de llegar a Proyectos. */

/* --- Botón --- */
.menu-btn {
  display: none;                    /* lo enciende la media query */
  align-items: center; justify-content: center;
  width: 40px; height: 40px;
  padding: 0; border: 0; border-radius: 50%;
  background: hsl(0 0% 100% / .06);
  color: var(--texto);
  cursor: pointer;
  transition: background .25s ease;
}
.menu-btn:hover { background: hsl(0 0% 100% / .12); }

.menu-btn__barras { display: block; width: 17px; }
.menu-btn__barras i {
  display: block; height: 1.5px; border-radius: 2px;
  background: currentColor;
  transition: transform .38s cubic-bezier(.16,1,.3,1), opacity .2s ease;
}
.menu-btn__barras i:first-child { margin-bottom: 5px; }

/* Abierto: las dos barras se cruzan en aspa. Se anima con transform, así que
   no hay que cambiar el marcado ni reflowear nada. */
.menu-btn[aria-expanded="true"] .menu-btn__barras i:first-child {
  transform: translateY(3.25px) rotate(45deg);
}
.menu-btn[aria-expanded="true"] .menu-btn__barras i:last-child {
  transform: translateY(-3.25px) rotate(-45deg);
}

/* --- Panel --- */
.menu {
  position: fixed; inset: 0; z-index: 9000;
  display: grid;
  grid-template-rows: 1fr auto;
  gap: 2rem;
  padding: 7.5rem var(--lado) 2.5rem;
  background: hsl(0 0% 3% / .97);
  -webkit-backdrop-filter: blur(18px);
          backdrop-filter: blur(18px);
  opacity: 0;
  transition: opacity .34s ease;
}
.menu[data-abierto] { opacity: 1; }

/* [hidden] por sí solo lo ganaría el display:grid de arriba: la regla de
   display de una clase tiene más peso que el estilo de agente de usuario. */
.menu[hidden] { display: none; }

.menu__nav { display: grid; align-content: center; gap: .35rem; }

.menu__nav a {
  display: flex; align-items: baseline; gap: 1rem;
  padding: .5rem 0;
  font-family: var(--fuente-titulo);
  font-size: clamp(2.2rem, 11vw, 3.4rem);
  line-height: 1.06;
  letter-spacing: -0.02em;
  /* Entra en cascada. Empieza desplazado, no invisible: si algo fallara a
     mitad de la animación, el enlace seguiría siendo pulsable. */
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .5s ease, transform .5s cubic-bezier(.16,1,.3,1);
}
.menu[data-abierto] .menu__nav a { opacity: 1; transform: none; }
.menu[data-abierto] .menu__nav a:nth-child(1) { transition-delay: .06s; }
.menu[data-abierto] .menu__nav a:nth-child(2) { transition-delay: .11s; }
.menu[data-abierto] .menu__nav a:nth-child(3) { transition-delay: .16s; }
.menu[data-abierto] .menu__nav a:nth-child(4) { transition-delay: .21s; }
.menu[data-abierto] .menu__nav a:nth-child(5) { transition-delay: .26s; }

.menu__nav a span {
  font-family: var(--fuente-mono);
  font-size: .7rem; letter-spacing: .15em;
  color: var(--apagado);
}
.menu__nav a.es-activo { color: var(--acento-claro); }
.menu__nav a.es-activo span { color: var(--acento-claro); }

.menu__pie {
  display: grid; gap: .6rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--trazo);
  font-family: var(--fuente-mono);
  font-size: .78rem;
  color: var(--apagado);
}
.menu__pie a { transition: color .25s ease; }
.menu__pie a:hover { color: var(--texto); }

/* Bloqueo del scroll de fondo mientras el panel está abierto. */
body.menu-abierto { overflow: hidden; }

@media (max-width: 720px) {
  /* El botón solo existe si el JS se enganchó. Un botón que no abre nada
     es peor que no tener botón. */
  [data-menu-listo] .menu-btn { display: inline-flex; }
}

@media (min-width: 721px) {
  /* Por encima del breakpoint la pastilla ya muestra los enlaces: el panel
     no debe poder quedarse abierto si alguien gira el teléfono. */
  .menu { display: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  .menu, .menu__nav a, .menu-btn__barras i { transition: none; }
  .menu__nav a { opacity: 1; transform: none; }
}

/* Con el panel abierto la pastilla se reduce al botón de cerrar: la marca y
   el «Escríbeme» duplicarían enlaces que el propio panel ya ofrece, y encima
   quedarían sobre un fondo que ya no es el suyo. */
body.menu-abierto .pastilla { background: transparent; border-color: transparent; box-shadow: none; }
body.menu-abierto .marca,
body.menu-abierto .saluda,
body.menu-abierto .pastilla__sep { display: none; }
body.menu-abierto .nav { justify-content: flex-end; }

/* ═══════════════════════ TECNOLOGÍAS ═══════════════════════
   Va justo bajo la portada, como respiro entre el retrato y los proyectos.
   Agrupada por rol y no en una fila corrida: 23 logos seguidos son un muro
   que nadie lee; agrupados dicen algo sobre cómo trabajas. */

.tecno {
  max-width: var(--ancho);
  margin: 0 auto;
  padding: clamp(2.5rem, 6vh, 4.5rem) var(--lado) clamp(1.5rem, 4vh, 2.5rem);
}

.tecno__grupos {
  display: flex;
  gap: 2rem;
  flex: 0 0 auto;
}

.tecno__rotulo {
  font-family: var(--fuente-mono);
  font-size: .62rem; font-weight: 400;
  text-transform: uppercase; letter-spacing: .2em;
  color: var(--apagado);
  margin: 0 0 1.1rem;
  padding-bottom: .7rem;
  border-bottom: 1px solid var(--trazo);
}

.tecno__lista {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 1.35rem 1.6rem;
}

.tecno__lista li {
  display: flex; align-items: center; gap: .55rem;
  color: var(--apagado);
  transition: color .3s ease;
}
.tecno__lista li:hover { color: var(--texto); }

.tecno__lista svg {
  width: 20px; height: 20px;
  flex: 0 0 20px;
  /* currentColor: los logos heredan el color del texto en vez de traer cada
     uno su azul, su morado y su naranja corporativos. Así la tira se lee como
     una serie y no compite con el resto de la página. */
  fill: currentColor;
}

.tecno__lista span {
  font-family: var(--fuente-mono);
  font-size: .72rem;
  letter-spacing: .02em;
  white-space: nowrap;
}


@media (prefers-reduced-motion: reduce) {
  .tecno__lista li { transition: none; }
}


/* ═══════════════════════ VISOR DE GALERÍA ═══════════════════════
   Sobre <dialog> nativo: el foco atrapado, el cierre con Escape y dejar
   inerte el resto de la página vienen de serie. Reimplementarlo a mano es
   como salen a medias la mitad de los lightbox. */

.tarjeta__abrir {
  display: grid; grid-template-rows: auto auto; gap: 1rem;
  width: 100%; padding: 0; margin: 0;
  border: 0; background: none;
  font: inherit; color: inherit; text-align: left;
  cursor: zoom-in;
}
.tarjeta__abrir:focus-visible { outline: 2px solid var(--acento-claro); outline-offset: 6px; border-radius: var(--radio); }
.tarjeta__abrir:hover img { transform: scale(1.04); }
.tarjeta__abrir:hover .tarjeta__velo { opacity: 0; }
.tarjeta__abrir:hover .tarjeta__placa { opacity: 1; transform: translate(-50%, -50%) scale(1); }
.tarjeta__abrir:hover .tarjeta__pie b { color: var(--acento-claro); }

/* Rejilla de dos filas: la imagen se queda el alto sobrante y el panel de
   datos ocupa lo suyo. Con alto fijo el conjunto medía más que la pantalla y
   el botón de cerrar quedaba cortado en portátiles bajos. */
.visor {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 1.25rem;
  width: min(94vw, 1180px);
  max-width: none;
  max-height: 88vh;
  margin: auto;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--texto);
  overflow: visible;
}
.visor::backdrop {
  background: hsl(0 0% 2% / .94);
  -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
}
.visor, .visor::backdrop {
  transition: opacity .3s ease, overlay .3s allow-discrete, display .3s allow-discrete;
  opacity: 0;
}
.visor[open], .visor[open]::backdrop { opacity: 1; }
@starting-style { .visor[open], .visor[open]::backdrop { opacity: 0; } }

.visor__marco {
  min-height: 0;
  display: grid;
  border-radius: 14px;
  overflow: hidden;
  border: 1px solid var(--trazo);
  background: hsl(0 0% 6%);
}
.visor__marco img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* --- Panel de datos --- */
.visor__info { display: grid; gap: 1rem; }

.visor__pie {
  margin: 0;
  font-size: .95rem; line-height: 1.55;
  color: hsl(0 0% 84%);
  text-align: center;
  /* Alto reservado para dos líneas: sin esto, el panel salta al pasar de una
     descripción corta a una larga y arrastra las miniaturas con él. */
  min-height: 3em;
}
.visor__ir {
  color: var(--acento-claro);
  text-decoration: underline; text-underline-offset: 3px;
  white-space: nowrap;
}
.visor__ir:hover { color: var(--texto); }

/* --- Miniaturas --- */
.visor__tiras {
  display: flex; align-items: center; justify-content: center;
  gap: .9rem; flex-wrap: wrap;
}
.visor__miniaturas {
  list-style: none; margin: 0; padding: 0;
  display: flex; gap: .5rem;
}
.visor__miniaturas button {
  display: block;
  width: 84px; aspect-ratio: 16 / 10;
  padding: 0;
  border: 1px solid var(--trazo);
  border-radius: 7px;
  overflow: hidden;
  background: hsl(0 0% 8%);
  cursor: pointer;
  opacity: .45;
  transition: opacity .25s ease, border-color .25s ease, transform .25s ease;
}
.visor__miniaturas button:hover { opacity: .85; transform: translateY(-2px); }
.visor__miniaturas button[aria-current="true"] {
  opacity: 1;
  border-color: var(--acento-claro);
}
.visor__miniaturas img { width: 100%; height: 100%; object-fit: cover; display: block; }

.visor__nav {
  width: 40px; height: 40px; flex: 0 0 40px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--trazo);
  background: hsl(0 0% 100% / .05);
  color: var(--texto);
  font-size: 1rem; cursor: pointer;
  transition: background .25s ease, border-color .25s ease;
}
.visor__nav:hover { background: hsl(0 0% 100% / .14); border-color: var(--acento); }
.visor__nav[disabled] { opacity: .25; cursor: default; }

.visor__cuenta {
  margin: 0;
  font-family: var(--fuente-mono);
  font-size: .68rem; letter-spacing: .14em;
  color: var(--apagado);
  min-width: 5ch; text-align: center;
}

/* --- Sitios en vivo --- */
.visor__sitios {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: .5rem .8rem;
  padding-top: 1rem;
  border-top: 1px solid var(--trazo);
}
.visor__sitios[hidden] { display: none; }
.visor__sitiosRot {
  font-family: var(--fuente-mono);
  font-size: .6rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--apagado);
}
.visor__enlaces {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: .45rem;
}
.visor__enlaces a {
  display: inline-block;
  padding: .42rem .85rem;
  border: 1px solid var(--trazo);
  border-radius: 999px;
  font-family: var(--fuente-mono);
  font-size: .68rem; letter-spacing: .03em;
  /* Antes iban en gris apagado sobre negro y no se leían. Un enlace tiene
     que parecer pulsable. */
  color: hsl(0 0% 82%);
  background: hsl(0 0% 100% / .04);
  transition: color .22s ease, border-color .22s ease, background .22s ease;
}
.visor__enlaces a:hover {
  color: #fff; border-color: var(--acento-claro);
  background: hsl(0 0% 100% / .1);
}

/* Dentro del marco, no encima del dialogo. Colgada por fuera se salia de la
   pantalla en portatiles de 768 px de alto y no habia forma de cerrar con el
   raton. */
.visor__cerrar {
  position: absolute; top: .85rem; right: .85rem; z-index: 3;
  width: 42px; height: 42px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--trazo);
  background: hsl(0 0% 4% / .72);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  color: var(--texto);
  font-size: 1.4rem; line-height: 1; cursor: pointer;
  transition: background .25s ease;
}
.visor__cerrar:hover { background: hsl(0 0% 12% / .92); }

@media (max-width: 700px) {
  .visor { width: 94vw; gap: .9rem; }
  .visor__cerrar { top: .6rem; right: .6rem; width: 38px; height: 38px; }
  .visor__pie { font-size: .85rem; min-height: 3.4em; }
  .visor__miniaturas button { width: 56px; }
  .visor__nav { width: 36px; height: 36px; flex: 0 0 36px; }
  .visor__sitios { gap: .4rem; }
}

@media (prefers-reduced-motion: reduce) {
  .visor, .visor::backdrop { transition: none; }
  .visor__miniaturas button:hover { transform: none; }
  .tarjeta__abrir:hover img { transform: none; }
}

/* --- Iconos de la pastilla --- */
/* Sustituyen al botón «Escríbeme». Van con currentColor para que sean de la
   misma familia visual que el resto de la barra: si cada uno trajera su color
   de marca (verde WhatsApp, negro GitHub) la píldora parecería una barra de
   compartir en redes, no una navegación. */
.pastilla__ico {
  display: grid; place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  color: var(--apagado);
  transition: color .25s ease, background .25s ease;
}
.pastilla__ico svg { width: 17px; height: 17px; fill: currentColor; display: block; }
.pastilla__ico:hover { color: var(--texto); background: hsl(0 0% 100% / .08); }
.pastilla__ico:focus-visible { outline: 2px solid var(--acento-claro); outline-offset: 2px; }

/* Las portadas que son pantallazos de pagina completa (no mockups en portátil)
   se recortan desde el CENTRO, no desde arriba. Una web tiene la maquetación
   centrada, así que recortar por un solo lado la deja visiblemente torcida. */
.tarjeta--captura img { object-position: center; }

@media (max-width: 720px) {
  /* En la barra de móvil la firma queda sola con los iconos, y a 26px se veía
     pequeña frente a ellos. */
  .marca__logo { height: 30px; }
}


/* ═══════════════════════ MOVIMIENTO DE LA PORTADA ═══════════════════════
   Cuatro capas, todas sobre transform u opacidad de elementos que NO son el
   LCP. El retrato es el elemento LCP en escritorio, así que nunca se le anima
   la opacidad: solo la máscara y el desplazamiento. */

/* --- 1. Profundidad: puntero y scroll ---
   --mx/--my las escribe el bucle rAF ya suavizadas (lerp 0,035), y --heroY
   es el avance del scroll dentro de la portada. La foto va en sentido
   CONTRARIO al cursor y más lenta que el texto al bajar: eso es lo que crea
   la sensación de dos planos. */
.portada__fondo {
  /* Solo paralaje de SCROLL. Se retiro el seguimiento del cursor: en un
     retrato, que la cara se desplace al mover el raton se lee como que la
     imagen flota, no como profundidad. Con el scroll si funciona, porque el
     desplazamiento tiene una direccion que el ojo ya espera. */
  transform: translate3d(0, calc(var(--heroY, 0) * 60px), 0);
  will-change: transform;
}
.portada__centro {
  transform: translate3d(0, calc(var(--heroY, 0) * -22px), 0);
}

/* --- 2. Revelado del retrato ---
   Se anima la MÁSCARA, no la opacidad: un elemento que empieza en opacity 0
   no cuenta como pintado y retrasaría el LCP lo que dure la animación. */
.portada__fondo {
  -webkit-mask-size: 200% 100%; mask-size: 200% 100%;
  -webkit-mask-position: 100% 0; mask-position: 100% 0;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  animation: revelar-retrato 1.5s cubic-bezier(.22,1,.36,1) .15s both;
}
@keyframes revelar-retrato {
  from { -webkit-mask-position: 100% 0; mask-position: 100% 0; }
  to   { -webkit-mask-position: 0% 0;  mask-position: 0% 0; }
}

/* --- 3. Luz que respira ---
   Un halo detrás del retrato, muy tenue y muy lento. 14s: por debajo de unos
   diez se percibe como parpadeo en vez de como luz de pantalla. */
.portada::before {
  content: "";
  position: absolute;
  top: -10%; right: 4%;
  width: 46%; aspect-ratio: 1;
  z-index: 0;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--acento) 16%, transparent), transparent 68%);
  filter: blur(60px);
  animation: respirar 14s ease-in-out infinite;
}
@keyframes respirar {
  0%, 100% { opacity: .5; transform: scale(1); }
  50%      { opacity: .9; transform: scale(1.08); }
}

/* --- 4. Cifras --- */
.marcador b { font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .portada__fondo, .portada__centro { transform: none; animation: none;
    -webkit-mask-position: 0 0; mask-position: 0 0; }
  .portada::before { animation: none; opacity: .6; }
}

/* ═══════════════════════ MARQUEE DE TECNOLOGÍAS ═══════════════════════
   Cada categoría sigue siendo su caja; lo que cambia es que la fila se
   desplaza sola y se puede arrastrar. */

.tecno__pista {
  overflow-x: auto;
  overflow-y: hidden;
  /* La barra se oculta porque el gesto es arrastrar, no scrollear. El
     contenido sigue siendo alcanzable con rueda y con teclado. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  cursor: grab;
  /* Desvanece los dos extremos: sin esto, las cajas se cortan a filo y se ve
     como un desbordamiento, no como una cinta que continúa. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 4%, #000 96%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 4%, #000 96%, transparent);
  margin-top: 2rem;
}
.tecno__pista::-webkit-scrollbar { display: none; }
.tecno__pista[data-arrastrando] {
  cursor: grabbing;
  /* Mientras se arrastra no debe seleccionarse el texto de las etiquetas. */
  user-select: none;
}

.tecno__cinta { display: flex; width: max-content; }

/* Cada caja de categoría, con ancho propio para que la cinta tenga ritmo. */
.tecno__grupo {
  flex: 0 0 auto;
  width: 15rem;
}
.tecno__pista .tecno__lista { gap: 1rem 1.25rem; }

@media (max-width: 560px) {
  .tecno__grupo { width: 13rem; }
  .tecno__grupos { gap: 1.5rem; }
}

/* ═══════════════════════ CINTAS DE PROYECTOS ═══════════════════════
   Dos filas que corren en sentidos opuestos, se arrastran con raton o dedo y
   aceleran con el scroll de la pagina. Cada fila lleva sus tarjetas dos veces:
   la copia es lo que permite que el bucle no tenga costura. */

.cintas { display: grid; gap: 1.75rem; }

.cinta {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  cursor: grab;
  /* El dedo desliza en horizontal sin secuestrar el scroll vertical de la
     pagina: pan-y se lo devuelve al navegador. */
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
  /* Los extremos se desvanecen; sin esto las tarjetas se cortan a filo y
     parece un desbordamiento, no una cinta que sigue. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
}
.cinta::-webkit-scrollbar { display: none; }
.cinta[data-agarrando] { cursor: grabbing; user-select: none; }

.cinta__pista { display: flex; width: max-content; }
.cinta__grupo { display: flex; gap: 1.75rem; padding-right: 1.75rem; }

/* En cinta la tarjeta deja de depender de columnas de rejilla: ancho propio. */
.cinta .tarjeta,
.cinta .tarjeta--7,
.cinta .tarjeta--5 {
  grid-column: auto;
  flex: 0 0 auto;
  width: clamp(280px, 34vw, 440px);
}
/* Mientras se arrastra, el clic no debe abrir la galeria. */
.cinta[data-agarrando] .tarjeta__abrir,
.cinta[data-agarrando] a { pointer-events: none; }

@media (max-width: 700px) {
  .cintas { gap: 1.25rem; }
  .cinta__grupo { gap: 1.1rem; padding-right: 1.1rem; }
  .cinta .tarjeta { width: min(78vw, 340px); }
}

@media (prefers-reduced-motion: reduce) {
  .cinta { scroll-behavior: auto; }
}

/* Las cintas se salen del contenedor y ocupan el ancho completo de la ventana.
   El encabezado de la seccion sigue alineado con el resto de la pagina; solo
   las filas van a sangre. */
.cintas {
  width: 100vw;
  /* Saca el bloque de los margenes de la seccion: 50% lo lleva al centro del
     contenedor y -50vw al centro de la ventana. */
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  max-width: 100vw;
}
/* Con la cinta a sangre, la primera tarjeta ya no debe pegarse al borde. */
.cinta__grupo:first-child { padding-left: var(--lado); }

/* Misma sangre completa para la cinta de tecnologias: el rotulo sigue
   alineado con la pagina y solo la cinta se va a los bordes. */
.tecno__pista {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  max-width: 100vw;
}
.tecno__cinta > .tecno__grupos:first-child { padding-left: var(--lado); }
