/* ==========================================================================
   Oscar Canton — portfolio
   Dirección B: keynote oscuro. Fondo casi negro #08080B con dos auroras
   (violeta y cian) muy diluidas, un único acento violeta #7C6BFF y tarjetas
   elevadas de esquinas amplias. La tipografía la lleva Bricolage Grotesque
   (display), Onest (cuerpo/UI) y JetBrains Mono (metadatos), autoalojadas
   OFL. El verde sigue siendo semántico (status-live y el punto de
   disponibilidad), nunca decorativo.
   ========================================================================== */

@font-face {
  font-family: "Bricolage Grotesque";
  src: url("/assets/fonts/bricolage-grotesque-var.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Onest";
  src: url("/assets/fonts/onest-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("/assets/fonts/jetbrains-mono-var.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: dark;

  /* Superficies — casi negro FRÍO, nunca #000 puro */
  --bg: #08080B;            /* fondo global */
  --bg-surface: #14141A;    /* tarjetas, zonas elevadas */
  --bg-elevated: #1B1B24;   /* hover de superficie, preview */

  /* Texto, 3 niveles + muted cálido. --label-tertiary está FIJADO por
     validate-site.mjs: la línea debe quedar exactamente así (cualquier
     espaciado vale, el valor no). */
  --label: #EDEDF2;                  /* blanco frío */
  --label-secondary: rgba(235, 235, 245, 0.7);
  --label-tertiary: rgba(235, 235, 245, 0.55);
  --label-muted: #8E8EA0;            /* meta-etiquetas mono */

  /* Acento ÚNICO: violeta. Marca, links, focus, selección, status-soon,
     cursor. */
  --accent: #7C6BFF;
  --accent-strong: #A99BFF;                 /* hover */
  --accent-dim: rgba(124, 107, 255, 0.16);  /* rellenos (status, halos) */
  --accent-hairline: rgba(124, 107, 255, 0.42);
  --accent-cool: #26BECF;                   /* segunda aurora, solo fondo */

  /* Verde SEMÁNTICO, no acento: SOLO status-live y el punto «disponible». */
  --green: #30D158;
  --green-dim: rgba(48, 209, 88, 0.14);

  /* Bordes */
  --hairline: rgba(237, 237, 242, 0.12);
  --separator: rgba(237, 237, 242, 0.22);

  /* Tipografía */
  --display: "Bricolage Grotesque", "SF Pro Display", -apple-system,
    BlinkMacSystemFont, system-ui, Helvetica, Arial, sans-serif;
  --sans: "Onest", -apple-system, BlinkMacSystemFont, "SF Pro Text",
    system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Espaciado */
  --xs: 4px;  --sm: 8px;  --md: 16px;  --lg: 24px;
  --xl: 32px; --xxl: 48px; --xxxl: 80px;

  /* Radios amplios: el keynote se apoya en tarjetas redondeadas */
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 24px;
  --radius-pill: 999px;

  --page: 1280px;   /* columna más ancha: el índice respira */
  --rail: 176px;    /* raíl editorial (número + etiqueta) */

  /* Jerarquía de movimiento: micro = hover/foco · componente = tarjeta/menú ·
     sección = cambio de capítulo · wow = una sola vez, en la entrada. */
  --dur-micro: 180ms;
  --dur-component: 300ms;
  --dur-section: 550ms;
  --dur-wow: 750ms;
  --ease-out: cubic-bezier(0.2, 0.7, 0.3, 1);
}

* { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--label);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.6;
  position: relative;
}

/* Aurora: violeta centrada arriba y cian desplazada a la derecha, las dos muy
   diluidas y fijas. Es todo el fondo que hay; el grain solo le da grano. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(ellipse 62% 38% at 50% -8%, rgba(124, 107, 255, 0.26), transparent 68%),
    radial-gradient(ellipse 48% 30% at 88% 10%, rgba(38, 190, 207, 0.13), transparent 70%);
}

/* Grain de película global: SVG feTurbulence en data-URI, ESTÁTICO (sin
   keyframes): cero coste de animación y nada que apagar en reduce. Va por
   encima de todo, también del header. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 300;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url%28%23n%29'/%3E%3C/svg%3E");
  background-size: 240px 240px;
}

body > * { position: relative; z-index: 1; }

/* Enlaces de texto: subrayado animado en acento (1px → 2px al hover). */
a {
  color: var(--label);
  text-decoration: none;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1px;
  transition: background-size var(--dur-micro) var(--ease-out);
  padding-bottom: 1px;
}

a:hover { background-size: 100% 2px; }

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

/* Selección en naranja diluido: la hoja es casi negra y una selección sólida
   taparía el texto en vez de resaltarlo. */
::selection {
  background: rgba(124, 107, 255, 0.34);
  color: var(--label);
}

.skip-link {
  position: fixed;
  top: var(--sm);
  left: var(--md);
  z-index: 100;
  padding: 10px var(--md);
  border-radius: var(--radius-sm);
  background: var(--label);
  background-image: none;
  color: var(--bg);
  font-weight: 600;
  transform: translateY(calc(-100% - var(--lg)));
}

.skip-link:focus,
.skip-link:focus-visible {
  transform: none;
}

/* ---------------------------------------------------------------- iconos */

/* El sprite se declara una vez al principio del documento y cada icono lo
   instancia con <use>. Se saca del flujo en vez de con `display: none`, que
   en algunos navegadores impide que <use> resuelva la referencia. */
.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Trazo y no relleno; el color lo hereda del enlace que lo contiene. */
.ico {
  width: 1.05em;
  height: 1.05em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.16em;
}

/* ------------------------------------------------------------- monoespacia */

/* Meta-etiquetas: JetBrains Mono pequeño, mayúsculas, tracking abierto.
   Bricolage es solo para display/titulares/cifras; Onest para cuerpo y UI. */
.mono { font-family: var(--mono); }

.spec-num,
.spec-label,
.status,
.hero-meta,
.work-row-meta,
.xp-when {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* --------------------------------------------------------------- cabecera */

/* La barra no pinta nada: el fondo y el desenfoque viven en la píldora. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  padding-inline: var(--md);
}

/* Píldora centrada: marca con retrato a la izquierda, acciones a la derecha y
   un solo borde para las dos mitades. */
.site-header-inner {
  width: max-content;
  max-width: calc(100vw - 2 * var(--lg));
  margin: var(--md) auto;
  padding: 9px 10px 9px 9px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--lg);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: rgba(20, 20, 26, 0.72);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
}

.header-actions {
  display: flex;
  align-items: center;
  gap: var(--md);
}

/* Separador vertical entre marca y acciones, del ancho de un hairline. */
.header-actions::before {
  content: "";
  width: 1px;
  height: 18px;
  background: var(--hairline);
}

.header-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--label-secondary);
  font-size: 0.875rem;
  background-image: none;
  padding: 3px 0;
}

.header-link:hover { color: var(--label); }

/* Enlaces de sección dentro de la píldora. */
.header-nav {
  display: flex;
  align-items: center;
  gap: var(--lg);
  font-size: 0.875rem;
}

.header-nav a {
  color: var(--label-secondary);
  background-image: none;
}

.header-nav a:hover { color: var(--label); }

/* Marca: retrato + nombre en Bricolage. El retrato es decorativo (la misma
   cara, con alt, vive en la sección de contacto). */
.brand {
  display: inline-flex;
  align-items: baseline;
  gap: 0.28em;
  font-family: var(--display);
  font-weight: 700;
  font-size: 0.9375rem;
  letter-spacing: -0.01em;
  background-image: none;
  padding: 0;
  white-space: nowrap;
}

.brand-avatar {
  align-self: center;
  margin-right: 10px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--separator);
  display: block;
}

.brand-mark { color: inherit; }

.lang-switch {
  display: flex;
  gap: var(--xs);
  font-family: var(--mono);
  font-size: 0.75rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  padding: 2px;
}

.lang-switch a {
  color: var(--label-tertiary);
  padding: 3px var(--sm);
  border-radius: var(--radius-pill);
  background-image: none;
}

.lang-switch a:hover { color: var(--label); background-color: var(--bg-elevated); }

.lang-switch [aria-current="true"] {
  color: var(--label);
  background-color: var(--bg-elevated);
}

/* Barra de progreso de lectura: 2 px al pie de la cabecera, acento plano.
   Nace en scaleX(0) y la escribe micro-interactions.js con scroll + rAF; sin
   JS no se ve, que es el estado correcto para un adorno que mide el scroll. */
.scroll-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  pointer-events: none;
}

/* ----------------------------------------------------------------- layout */

main {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--lg);
}

/* Mantiene visibles los títulos al navegar desde enlaces internos bajo la
   cabecera sticky. */
:where(h1, h2, h3)[id] {
  scroll-margin-top: 6rem;
}

/* Sección: cabecera de dos columnas —número, etiqueta y titular a la
   izquierda; entradilla a la derecha— y contenido a todo ancho debajo. */
.spec {
  padding: var(--xxxl) 0;
  border-top: 1px solid var(--hairline);
}

.spec-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 400px;
  align-items: end;
  column-gap: var(--xxl);
  margin-bottom: var(--xl);
}

.spec-head-main { min-width: 0; }

.spec-eyebrow {
  margin: 0 0 var(--sm);
  color: var(--accent-strong);
}

/* El número de sección: mono, acento plano, sin degradado. */
.spec-num {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.spec-label { color: var(--label-muted); }

.spec-intro {
  margin: 0;
  color: var(--label-secondary);
  font-size: 1rem;
  line-height: 1.6;
}

.spec h2 { margin: 0; }

/* ------------------------------------------------------------ tipografía */

/* Bricolage Grotesque en display/titulares/cifras. El contraste lo dan el
   tamaño y el peso; el optical sizing sigue al tamaño real. */
h1 {
  font-family: var(--display);
  font-weight: 700;
  font-optical-sizing: auto;
  font-size: clamp(2rem, 5.6vw, 4.125rem);
  line-height: 1.04;
  letter-spacing: -0.03em;
  max-width: 1180px;
  margin: 0 0 var(--lg);
}

/* Frase acento del h1: degradado violeta → lavanda recortado al texto.
   Fallback: donde no haya background-clip, queda en violeta plano. */
.h1-mark {
  color: var(--accent-strong);
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .h1-mark {
    background-image: linear-gradient(100deg, var(--accent) 0%, var(--accent-strong) 55%, #D9D2FF 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

h2 {
  font-family: var(--display);
  font-weight: 700;
  font-optical-sizing: auto;
  font-size: clamp(1.875rem, 3.4vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: -0.025em;
  margin: 0 0 var(--md);
}

h3 {
  font-family: var(--display);
  font-size: 1.375rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0;
}

p { margin: 0 0 var(--md); color: var(--label-secondary); }
p strong { color: var(--label); font-weight: 500; }
p:last-child { margin-bottom: 0; }

.lead {
  font-size: clamp(1.0625rem, 1rem + 0.3vw, 1.1875rem);
  line-height: 1.55;
  max-width: 62ch;
}

/* ------------------------------------------------------------------- hero */

/* La tipografía ES el visual: sin canvas de partículas ni retícula; el fondo
   lo dan el halo y el grain globales. */
.hero {
  position: relative;
  padding: clamp(var(--xxl), 8vw, 120px) 0 var(--xxl);
  border-top: 0;
  display: block;
  text-align: center;
}

.hero h1,
.hero .lead { margin-inline: auto; }
.hero .cta-row { justify-content: center; }

.hero-meta {
  color: var(--label-secondary);
  margin: 0 auto var(--lg);
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--sm);
  align-items: center;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: rgba(20, 20, 26, 0.6);
  padding: 7px 16px 7px 13px;
}

/* Punto verde «disponible»: el único uso del verde semáforo fuera de
   status-live. Pulso sutil solo sin preferencia de movimiento reducido. */
.dot {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 0 3px var(--green-dim);
}

@keyframes dot-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--green-dim); }
  50% { box-shadow: 0 0 0 6px var(--green-dim); }
}

@media (prefers-reduced-motion: no-preference) {
  .dot { animation: dot-pulse 2.4s var(--ease-out) infinite; }
}

/* El titular llega pre-dividido en dos líneas desde el HTML —mismo marcado en
   los dos idiomas— y cada línea sube por su propio hueco. Sin JavaScript o
   con movimiento reducido el titular se pinta entero e inmediato.
   El padding/margin negativo de abajo deja sitio a las descendentes: con
   `overflow: hidden` y nada más, la «g» de «Engineer» se cortaba por abajo. */
/* Cada línea es un BLOQUE: si el titular envuelve en una pantalla estrecha,
   la línea crece y el `overflow: hidden` del reveal sigue sin cortar nada.
   El <br> del marcado se oculta (está ahí para el espacio accesible entre
   palabras, que el salto lo da ya el display: block). */
.h1-line { display: block; overflow: hidden;
           padding-bottom: 0.12em; margin-bottom: -0.12em; }
h1 br { display: none; }
.h1-line-inner { display: inline-block; }
@keyframes h1-line-in { from { transform: translateY(112%); } to { transform: none; } }
@media (prefers-reduced-motion: no-preference) {
  .h1-line-inner { animation: h1-line-in var(--dur-wow) var(--ease-out) backwards; }
  .h1-line:nth-of-type(2) .h1-line-inner { animation-delay: 0.14s; }
}

/* Reveal de titulares por líneas enmascaradas: los h2 llevan sus líneas
   pre-envueltas en .rl/.rl-in (sin <br>: el salto lo da el display: block, y
   así nth-child cuenta limpio). El stagger sale de :nth-child, sin delays
   inline. La puerta .js la pone micro-interactions.js como primera sentencia. */
.rl { display: block; overflow: hidden; padding-bottom: 0.12em; margin-bottom: -0.12em; }
.rl-in { display: block; }
@media (prefers-reduced-motion: no-preference) {
  .js [data-reveal] .rl-in {
    transform: translateY(110%);
    transition: transform var(--dur-wow) var(--ease-out);
  }
  .js [data-reveal].is-revealed .rl-in { transform: none; }
  .js [data-reveal] .rl:nth-child(2) .rl-in { transition-delay: 0.08s; }
  .js [data-reveal] .rl:nth-child(3) .rl-in { transition-delay: 0.16s; }
}

/* --------------------------------------------------------------- métricas */

/* Cifras en fila tipográfica editorial: hairlines arriba/abajo, etiqueta mono
   uppercase y número en Bricolage. El mismo tratamiento en hero y en filas. */
.metrics {
  display: flex;
  flex-wrap: wrap;
  gap: var(--xl);
  margin: var(--lg) 0;
  padding: var(--md) 0;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  list-style: none;
}

.metrics div {
  display: flex;
  flex-direction: column;
  gap: var(--xs);
}

/* Etiqueta arriba y dato debajo: se lee «Fuentes → MITECO», no al revés. */
.metrics dt {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--label-muted);
}

.metrics dd {
  margin: 0;
  font-family: var(--display);
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: -0.015em;
  /* Cifras de ancho fijo: la hoja de especificación no baila. */
  font-variant-numeric: tabular-nums;
}

.work-row-body .metrics dd { font-size: 1.25rem; }

/* ---------------------------------------------------- franja de clientes */

/* Marcas de terceros: el sitio no reproduce ni imita sus logotipos. Aquí van
   solo sus nombres, en texto. */
.client-strip {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--md) var(--xxl);
  padding: var(--xl) 0;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}

.client-strip-label {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--label-muted);
}

.client-strip ul {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--md) var(--xl);
  margin: 0;
  padding: 0;
  list-style: none;
}

.client-strip li {
  display: inline-flex;
  align-items: center;
  gap: var(--sm);
  font-family: var(--display);
  font-weight: 600;
  font-size: 1.3125rem;
  letter-spacing: -0.01em;
  color: var(--label-secondary);
}

/* -------------------------------------------------------------- acciones */

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md);
  margin-top: var(--lg);
}

.contact-actions { align-items: center; }

/* Patrón --active (0/1) que modula el glow; sin escala. Retintado a naranja. */
.btn { display: inline-flex; align-items: center; gap: var(--sm);
       padding: 11px var(--lg); border-radius: var(--radius-pill);
       font-size: 0.9375rem; font-weight: 500; white-space: nowrap;
       background-image: none;
       transition: transform var(--dur-micro) var(--ease-out),
                   border-color var(--dur-micro) var(--ease-out),
                   box-shadow var(--dur-micro) var(--ease-out),
                   opacity var(--dur-micro) var(--ease-out); }
.btn:active { transform: translateY(1px); }

.btn-primary {
  --active: 0;
  background-color: var(--accent);
  color: #FFFFFF;
  border: 1px solid var(--accent);
  box-shadow: 0 0 calc(var(--active) * 22px) rgba(124, 107, 255, calc(var(--active) * 0.42));
}
.btn-primary:hover { --active: 1; }

.btn-secondary { color: var(--label); border: 1px solid var(--separator); }
.btn-secondary:hover { border-color: var(--accent-hairline); color: var(--accent-strong); }

/* Versión compacta para la píldora de la cabecera. */
.btn-compact {
  height: 34px;
  padding-inline: var(--md);
  font-size: 0.875rem;
}

/* --------------------------------------------------------- experiencia */

/* Timeline tipográfico: cada entrada es una fila con hairline superior; año
   mono a la izquierda en columna fija, cliente en Onest 500 y
   descripción en secundario. Sin tinta viva ni dots animados. */
.xp-projects {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

.xp-item {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  padding: var(--lg) var(--xl);
}

/* El proyecto en curso ocupa la fila entera y lleva el acento. */
.xp-lead {
  grid-column: 1 / -1;
  padding: var(--xl) var(--xl) var(--lg);
  border-color: var(--accent-hairline);
  background:
    linear-gradient(100deg, var(--accent-dim) 0%, var(--bg-surface) 55%);
}

.xp-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--md);
  margin-bottom: var(--sm);
}

.xp-client {
  display: inline-flex;
  align-items: center;
  gap: var(--sm);
  font-family: var(--display);
  font-weight: 600;
  font-size: 1.375rem;
  letter-spacing: -0.015em;
}

.xp-lead .xp-client { font-weight: 700; font-size: 1.875rem; }

/* El resto no son tarjetas: filas con filete superior, más pequeñas, para que
   Inditex sea lo primero que se lee de la sección. */
.xp-item:not(.xp-lead) {
  border: 0;
  border-top: 1px solid var(--hairline);
  border-radius: 0;
  background: none;
  padding: var(--md) 0 0;
}

.xp-item:not(.xp-lead) .xp-client { font-size: 1.125rem; }

.xp-item:not(.xp-lead) .xp-desc { margin-bottom: var(--sm); font-size: 0.9375rem; }

.xp-when { color: var(--label-muted); white-space: nowrap; }

.xp-role {
  display: block;
  margin-bottom: var(--sm);
  font-size: 0.875rem;
  color: var(--accent-strong);
}

.xp-desc {
  margin: 0 0 var(--md);
  color: var(--label-secondary);
  line-height: 1.62;
}

.xp-stack {
  display: block;
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.7;
  color: var(--label-muted);
}

/* --------------------------------------------------------- filas-índice */

/* Cada proyecto es una tarjeta elevada. Las de producto propio reparten
   texto a la izquierda y pila de dispositivos a la derecha; las de
   herramientas son de una sola columna y van en rejilla 2x2. */
.work-list {
  list-style: none;
  margin: var(--xl) 0 0;
  padding: 0;
  display: grid;
  gap: var(--lg);
}

.work-card {
  display: flex;
  align-items: center;
  gap: var(--xxl);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  background: linear-gradient(180deg, var(--bg-surface) 0%, #0E0E14 100%);
  padding: var(--xl);
}

.work-row-body { flex: 1 1 auto; min-width: 0; }

.work-shot { flex: none; }

.tool-list {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.tool-card {
  display: block;
  background: var(--bg-elevated);
}

.work-row-head {
  display: flex;
  align-items: center;
  gap: var(--md);
  margin-bottom: var(--md);
}

.work-row-titles { min-width: 0; }

.work-row-name {
  display: flex;
  align-items: center;
  gap: var(--sm);
  flex-wrap: wrap;
}

.work-row-head h3 {
  font-family: var(--display);
  font-weight: 700;
  font-optical-sizing: auto;
  font-size: clamp(1.5rem, 2.4vw, 1.6875rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin: 0;
  transition: color var(--dur-component) var(--ease-out),
              transform var(--dur-component) var(--ease-out);
}

@media (hover: hover) {
  .work-card:hover h3 { color: var(--accent); }
}

.work-row-meta {
  margin: 5px 0 0;
  color: var(--label-muted);
}

/* Solo las dos apps llevan distintivo, y es su icono de verdad: el radio es
   la proporción del recorte de iOS; la imagen se queda cuadrada y la esquina
   la pone el CSS. */
.work-appicon {
  width: 54px;
  height: 54px;
  flex: none;
  align-self: center;
  border-radius: 13px;
  border: 1px solid var(--hairline);
  object-fit: cover;
  background: var(--bg-surface);
}

.work-row-body p { max-width: 68ch; }

.status {
  padding: 3px var(--sm);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* Verde semántico: solo «en la App Store» y el punto de disponibilidad. */
.status-live { color: var(--green); background: var(--green-dim); }
.status-soon { color: var(--accent); background: var(--accent-dim); }

/* Tarjeta de herramienta: kicker mono, titular, texto, una línea de cifras
   y una de stack. Sin métricas sueltas: la cifra va donde se puede leer. */
.tool-kicker {
  margin: 0 0 var(--md);
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--label-muted);
}

/* El titular pesa lo mismo que el de las tarjetas de producto: son proyectos
   de primera, no notas al pie. Un filete de acento debajo remata la cabecera
   de la tarjeta. */
.tool-card h3 {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-optical-sizing: auto;
  font-size: clamp(1.5rem, 2.4vw, 1.6875rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.tool-card .work-row-name {
  margin-bottom: var(--md);
  padding-bottom: var(--md);
  border-bottom: 1px solid var(--accent-hairline);
}

.tool-card p { color: var(--label-secondary); }

.tool-stat {
  margin: var(--md) 0 var(--xs);
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--label);
}

.tool-stack {
  margin: 0 0 var(--md);
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.7;
  color: var(--label-muted);
}

/* Los enlaces de la fila: cada uno es un destino con su icono, en gris
   contenido para que haya una sola acción principal por fila. */
.work-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sm);
  margin-top: var(--md);
  font-size: 0.875rem;
}

.work-links a {
  display: inline-flex;
  align-items: center;
  gap: var(--sm);
  padding: 8px 13px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background-image: none;
  color: var(--label-secondary);
  transition: border-color var(--dur-micro) var(--ease-out),
              color var(--dur-micro) var(--ease-out),
              background-color var(--dur-micro) var(--ease-out);
}

.work-links a .ico { color: var(--accent); }

.work-links a:hover {
  color: var(--label);
  border-color: var(--separator);
  background-color: rgba(235, 235, 245, 0.05);
}

/* El badge oficial de Apple, no una reproducción: ni se recolorea, ni se le
   cambian las esquinas, ni se le añaden efectos. */
.work-links .app-store-badge {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  transition: opacity var(--dur-micro) var(--ease-out);
}

.work-links .app-store-badge:hover {
  background-color: transparent;
  opacity: 0.86;
}

/* Medidas fijas en la proporción exacta del lockup: ni deformación ni salto
   de layout mientras carga. */
/* Chip de la web del producto: icono real de la app + «Web», a la altura
   del badge de App Store para que la fila alinee. */
.work-links .site-chip {
  height: 40px;
  padding: 0 14px 0 8px;
  border: 1px solid var(--separator);
  border-radius: 10px;
  color: var(--label);
  font-weight: 500;
}

.site-chip img {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  border: 1px solid var(--hairline);
}

.work-links .site-chip:hover {
  border-color: var(--accent-hairline);
  color: var(--accent-strong);
}

.work-links .app-store-badge img {
  display: block;
  width: 119.664px;
  height: 40px;
}

/* Pila de dispositivos: tres capturas en marco, la del centro al frente y
   dos abanicadas detrás. El marco es la imagen de producto del iPhone 17 Pro
   (Deep Blue) de Apple, usada para mostrar apps propias; la captura va
   debajo, recortada al hueco de pantalla del marco. */
.work-shot {
  margin: 0;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 1px;
  max-width: 100%;
  /* Ancho reservado para el abanico: las dos traseras salen del flujo, así
     que sin esto la pila se saldría de la tarjeta. */
  width: 368px;
}

.device {
  position: relative;
  width: 196px;
  aspect-ratio: 1350 / 2760;
  /* La caja ya no es rectangular: la sombra sigue al alfa del marco. */
  filter: drop-shadow(0 24px 44px rgba(0, 0, 0, 0.62));
}

/* La captura, en el hueco de pantalla del marco. Medido sobre el alfa del
   PNG de 1350x2760: hueco de 1206x2622 —exactamente la resolución nativa de
   las capturas— con origen en (72, 69). De ahí top 2.5%, left 5.333%,
   89.334% x 95%; al coincidir las proporciones, object-fit: cover no recorta
   nada. El radio mínimo sin fuga por la esquina curva del cuerpo es 73px
   (6.05% del ancho del hueco): 7%/3.4% lo cubre. Un <img> absoluto no se
   estira con inset, así que el tamaño va explícito. */
.device img {
  position: absolute;
  top: 2.5%;
  left: 5.333%;
  width: 89.334%;
  height: 95%;
  object-fit: cover;
  border-radius: 7% / 3.4%;
  background: #000;
}

.device .device-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
  background: none;
}

/* Las dos de detrás salen del flujo para que la pila no ensanche la columna. */
.device-back {
  position: absolute;
  top: 50%;
  left: 50%;
  opacity: 0.72;
}

.device-back-left  { transform: translate(-50%, -50%) translateX(-86px) rotate(-8deg); }
.device-back-right { transform: translate(-50%, -50%) translateX(86px) rotate(8deg); }

/* ---------------------------------------------------------------- contacto */

/* Panel con su propia aurora: retrato a la izquierda, texto a la derecha. */
.spec-contact { border-top: 0; padding-bottom: var(--xxl); }

.contact-panel {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  gap: var(--xxl);
  align-items: center;
  padding: var(--xxl);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(700px 300px at 50% 110%, rgba(124, 107, 255, 0.28) 0%, rgba(124, 107, 255, 0) 70%),
    linear-gradient(180deg, #14141C 0%, #0C0C11 100%);
}

.contact-body { min-width: 0; }
.contact-body h2 { margin: 0 0 var(--sm); }
.contact-body > p { margin: 0 0 var(--xl); max-width: 52ch; color: var(--label-secondary); }

.portrait {
  margin: 0;
  width: 232px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--hairline);
}

.portrait img {
  display: block;
  width: 100%;
  height: 290px;
  object-fit: cover;
  object-position: center 16%;
}

/* -------------------------------------------------------------------- pie */

.site-footer {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--xl) var(--lg) var(--xxl);
  border-top: 1px solid var(--hairline);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--md);
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--label-tertiary);
}

.site-footer nav { display: flex; flex-wrap: wrap; gap: var(--md); }
.site-footer nav a { color: var(--label-secondary); background-image: none; }
.site-footer nav a:hover { color: var(--label); }

/* El bloque legal vive dentro del footer: ocupa toda la fila superior y se
   separa del resto con un hairline. */
.footer-line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sm) var(--xl);
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--label-muted);
}

/* El aviso legal se pliega: obligatorio, pero no es el remate de la página. */
.footer-legal {
  border-top: 1px solid var(--hairline);
  padding-top: var(--md);
  color: var(--label-secondary);
  font-size: 0.8125rem;
}

.footer-legal summary {
  cursor: pointer;
  color: var(--label-muted);
  font-family: var(--mono);
  font-size: 0.75rem;
}

.footer-legal dl {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--xs) var(--md);
  margin: var(--sm) 0 0;
}

.footer-legal dt {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--label-muted);
}

.footer-legal dd { margin: 0; color: var(--label-secondary); }

/* -------------------------------------------------------------------- 404 */

.notfound {
  position: relative;
  min-height: 340px;
  text-align: center;
  padding: var(--xxxl) 0;
  border-top: 0;
  display: block;
}

/* Lluvia de tokens de Swift detrás del 404. Solo en esta página. Lo revela
   matrix-404.js. Los dos elementos van posicionados, así que manda el orden
   del documento: el lienzo va primero y el texto se pinta encima. */
.matrix[hidden] { display: none; }

/* La máscara es un anillo, no un foco: se abre un hueco en el centro para que
   la lluvia no pase por detrás del texto. */
.matrix {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0.6;
  border-radius: var(--radius-lg);
  -webkit-mask-image: radial-gradient(ellipse 68% 62% at 50% 50%,
    transparent 0%, transparent 34%, #000 62%, transparent 100%);
  mask-image: radial-gradient(ellipse 68% 62% at 50% 50%,
    transparent 0%, transparent 34%, #000 62%, transparent 100%);
}

.notfound > :not(.matrix) { position: relative; }

/* El código del 404 en Bricolage violeta plano: nada de degradado. */
.notfound-code {
  font-family: var(--display);
  font-weight: 350;
  font-optical-sizing: auto;
  font-size: clamp(3.5rem, 14vw, 9rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--accent);
  margin: 0 0 var(--md);
}

/* --------------------------------------------------------------- entrada */

/* Entrada escalonada de las secciones; el hero pinta inmediato porque su
   titular es la prueba principal para recruiter. */
@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

main > * { animation: rise var(--dur-wow) var(--ease-out) backwards; }
main > *:nth-child(1) { animation-delay: 0.04s; }
main > *:nth-child(2) { animation-delay: 0.12s; }
main > *:nth-child(3) { animation-delay: 0.18s; }
main > *:nth-child(4) { animation-delay: 0.24s; }
main > *:nth-child(n + 5) { animation-delay: 0.3s; }

main > .hero { animation: none; }

/* ------------------------------------------------------ reveals al scroll */

/* Los reveals los gobierna IntersectionObserver (micro-interactions.js añade
   .is-revealed al entrar en viewport). La puerta `.js` la pone ese mismo
   script como PRIMERA sentencia: sin JavaScript nada queda oculto — mejora
   progresiva estricta. Con movimiento reducido el bloque ni siquiera se
   aplica. */
@media (prefers-reduced-motion: no-preference) {
  .js [data-reveal] {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity var(--dur-section) var(--ease-out),
                transform var(--dur-section) var(--ease-out);
  }
  .js [data-reveal].is-revealed { opacity: 1; transform: none; }
}

/* --------------------------------- reveal de capturas (scroll-driven) */

/* Mejora progresiva: solo donde existe animation-timeline Y sin preferencia
   de movimiento reducido. Sin soporte el teléfono simplemente se muestra; la
   fila ya entra con data-reveal. `animation-timeline:` SOLO puede vivir
   dentro de este bloque (regla del validador). */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .work-shot .device {
      animation: shot-reveal linear both;
      animation-timeline: view();
      animation-range: entry 10% entry 60%;
    }
  }
}

@keyframes shot-reveal {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* --------------------------------------------------------- botón magnético */

/* Magnetismo del botón primario: --tx/--ty las escribe magnetic-buttons.js con
   su lerp dentro del rAF compartido, así que la transición CSS NO puede
   incluir transform en este contexto: una transición encima del lerp lo
   volvería gomoso. Sin JavaScript las variables caen en 0px y la declaración
   es una identidad inofensiva. El :active repone el hundido de 1 px encima
   del translate magnético para convivir con la regla global .btn:active. */
@media (hover: hover) and (pointer: fine) {
  .btn-primary {
    transform: translate(var(--tx, 0px), var(--ty, 0px));
    transition: border-color var(--dur-micro) var(--ease-out),
                box-shadow var(--dur-micro) var(--ease-out),
                opacity var(--dur-micro) var(--ease-out);
  }

  .btn-primary:active {
    transform: translate(var(--tx, 0px), var(--ty, 0px)) translateY(1px);
  }
}

/* ---------------------------------------------------------------- móviles */

@media (max-width: 1080px) {
  .spec-head { grid-template-columns: minmax(0, 1fr); row-gap: var(--md); }
  .spec-intro { max-width: 68ch; }

  /* La pila de dispositivos baja debajo del texto. */
  .work-card { flex-direction: column; align-items: stretch; gap: var(--lg); }
  .work-shot { align-self: center; }
}

@media (max-width: 860px) {
  :where(h1, h2, h3)[id] { scroll-margin-top: 7rem; }

  .spec { padding: var(--xxl) 0; }

  .hero { padding: var(--xxl) 0; }

  .header-nav { display: none; }

  .client-strip { gap: var(--sm) var(--lg); }

  .xp-projects { grid-template-columns: minmax(0, 1fr); }

  .work-card { padding: var(--lg); }

  .tool-list { grid-template-columns: minmax(0, 1fr); }

  .contact-panel { grid-template-columns: minmax(0, 1fr); gap: var(--lg); padding: var(--xl); }
  .portrait { width: 180px; }
  .portrait img { height: 225px; }

  .footer-legal dl { grid-template-columns: 1fr; gap: 0; }
  .footer-legal dt { margin-top: var(--sm); }
}

/* ------------------------------------------------------- táctil sin hover */

/* En táctil el :hover se queda pegado tras el toque: se apaga solo lo que se
   mueve —elevación y zoom—; el cambio de color se queda como respuesta al
   toque. */
@media (hover: none) {
  .work-card:hover { transform: none; }
}

/* Mínimo táctil de 44 px (HIG) solo cuando el puntero primario es el dedo:
   con ratón no cambia un píxel del ritmo aprobado. La puerta es el puntero y
   no el ancho: un iPad apaisado navega a 1024 px o más y también toca. */
@media (pointer: coarse) {
  .work-links a { min-height: 44px; }

  .site-header-inner { padding-block: 8px; }

  .brand,
  .header-link,
  .lang-switch a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* En flex, el espacio entre «Oscar» y el <span> se colapsa: se repone. */
  .brand { justify-content: flex-start; gap: 0.28em; }
  .lang-switch a { min-width: 44px; }

  .site-footer nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

@media (max-width: 520px) {
  /* La píldora deja de medir por contenido: se ajusta al ancho disponible. */
  .site-header-inner {
    width: auto;
    max-width: none;
    padding: 6px 8px;
    gap: var(--sm);
  }
  .header-actions { gap: var(--xs); }
  .header-actions::before { display: none; }
  .brand-avatar { margin-right: 8px; }

  .brand,
  .lang-switch a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* En móvil no cabe todo: GitHub y el CTA de contacto se caen de la píldora.
     Los dos siguen en el hero, en la sección de contacto y en el pie. Va
     DESPUÉS del grupo de arriba: si no, ese `display: inline-flex` los
     resucitaría. */
  .header-link,
  .header-actions .btn-compact { display: none; }

  .brand { justify-content: flex-start; gap: 0.28em; }
  .lang-switch { gap: 0; }
  .lang-switch a { min-width: 44px; padding-inline: 0; }

  /* Una columna: la dirección de correo no cabe en media fila a 390 px. */
  .hero .cta-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
  }

  .hero .cta-row .btn {
    width: 100%;
    justify-content: center;
  }

  /* La pila cabe en 320 px sólo si se estrecha y se junta el abanico. */
  .work-shot { width: 274px; }
  .device { width: 150px; }
  .device-back-left  { transform: translate(-50%, -50%) translateX(-62px) rotate(-8deg); }
  .device-back-right { transform: translate(-50%, -50%) translateX(62px) rotate(8deg); }

  main { padding-inline: var(--md); }
}

/* ----------------------------------------------------------- más contraste */

/* Quien pide más contraste sube la microtipografía y los filetes. Solo se
   sobreescriben tokens: la declaración base de --label-tertiary en :root
   está fijada literalmente por validate-site.mjs y no se toca. */
@media (prefers-contrast: more) {
  :root {
    --label-secondary: rgba(235, 235, 245, 0.85);
    --label-tertiary: rgba(235, 235, 245, 0.76);
    --hairline: rgba(235, 235, 245, 0.28);
  }
}

/* ------------------------------------------------------ movimiento reducido */

/* Las cinco reglas de arriba —hasta `.spec-label { animation: none; }`— están
   fijadas literalmente por validate-site.mjs (selectores, agrupaciones y
   orden dentro de cada grupo): no se reordenan ni se reescriben. Todo lo
   demás del reduce va DEBAJO de ellas. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  main > * { animation: none; }
  .proof-tile,
  .work-card,
  .btn,
  a { transition: none; }
  .proof-tile:hover,
  .work-card:hover,
  .btn:active { transform: none; }
  .proof-media img,
  .chip { transition: none; }
  .proof-tile:hover .proof-media img { transform: none; }
  .work-card,
  .xp-projects li,
  .spec-label { animation: none; }

  /* === debajo: anulaciones v3 (tras las ancladas) === */
  [data-reveal] { transition: none; opacity: 1; transform: none; }
  .h1-line-inner { animation: none; }
  .rl-in { transform: none; transition: none; }
  .scroll-progress { transform: none; }
  /* (el grain es estático: no hay keyframes que anular) */

  /* Pulso del punto de disponibilidad. */
  .dot { animation: none; }
  .work-row-head h3,
  .work-links a { transition: none; }
  .work-card:hover h3 { transform: none; }
}

/* El botón magnético: sin transición que pelee con el lerp ni hundido. */
@media (prefers-reduced-motion: reduce) and (hover: hover) and (pointer: fine) {
  .btn-primary { transition: none; }
  .btn-primary:active { transform: none; }
}
