/* ========================================================================
   Perigami - hoja de estilos
   Extraida de index.php; se carga con <link rel="stylesheet"> desde el <head>.
   ======================================================================== */

/* ── Fuente: Ubuntu Mono auto-hospedada ────────────────────────────────────
   Antes venía de fonts.googleapis.com. Auto-hospedarla quita dos dominios
   externos de la carga inicial (una petición al CSS de Google + otra al
   .woff2 de gstatic), y con eso la dependencia de red de terceros y el dato
   de IP que cada visita le entregaba a Google.
   Los cuatro archivos son los mismos subsets que servía Google (v19), con
   sus unicode-range originales: el navegador solo baja "latin" salvo que la
   página use de verdad un carácter del rango extendido, así que partirlos
   así pesa menos que un único archivo con todo adentro.
   font-display:swap conserva el comportamiento anterior: el texto se ve de
   inmediato con la fuente de respaldo y cambia al llegar Ubuntu Mono. Ojo:
   ese cambio sigue siendo asíncrono, así que el recálculo de clip-paths
   atado a document.fonts.ready (ver perigamiscripts.js) sigue haciendo
   falta — auto-hospedar acelera la carga, no la vuelve síncrona. */
@font-face {
  font-family: 'Ubuntu Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ubuntu-mono-400-latin.woff2') format('woff2');
  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: 'Ubuntu Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ubuntu-mono-400-latin-ext.woff2') format('woff2');
  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: 'Ubuntu Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/ubuntu-mono-700-latin.woff2') format('woff2');
  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: 'Ubuntu Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/ubuntu-mono-700-latin-ext.woff2') format('woff2');
  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;
}

/* ── Variables ─────────────────────────────────────────────────────────── */
:root {
  --cyan:    #009DD8;
  --green:   #6DD900;
  --orange:  #FF7100;
  --pink:    #F6006A;
  --blue:    #1700BC;
  --yellow:  #FFD900;
  --bg:      #000;
  --surface: #0c0c1b;
  --border:  #1c1c3a;
  --text:    #bfcfe0;
  --muted:   #6d80a0; /* mismo matiz que antes, aclarado para pasar AA (4.5:1) sobre --surface */
  --font:    'Ubuntu Mono', monospace;
  --gap:          clamp(.65rem, 2.2vw, 1.1rem);
  /* Bisel de modales: esquinas superior-izquierda e inferior-derecha dobladas */
  --cut-lg:   10px;
  --cut-lg-h: var(--cut-lg);
  --clip-lg: polygon(var(--cut-lg-h) 0, 100% 0, 100% calc(100% - var(--cut-lg)),
                     calc(100% - var(--cut-lg-h)) 100%, 0 100%, 0 var(--cut-lg));
  /* Anillo (marco) del mismo hexágono, para el borde del modal */
  --ring-w: 2px;
  --ring-w-diag: calc(var(--ring-w) * .41421);
  --clip-lg-ring: polygon(
    var(--cut-lg-h) 0, 100% 0, 100% calc(100% - var(--cut-lg)),
    calc(100% - var(--cut-lg-h)) 100%, 0 100%, 0 var(--cut-lg), var(--cut-lg-h) 0,
    calc(var(--cut-lg-h) + var(--ring-w-diag)) var(--ring-w),
    var(--ring-w) calc(var(--cut-lg) + var(--ring-w-diag)),
    var(--ring-w) calc(100% - var(--ring-w)),
    calc(100% - var(--cut-lg-h) - var(--ring-w-diag)) calc(100% - var(--ring-w)),
    calc(100% - var(--ring-w)) calc(100% - var(--cut-lg) - var(--ring-w-diag)),
    calc(100% - var(--ring-w)) var(--ring-w),
    calc(var(--cut-lg-h) + var(--ring-w-diag)) var(--ring-w)
  );
  --max-w:        62vw;
  --max-w-mobile: 92vw;
  --max-w-logo:   540px;

  --cols-desktop: 7;
  --cols-tablet:  5;
  --cols-mobile:  3;
  --breakpoint:        900px;  /* desde aquí, desktop */
  --breakpoint-mobile: 600px;  /* por debajo, móvil */
  --fs:      clamp(.85rem, 2.1vw, 1.02rem);
  --fs-sm:   clamp(.75rem, 1.8vw, .88rem);
  /* Gap entre triángulos Y radio de redondeo de esquina. */
  --tri-gap: 6px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { user-select: none; }

/* Patrón + viñeta: pintados como fondo del propio body, no como
   pseudo-elementos fixed;inset:0 aparte. Eran body::before (patrón) y
   html::before (viñeta) hasta que algún bloqueador de anuncios dejó de
   mostrarlos: un elemento fixed/inset:0/pointer-events:none con
   background-image o con un degradado hacia negro es la forma de un overlay
   que esos filtros ocultan o "verifican y sueltan" con retraso —de ahí el
   parpadeo que tenía la viñeta— igual que pasaba con las tarjetas del
   mosaico (ver CLAUDE.md §9). body ya cubre la pantalla completa
   (100vh/100dvh) por diseño, así que no hace falta un overlay aparte.

   Dos capas en el mismo background-image, la viñeta primero (se lista
   encima) y el patrón detrás; `background-size`/`background-repeat` llevan
   la misma cantidad de valores, en el mismo orden, uno por capa. La
   opacidad del patrón queda horneada en el propio SVG (`fill-opacity='.12'`)
   porque `opacity` no se le puede poner a una sola capa de un fondo con
   varias. El charco de luz (`body::after`, más abajo) SÍ sigue aparte: su
   pulso es una animación de `opacity` del elemento entero, y eso no se
   puede aplicar a una sola capa de un fondo compuesto. */
body {
  font-family: var(--font);
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 70% 70% at 50% 50%, transparent 30%, rgba(0,0,0,1) 100%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 54.12 93.75'%3E%3Cpath fill='%23009DD8' fill-opacity='.12' d='M37.64,0l14.15,24.5c.52.9,1.43,1.35,2.34,1.35V0h-16.48ZM7.01,29.9h40.1c2.08,0,3.38-2.25,2.34-4.05L34.52,0h-14.92L4.68,25.85c-1.04,1.8.26,4.05,2.34,4.05ZM51.79,38l-20.05,34.72c-1.04,1.8.26,4.05,2.34,4.05h20.05v-40.12c-.91,0-1.82.45-2.34,1.35ZM47.11,32.6H7.01c-2.08,0-3.38,2.25-2.34,4.05l20.05,34.72c1.04,1.8,3.64,1.8,4.68,0l20.05-34.72c1.04-1.8-.26-4.05-2.34-4.05ZM22.38,72.72L2.34,38c-.52-.9-1.43-1.35-2.34-1.35v40.12h20.05c2.08,0,3.38-2.25,2.34-4.05ZM31.74,83.52l5.9,10.22h16.48v-14.27h-20.05c-2.08,0-3.38,2.25-2.34,4.05ZM2.34,24.5L16.48,0H0v25.85c.91,0,1.82-.45,2.34-1.35ZM24.72,84.87l-5.12,8.88h14.92l-5.12-8.88c-1.04-1.8-3.64-1.8-4.68,0ZM20.04,79.47H0v14.27h16.48l5.9-10.22c1.04-1.8-.26-4.05-2.34-4.05Z'/%3E%3C/svg%3E");
  background-size: 100% 100%, 140px 242.49px;
  background-repeat: no-repeat, repeat;
  color: var(--text);
  height: 100vh;
  height: 100dvh;
  display: flex;
  justify-content: center;
  padding: var(--gap);
  overflow: hidden;
}
/* Charco de luz */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  background: radial-gradient(ellipse 62% 52% at 50% 42%, rgba(0,157,216,.13) 0%, transparent 68%);
  animation: bg-pulse 10s ease-in-out infinite;
  pointer-events: none;
  z-index: -2;
}
@keyframes bg-pulse {
  0%, 100% { opacity: .35; }
  50%      { opacity:  1;  }
}

body.modal-open { overflow: hidden; }

/* ── Container ─────────────────────────────────────────────────────────── */
.container {
  position: relative;
  width: 100%;
  max-width: var(--max-w);
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

/* Los px deben coincidir con --breakpoint / --breakpoint-mobile (CSS no permite var() dentro de @media) */
@media (max-width: 899px) {
  .container { max-width: 82vw; }
}
@media (max-width: 600px) {
  .container { max-width: var(--max-w-mobile); }
}

/* ── Logo ──────────────────────────────────────────────────────────────── */
/* El layout (ancho máximo, centrado, aire lateral) vive en .logo-row, no en
   el propio <header>: así header queda pegado 1:1 al dibujo del logo (sin su
   propio padding) y .logo-visual —adentro— puede posicionar el "+" en % que
   sí coinciden con el arte, sin tener que descontar ningún padding ajeno. */
.logo-row {
  flex-shrink: 0;
  padding: 0 calc(var(--gap) * 2);
  max-width: var(--max-w-logo);
  align-self: center;
  width: 100%;
}
header { width: 100%; }
/* Trato interactivo del logo, compartido por las dos instancias clickeables de la página */
.logo-trigger { cursor: pointer; transform-origin: bottom center; width: 100%; }
/* Envoltorio ceñido al dibujo (mismo viewBox 134.6x35 del SVG, ver
   assets/logoperigami.svg): sin padding propio, así el % de .plus-trigger
   cae exactamente donde cae en el arte, no en el header con aire alrededor.
   aspect-ratio explícito (no height:auto) porque un % de alto en un hijo con
   position:absolute solo resuelve contra una altura DEFINIDA del padre — con
   el alto en "auto" (el que da el propio <svg>) el navegador lo trata como
   indefinido y el % de top/height del "+" no calcularía nada. */
.logo-visual { position: relative; aspect-ratio: 134.6 / 35; }
/* El "+" nace fuera del propio dibujo (a la derecha del todo, como un
   superíndice de marca — "Perigami+"), por eso left pasa de 100%: no hay
   padding en .logo-visual que lo empuje solo, hay que correrlo a mano. */
.plus-trigger {
  position: absolute;
  top: 6%;
  left: 100%;
  width: 8%;
  aspect-ratio: 1;
  padding: 0;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
}
.plus-svg { display: block; width: 100%; height: 100%; overflow: visible; }
.plus-trigger .cls-1 { fill: var(--cyan); transition: fill .2s ease; }
.plus-trigger:hover .cls-1,
.plus-trigger:focus-visible .cls-1,
.plus-trigger:active .cls-1 { fill: #fff; }
.plus-trigger:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; }
/* El logo se despliega hasta quedar plano en vez de estirarse */
@keyframes logo-unfold {
  from { transform: perspective(900px) rotateX(-16deg); }
  to   { transform: perspective(900px) rotateX(0deg);   }
}
/* :not(:has(...)) descarta el hover cuando el puntero está sobre el "+"
   anidado adentro (ver .plus-trigger): ese botón ya tiene su propia reacción
   (se pone blanco él solo) y no debe además desplegar/iluminar todo el logo,
   serían dos elementos reaccionando a la vez al mismo puntero. */
.logo-trigger:hover:not(:has(.plus-trigger:hover)) { animation: logo-unfold 420ms cubic-bezier(.22,.68,.35,1) forwards; }
.logo-trigger:focus-visible { outline: 2px solid var(--cyan); outline-offset: 4px; }
.logo-svg { display: block; width: 100%; height: auto; overflow: visible; }

/* El logo del encabezado arranca invisible y lo revela el JS recién cuando el
   mosaico ya está armado (ver el bloque de entrada en perigamiscripts.js).
   Sin esto se ve un rato largo de logo enorme y solo: perigamiscripts.js va
   con defer al final del body, así que el navegador pinta el HTML ya parseado
   ANTES de ejecutarlo — y en ese pintado el logo viene completo y a ancho
   completo, mientras las tarjetas (position:absolute) todavía no tienen
   left/top/width/height y no ocupan lugar. Debajo del logo no hay nada.

   Limitado a html:not(.no-js) porque quien revela es el script: sin JS no
   habría quien lo hiciera y el logo quedaría oculto para siempre. La clase
   no-js la saca un <script> inline del <head>, no este archivo, así que la
   regla ya decide bien en el primer pintado y no un cuadro después. */
html:not(.no-js) #logoHeader         { opacity: 0; }
html:not(.no-js) #logoHeader.is-in   { opacity: 1; }

/* Relleno del grupo de letras */
@keyframes logo-shimmer {
  0%, 100% { filter: hue-rotate(0deg)   saturate(1)    brightness(1);    }
  50%      { filter: hue-rotate(18deg)  saturate(1.35) brightness(1.07); }
}
.logo-svg .perigami { animation: logo-shimmer 7s ease-in-out infinite; }

/* El path de fondo ("borde") no aparece con un fundido parejo */
.logo-svg .borde {
  fill: #fff;
  clip-path: circle(0% at var(--mx, 50%) var(--my, 50%));
  transition: clip-path .65s cubic-bezier(.16,.84,.44,1);
}
/* Mismo :not(:has(...)) que el desplegado de arriba: el "+" tiene su propio
   blanco y no debe encender también el del logo entero. */
.logo-trigger:hover:not(:has(.plus-trigger:hover))  .logo-svg .borde,
.logo-trigger:focus-visible:not(:has(.plus-trigger:focus-visible)) .logo-svg .borde,
.logo-trigger:active:not(:has(.plus-trigger:active)) .logo-svg .borde {
  clip-path: circle(150% at var(--mx, 50%) var(--my, 50%));
}

/* ── Main / Masonry ────────────────────────────────────────────────────── */
/* scrollbar-gutter:stable both-edges reserva el mismo espacio de la barra
   de scroll a los dos lados, exista scroll o no — sin esto, el mosaico se
   ve centrado cuando el contenido cabe entero (sin scroll) pero corrido a
   la izquierda apenas aparece la barra (que solo resta ancho a la
   derecha), dependiendo de cuántas filas haya en cada momento. */
main {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-gutter: stable both-edges;
}

/* Mosaico de triángulos que comparten arista de verdad —no una grilla de celdas cuadradas */
.masonry {
  flex: 1;
  min-height: 0;
  position: relative;
  overflow: visible;
  perspective: 1100px;
}
/* Sin JS: no hay forma de calcular el mosaico solapado */
.no-js .masonry {
  display: grid;
  grid-template-columns: repeat(var(--cols-mobile), 1fr);
  gap: 6px;
  padding: .6rem 0;
}
@media (min-width: 600px) { .no-js .masonry { grid-template-columns: repeat(var(--cols-tablet), 1fr); } }
@media (min-width: 900px) { .no-js .masonry { grid-template-columns: repeat(var(--cols-desktop), 1fr); } }
.no-js .project-item        { position: static; aspect-ratio: 1 / .866; }
.no-js .project-item.prior  { grid-column: span 2; grid-row: span 2; }
.no-js .project-item.huge   { grid-column: span 3; grid-row: span 3; }
.no-js .project-item:nth-child(odd)  .project-card { clip-path: polygon(50% 8%, 92% 92%, 8% 92%); }
.no-js .project-item:nth-child(even) .project-card { clip-path: polygon(8% 8%, 92% 8%, 50% 92%); }

/* ── Tarjeta = triángulo recortado ─────────────────────────────────────── */
/* pointer-events:none acá es la clave del hover */
.project-item {
  position: absolute;
  pointer-events: none;
  filter: drop-shadow(0 2px 2px rgba(0,0,0,.55));
}

/* Borde de hover: un único elemento COMPARTIDO (creado por JS, ver el bloque de hover/focus al final del body) */
.hover-ring {
  position: absolute;
  z-index: 100;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}
.hover-ring.is-visible { opacity: 1; }

.project-card {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: var(--surface);
  background-image: var(--prev, none);
  transition: background-color .4s ease;
  background-size: cover;
  background-position: var(--bg-x, center) var(--bg-y, center);
  text-decoration: none;
  cursor: pointer;
  display: block;
  /* .project-item (el padre) tiene pointer-events:none */
  pointer-events: auto;
  /* --tri-clip lo calcula JS (applyClips) por tarjeta. */
  clip-path: var(--tri-clip, none);
  /* Inner-glow del --paper de ESTA tarjeta (su color promedio, ya subido de
     saturación/claridad al calcularlo — ver index.php §4.2): remarca el
     contorno en las tarjetas oscuras, que si no se pierden contra el fondo
     oscuro del mosaico. clip-path recorta el box-shadow igual que recorta
     todo lo demás, así que el brillo sigue el triángulo, no el rectángulo.
     El blur grande con spread negativo lo concentra cerca del borde en vez
     de inundar toda la tarjeta. */
  box-shadow:
    inset 0 0 30px 5px color-mix(in srgb, var(--paper, transparent) 65%, transparent);
}
.project-card:not([style]) {
  background-image: linear-gradient(135deg,
    rgba(0,157,216,.18) 0%, rgba(109,217,0,.10) 50%, rgba(246,0,106,.14) 100%);
}

/* Arrastrar y soltar (ver JS al final del body): la tarjeta que se arrastra se atenúa */
.project-card.is-dragging { opacity: .35; }
/* El borde blanco (arriba) ya marca el hover/foco */
.project-card:focus-visible { outline: none; }

/* Degradé sutil sobre la imagen, para que el hover se note incluso sin título encima */
.project-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    rgba(8,8,15,.05) 0%,
    rgba(8,8,15,.20) 60%,
    rgba(8,8,15,.38) 100%);
  pointer-events: none;
  transition: background .3s ease;
}
/* Destello al entrar en hover: un blanco breve que se apaga casi de inmediato */
.project-card.flash::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #fff;
  pointer-events: none;
  animation: hover-flash .5s ease-out forwards;
}
@keyframes hover-flash {
  from { opacity: .9; }
  to   { opacity: 0; }
}

/* Marca de proyecto nuevo: un anillo interno del mismo --tri-clip que ya
   calcula el JS por tarjeta (así sigue el triángulo exacto, apunte hacia
   donde apunte, sin coordenadas propias que se puedan salir del recorte) */
.new-badge {
  position: absolute;
  inset: 0;
  clip-path: var(--tri-clip, none);
  box-shadow: inset 0 0 0 3px var(--yellow);
  animation: new-badge-pulse 2.2s ease-in-out infinite;
  pointer-events: none;
}
@keyframes new-badge-pulse {
  0%, 100% { opacity: .55; }
  50%      { opacity: 1;   }
}

/* ── Franja de accesos (redes, contacto) ───────────────────────────────── */
/* Última de las tres etapas de entrada (tarjetas → logo → franja, ver
   assets/perigamiscripts.js). Arranca invisible por el mismo motivo que
   #logoHeader: sin esto se vería fija en su esquina desde el primer
   pintado, antes de que las tarjetas y el logo hayan terminado de entrar. */
html:not(.no-js) .util-strip       { opacity: 0; }
html:not(.no-js) .util-strip.is-in { opacity: 1; }
.util-strip {
  position: fixed;
  right: var(--gap);
  bottom: var(--gap);
  display: flex;
  z-index: 150; /* por encima de una tarjeta en hover (101) y su anillo (100) */
  pointer-events: none;
}
/* Segundo grupo, mismo lenguaje visual, alineado al borde opuesto */
.util-strip-left { right: auto; left: var(--gap); }
.util-tri {
  position: relative;
  display: block;
  pointer-events: auto;
  background: #161c26;
  border: none; /* reseteo de <button>: el de contacto ya no es <a href="#">, ver index.php */
  padding: 0;
  cursor: pointer;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.5));
  transition: filter .2s ease, background-color .2s ease;
}
.util-tri + .util-tri { margin-left: -32px; } /* medio-pitch (mitad de UTIL_W), igual que el mosaico */
.util-tri:hover, .util-tri:focus-visible, .util-tri:active {
  background: #fff;
  filter: drop-shadow(0 6px 8px rgba(0,0,0,.6));
  outline: none;
}
/* El icono es blanco sobre el triángulo oscuro (normal) y negro sobre el
   blanco (hover/click) — mismos dos estados que perigamisocialbutts.svg /
   perigamisocialbutts_hover.svg. Los paths usan fill="currentColor".
   Solo para los de la franja: .util-hole es una clase compartida con el
   cierre de modal y el toggle de idioma, que siguen usando el truco del
   agujero (mix-blend-mode) — ver la regla de .util-hole más abajo. */
.util-tri .util-hole { color: #fff; mix-blend-mode: normal; }
.util-tri:hover .util-hole, .util-tri:focus-visible .util-hole, .util-tri:active .util-hole {
  color: #000;
}
/* Entrada de cada triángulo, mismo lenguaje que .project-item.enter
   (card-unfold): se despliega desde plano, pivotando desde abajo. Perspectiva
   más corta (400 vs 900) porque estos triángulos son mucho más chicos —con
   900 el pliegue casi no se notaba. */
@keyframes util-unfold {
  from { opacity: 0; transform: perspective(400px) rotateX(-75deg); }
  to   { opacity: 1; transform: perspective(400px) rotateX(0deg);   }
}
.util-tri.enter {
  opacity: 0;
  transform-origin: bottom center;
  animation: util-unfold .4s cubic-bezier(.22,.68,.35,1) forwards;
  animation-delay: var(--delay, 0ms);
}
.util-hole {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  mix-blend-mode: destination-out;
  pointer-events: none;
}
@media (max-width: 380px) {
  .util-strip { right: calc(var(--gap) - 4px); bottom: calc(var(--gap) - 4px); }
  .util-strip-left { right: auto; left: calc(var(--gap) - 4px); }
}

/* ── Empty state ───────────────────────────────────────────────────────── */
.empty { flex:1; display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:var(--fs); }

/* ═══════════════════════════════════════════════════════════════════════ */
/* ── Modales ───────────────────────────────────────────────────────────── */
.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(8,8,15,.72);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  align-items: center;
  justify-content: center;
  padding: var(--gap);
  z-index: 100;
}
.modal-overlay.is-open {
  display: flex;
  animation: overlay-in .22s ease both;
}
#modalContact { z-index: 200; }
/* Por encima de TODO, incluida la franja de redes (150) y el modal de
   contacto (200) — así el blur/oscurecido del overlay también los tapa,
   en vez de que la franja (con su propio z-index más alto que el overlay
   base) se quede flotando encima sin opacar. */
#modalDatoHoy { z-index: 300; }

/* "Dato de hoy": sin .modal-wrap/.modal (sin panel ni clip-path de esquinas)
   — la imagen es el único contenido, el overlay genérico ya la centra. */
.modal-overlay-bare .dato-hoy-modal-link { display: block; line-height: 0; }
.modal-overlay-bare img {
  display: block;
  max-width: 92vw;
  max-height: 92dvh;
  width: auto;
  height: auto;
}

@keyframes overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.modal-wrap {
  position: relative;
  width: 100%;
  max-width: 480px;
}

.modal-wrap::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--cyan);
  clip-path: var(--clip-lg-ring);
  transition: background .2s ease;
  pointer-events: none;
  z-index: 1;
}
.modal {
  background: #0c232b42;
  clip-path: var(--clip-lg);
  width: 100%;
  max-height: 92dvh;
  overflow-y: auto;
  box-shadow: 0 24px 60px rgba(0,0,0,.6);
  transform-origin: center top;
  animation: modal-in .32s cubic-bezier(.22,.68,.35,1) both;
}
/* El modal se despliega en lugar de deslizarse */
@keyframes modal-in {
  from { opacity: 0; transform: perspective(1200px) rotateX(-32deg); }
  to   { opacity: 1; transform: perspective(1200px) rotateX(0deg);   }
}

.modal-header {
  padding: 1.2rem 1.5rem 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 1rem;
}
/* Logo chico y a la izquierda: .logo-trigger trae width:100% de fábrica
   (pensado para el header de la página, que sí lo quiere a todo el ancho),
   así que hay que angostarlo específicamente acá. El tope en % (no solo un
   px fijo) es lo que lo mantiene proporcionado en modales angostos —
   .modal-wrap ya limita a 480px, pero en un teléfono chico ese 480 nunca se
   alcanza y el logo tiene que ceder antes que desbordar. */
.modal-header .logo-trigger { width: 540px; max-width: 64%; }
/* El logo angosto deja el header mucho más bajo que antes; subir el body
   encima de ese sobrante (en vez de dejarlo como aire vacío) es lo que
   realmente gana el espacio. Solo en "Acerca de": es el único modal con
   .modal-header — en Contacto, .modal-body es el primer contenido real
   (el <h2> de ahí es sr-only) y restarle margen lo pegaría contra el
   propio borde del modal, sin este sobrante que compensar. */
#modalAbout .modal-body { margin-top: -1rem; }

.modal-close {
  position: absolute;
  top: -10px;
  right: -19px;
  border: none;
  padding: 0;
  background: var(--cyan);
  isolation: isolate;
  cursor: pointer;
  display: block;
  transition: transform .2s ease, background .2s ease;
  z-index: 1;
}
.modal-close:hover,
.modal-close:focus-visible {
  background: #fff;
  transform: translateY(-3px) scale(1.2);
  outline: none;
}
.modal-close:active {
  transform: translateY(3px) scale(0.8);
}

/* Sobre la esquina inferior IZQUIERDA del modal — mismo offset que
   .modal-close pero reflejado abajo (bottom en vez de top) y a la
   izquierda (left en vez de right). De paso es la esquina SIN bisel
   (--clip-lg dobla arriba-izquierda y abajo-derecha), la misma situación
   de esquina "recta" en la que ya está .modal-close arriba. Sin un vecino
   con qué encajar acá, el ancho/alto y el clip-path los sigue poniendo
   sizeModalTriangles en JS, con la punta hacia arriba (down=false) tal
   como estaba. */
.modal-lang-toggle {
  position: absolute;
  bottom: -10px;
  left: -19px;
  border: none;
  padding: 0;
  background: var(--cyan);
  isolation: isolate;
  cursor: pointer;
  display: block;
  text-decoration: none;
  transition: transform .2s ease, background .2s ease;
  z-index: 1;
}
.modal-lang-toggle:hover,
.modal-lang-toggle:focus-visible {
  background: #fff;
  transform: translateY(-3px) scale(1.2);
  outline: none;
}
.modal-lang-toggle:active {
  transform: translateY(3px) scale(0.8);
}
/* El borde del modal también se pone blanco mientras el botón cerrar, el
   toggle de idioma o el botón enviar están en hover/foco */
.modal-wrap:has(.modal-close:hover, .modal-close:focus-visible,
                 .modal-lang-toggle:hover, .modal-lang-toggle:focus-visible,
                 .btn-primary:hover, .btn-primary:focus-visible)::before {
  background: #fff;
}

.modal-body {
  padding: 1.1rem 1.35rem 0 1.35rem;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text);
}
/* Sin padding abajo SOLO en Contacto: el botón enviar necesita tocar el
   borde inferior real del modal (que no tiene padding propio) — quitarle
   el padding a .modal-body en vez de cancelarlo con margin negativo en el
   botón evita que un cambio futuro al padding de arriba desalinee esa
   cuenta (ya pasó una vez: el padding pasó de 1rem a esto y la resta
   negativa quedó corta). */
#modalContact .modal-body { padding-bottom: 0; }
/* A diferencia de Contacto (título sr-only, sin nada que perder), acá el
   modal SÍ necesita aire abajo: la lista de precios + el botón no deben
   quedar pegados contra el borde inferior del recorte. */
#modalCompra .modal-body { padding-bottom: 1.35rem; }
#modalCompra .modal-body h2 {
  margin: 0 0 .75rem;
  color: var(--cyan);
  font-size: 1.1rem;
  letter-spacing: .02em;
}
.modal-body p + p { margin-top: .6em; }
/* Destacados y enlaces del cuerpo: cyan y en negrilla; blancos al interactuar */
.modal-body p em,
.modal-body p a:not(.about-cv) {
  color: var(--cyan);
  font-style: normal;
  font-weight: 700;
}
.modal-body p a:not(.about-cv) {
  text-decoration: none;
  transition: color .2s ease;
}
.modal-body p a:not(.about-cv):hover,
.modal-body p a:not(.about-cv):focus-visible,
.modal-body p a:not(.about-cv):active {
  color: #fff;
  outline: none;
}
/* Un enlace que envuelve un em: el color lo manda el enlace, no el em */
.modal-body p a:not(.about-cv) em { color: inherit; }
/* Barra de scroll delgada: el modal creció y ahora sí puede desbordar.
   El "carril" completo mide 10px, pero el thumb visible es de 4px con un
   borde transparente de 3px a cada lado (background-clip:content-box) —
   así el trazo cyan queda centrado en el carril en vez de pegado al borde
   derecho del modal. Firefox (scrollbar-width/color) no tiene este truco
   disponible; ahí se ve el thumb delgado normal, pegado al borde, sin que
   se rompa nada. */
.modal { scrollbar-width: thin; scrollbar-color: var(--cyan) transparent; }
.modal::-webkit-scrollbar       { width: 10px; }
.modal::-webkit-scrollbar-thumb {
  background: var(--cyan);
  background-clip: content-box;
  border: 3px solid transparent;
  border-radius: 999px;
}
.modal::-webkit-scrollbar-track { background: transparent; }

/* ── Modal "Acerca de": nombre clicable con tooltip triangular ───────────── */
/* display:inline (el de fábrica en un <span>), no inline-block: el único
   hijo que afecta el flujo normal es el propio texto del nombre — el
   tooltip es position:absolute, así que NO participa del cálculo de
   línea/altura de este elemento. Es lo que evita el problema de fondo que
   tuvo la versión anterior (foto+nombre con un hijo display:block adentro
   de un inline-block, cuya línea base cada navegador calculaba distinto). */
.about-name-trigger {
  position: relative;
  color: var(--cyan);
  font-weight: 700;
  cursor: pointer;
}
.about-name-trigger:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}
/* Oculto por default; se muestra en :hover del nombre (escritorio) o con
   .is-active (JS la pone al clic/Enter/toque — necesario en móvil y tablet,
   donde no hay hover de verdad — y la quita a los 3s o al clic siguiente).
   El tamaño real y el clip-path del triángulo los pone JS, igual que el
   resto de triángulos del sitio. */
.about-photo-tip {
  position: absolute;
  left: 50%;
  bottom: 100%;
  margin-bottom: 8px;
  pointer-events: none;
  opacity: 0;
  transform-origin: bottom center;
  /* Mismo giro 3D que el de entrada de las tarjetas del mosaico
     (@keyframes card-unfold): rotateX(-84deg) → 0deg. Es un transition,
     no una animation, para que esconder (quitar .is-active, o dejar el
     hover) reproduzca el mismo giro en reversa solo, sin coreografía
     aparte en JS. */
  transform: translateX(-50%) perspective(900px) rotateX(-84deg);
  transition: opacity .6s cubic-bezier(.22,.68,.35,1),
              transform .6s cubic-bezier(.22,.68,.35,1);
  z-index: 6;
}
.about-name-trigger:hover .about-photo-tip,
.about-name-trigger.is-active .about-photo-tip {
  opacity: 1;
  transform: translateX(-50%) perspective(900px) rotateX(0deg);
}
/* Plato cyan más grande detrás de la foto, recortado con el mismo triángulo
   agrandado hacia afuera del centroide — igual que .hover-ring con las
   tarjetas del mosaico (ver EDGE_MARGIN/_borderClip en perigamiscripts.js).
   Un border/box-shadow normal no sigue los lados diagonales de un
   clip-path; esta técnica sí, porque el "borde" es en realidad el margen
   visible del plato que la foto, más chica y encima, no alcanza a tapar. */
.about-photo-tip-border {
  position: absolute;
  z-index: 1;
  background: var(--cyan);
}
.about-photo-tip-img {
  position: relative;
  z-index: 2;
  display: block;
  object-fit: cover;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,.5));
}

/* Hueco en línea que solo reserva el ANCHO (lo fija el JS junto al triángulo):
   con alto 0 no aporta nada al renglón, así el triángulo no lo estira */
.about-cv-slot {
  position: relative;
  display: inline-block;
  height: 0;
  margin: 0 -0.5rem;
  vertical-align: baseline;
}
/* Triángulo hacia abajo = "descargar", absoluto sobre el hueco: se sale del
   renglón sin empujarlo, centrado a media altura de la minúscula */
.about-cv {
  position: absolute;
  left: 0;
  top: -.5em;
  transform: translateY(-50%);
  background: var(--cyan);
  isolation: isolate;
  cursor: pointer;
  text-decoration: none;
  transition: transform .2s ease, background .2s ease;
}
/* Mismo comportamiento que .modal-close: sube y crece en hover, baja y encoge al oprimir */
.about-cv:hover,
.about-cv:focus-visible {
  background: #fff;
  transform: translateY(calc(-50% - 3px)) scale(1.2);
  outline: none;
}
.about-cv:active {
  transform: translateY(calc(-50% + 3px)) scale(0.8);
}

/* text-align en el <img> no hacía nada (no es contenido de texto) — la
   alineación a la derecha va en el contenedor, con flexbox. */
.about-firma-wrap {
  display: flex;
  justify-content: flex-end;
}
.about-firma {
  margin: 0 2rem 0 0;
  width: 128px;
  height: 128px;
}
/* ── Etiqueta de sección + chips de temas ───────────────────────────────── */
.modal-body .about-label {
  display: flex;
  align-items: center;
  gap: .6em;
  margin-top: 1.4rem;
  color: var(--muted);
  font-size: calc(var(--fs-sm) * .85);
  text-transform: uppercase;
  letter-spacing: .18em;
}
/* Triangulito de viñeta: el JS le pone --tri-bullet con esquinas redondeadas,
   como los triángulos del mosaico; el polygon es el respaldo sin JS */
.about-label::before {
  content: "";
  flex: 0 0 auto;
  width: 11px;
  height: 9px;
  background: var(--cyan);
  clip-path: var(--tri-bullet, polygon(50% 100%, 100% 0, 0 0));
}
.about-label::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.topics {
  --sesgo: 7px;
  --topics-gap: 4px;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  row-gap: var(--topics-gap);
  /* El hueco horizontal NO lo pone column-gap */
  column-gap: 0;
  margin: .5rem calc((var(--sesgo) - var(--topics-gap)) / 2) 0;
  padding: 0;
}
/* Trapecios alternados, igual que las barras de botones de caleidociclero */
.topic {
  /* (--sesgo se hereda de .topics) Margen negativo de media diferencia a cada lado */
  margin-inline: calc((var(--topics-gap) - var(--sesgo)) / 2);
  display: inline-flex;
  align-items: center;
  padding: .3rem calc(.62rem + var(--sesgo));
  font-size: calc(var(--fs-sm) * .88);
  color: var(--text);
  background: rgba(255,255,255,.05);
  transition: background .2s, color .2s;
}
.topic:nth-child(odd) {
  clip-path: polygon(var(--sesgo) 0, calc(100% - var(--sesgo)) 0, 100% 100%, 0 100%);
}
.topic:nth-child(even) {
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--sesgo)) 100%, var(--sesgo) 100%);
}
/* Paleta ciclada por posición: se repite cada 6 chips, haya los que haya */
.topic:nth-child(6n+1) { --tone: var(--cyan);   }
.topic:nth-child(6n+2) { --tone: var(--green);  }
.topic:nth-child(6n+3) { --tone: var(--orange); }
.topic:nth-child(6n+4) { --tone: var(--pink);   }
.topic:nth-child(6n+5) { --tone: var(--blue);   }
.topic:nth-child(6n)   { --tone: var(--yellow); }
.topic:hover {
  background: var(--tone, var(--cyan));
  color: #06060f;
}

/* ── Botones genéricos ─────────────────────────────────────────────────── */
/* Botones = pestañas de papel: sin borde, se distinguen por tono y trapecio
   (base más ancha que el borde superior — mismo lenguaje que los chips de
   .topics y los campos del formulario, no el chaflán viejo del extremo
   superior derecho). El polygon() de acá es el respaldo sin JS; el redondeo
   de esquina lo pone roundShapes() en JS, igual que a los chips. */
.btn {
  --sesgo: 8px;
  font-family: var(--font);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: .05em;
  padding: .5rem 1.1rem;
  border: none;
  clip-path: polygon(var(--sesgo) 0, calc(100% - var(--sesgo)) 0, 100% 100%, 0 100%);
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: .35em;
  transition: background .2s, color .2s;
}
.btn-primary {
  background: var(--cyan);
  color: #06060f; /* oscuro: sobre cyan, blanco tenía poco contraste */
  margin-left: auto;
}
.btn-primary:hover,
.btn-primary:focus-visible,
.btn-primary:active {
  background: #fff;
  outline: none;
}
.btn-primary:disabled {
  opacity: .5;
  cursor: not-allowed;
}

/* Pegado de verdad al borde inferior: el padding-bottom que lo hubiera
   separado ya se quitó arriba, en #modalContact .modal-body. A la derecha
   es al revés: SUMA aire de más sobre el padding que ya había, para alejarlo
   del borde (y de paso del bisel de esa esquina, --cut-lg). */
.form-submit-row {
  display: flex;
  justify-content: flex-end;
  margin: 0 1rem 0 0;
}

/* ── Formulario de contacto ─────────────────────────────────────────────── */
.field {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  margin-bottom: .9rem;
}
.field label {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.field input,
.field textarea {
  user-select: text;
  font-family: var(--font);
  font-size: var(--fs-sm);
  background: rgba(8,8,15,.6);
  border: none;
  color: var(--text);
  padding: .5rem .75rem;
  width: 100%;
  transition: background .2s, box-shadow .2s;
  resize: vertical;
}
.field input:focus,
.field textarea:focus {
  outline: none;
  background: rgba(0,157,216,.08);
  box-shadow: inset 0 0 0 2px var(--cyan);
}
.field input::placeholder,
.field textarea::placeholder { color: var(--muted); }
.field textarea { min-height: 90px; }

/* Captcha inline */
.captcha-row {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin-bottom: .9rem;
}
.captcha-row label {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
}
.captcha-row input {
  font-family: var(--font);
  font-size: var(--fs-sm);
  background: rgba(8,8,15,.6);
  border: none;
  color: var(--text);
  padding: .5rem .75rem;
  width: 80px;
  transition: background .2s, box-shadow .2s;
  -moz-appearance: textfield;
}
.captcha-row input::-webkit-outer-spin-button,
.captcha-row input::-webkit-inner-spin-button { -webkit-appearance: none; }
.captcha-row input:focus {
  outline: none;
  background: rgba(0,157,216,.08);
  box-shadow: inset 0 0 0 2px var(--cyan);
}

.form-msg {
  font-size: var(--fs-sm);
  padding: .5rem .75rem;
  margin-bottom: .9rem;
  display: none;
}
.form-msg.ok    { display: block; background: rgba(109,217,0,.14); color: var(--green); }
.form-msg.error { display: block; background: rgba(246,0,106,.14); color: var(--pink);  }

/* Error inline por campo (sugerencia 14) */
.field.invalid input,
.field.invalid textarea { box-shadow: inset 0 0 0 2px var(--pink); background: rgba(246,0,106,.08); }
.field-error {
  font-size: .72rem;
  color: var(--pink);
  min-height: 1em;
  letter-spacing: .02em;
}


.compra-lista {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .5rem;
}
.compra-fila {
  border: 1px solid var(--border);
  background: rgba(255,255,255,.02);
}
.compra-fila--combo { border-color: var(--cyan); }
.compra-fila--tenido {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .6rem .8rem;
  opacity: .7;
}
.compra-fila__radio {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .6rem .8rem;
  cursor: pointer;
}
.compra-fila__texto { display: flex; flex-direction: column; gap: .15rem; flex: 1 1 auto; }
.compra-fila__desc { font-size: .78rem; color: var(--muted); font-weight: 400; }
.compra-fila__precio { font-weight: 700; color: var(--cyan); white-space: nowrap; }
.compra-fila__estado { font-size: .8rem; color: var(--green); white-space: nowrap; }

.compra-aviso {
  font-size: .8rem;
  color: var(--pink);
  border-left: 2px solid currentColor;
  padding-left: .5rem;
  margin: 0 0 .75rem;
}
.compra-aviso[hidden] { display: none; }

/* Fila propia (no reusa .form-submit-row: ese trae un margin-right pensado
   para el layout del formulario de contacto, no para acá). */
.compra-submit-row {
  display: flex;
  justify-content: flex-end;
}

.compra-enlace {
  display: block;
  width: 100%;
  background: none;
  border: none;
  padding: .5rem 0 0;
  margin: 0;
  font-family: var(--font);
  font-size: .78rem;
  color: var(--muted);
  text-decoration: underline;
  text-align: right;
  cursor: pointer;
}
.compra-enlace:hover, .compra-enlace:focus-visible { color: var(--cyan); }

.compra-resultado { text-align: center; }
.compra-resultado-icono { width: 44px; height: 44px; margin: 0 auto .75rem; display: block; }
#iconoOk.compra-resultado-icono { color: var(--cyan); }
#iconoError.compra-resultado-icono { color: var(--pink); }
#iconoEspera.compra-resultado-icono { color: var(--muted); animation: compra-girar 1.4s linear infinite; }
@keyframes compra-girar { to { transform: rotate(360deg); } }
.compra-resultado h3 { margin: 0 0 .5rem; color: var(--text); }
.compra-resultado p { color: var(--muted); }

/* Texto solo para lectores de pantalla */
.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;
}

/* Mientras carga la preview, el fondo ya adelanta el --paper de ESE proyecto
   (calculado en PHP) en vez de un tono genérico: el patrón de pliegues, que
   sigue siendo el mismo trazo cyan para todos, se ve por encima */
.project-card.loading {
  background-color: color-mix(in srgb, var(--paper, var(--cyan)) 30%, var(--surface));
}

/* Entrada: cada tarjeta se despliega desde plano, como un libro pop-up */
@keyframes card-unfold {
  from { opacity: 0; transform: rotateX(-84deg); }
  to   { opacity: 1; transform: rotateX(0deg);   }
}
.project-item.enter {
  opacity: 0;
  transform-origin: bottom center;
  animation: card-unfold .6s cubic-bezier(.22,.68,.35,1) forwards;
  animation-delay: var(--delay, 0ms);
  /* Chrome a veces pinta un fotograma con la imagen de fondo a medio
     recortar durante este giro 3D (el clip-path vive en .project-card,
     hijo de este mismo elemento que gira) — parece un tema de cuándo arma
     la capa compuesta para el filter:drop-shadow de acá combinado con el
     rotateX. will-change avisa con antelación que esto va a animarse, así
     arma esa capa ANTES del primer fotograma en vez de a mitad de camino.
     Solo mientras dura la animación: la clase .enter se quita después
     (ver JS), así que no queda un will-change permanente sin uso. */
  will-change: transform, opacity;
}

/* Red de seguridad para viewports bajos (celular en horizontal, laptop con
   poca altura de ventana): un poco menos de aire en todos lados del modal
   "Acerca de", para reducir el riesgo de que el contenido no quepa entero
   sin scroll incluso con el compactado normal de arriba. */
@media (max-height: 700px) {
  .modal-header { padding-top: .7rem; }
  #modalAbout .modal-body { padding-top: .5rem; }
  .modal-body p + p { margin-top: .4em; }
  .modal-body .about-label { margin-top: .6rem; }
  .about-firma { width: 56px; height: 56px; margin-right: 1.5rem; }
}

/* Respeto a prefers-reduced-motion (sugerencia 1) */
@media (prefers-reduced-motion: reduce) {
  body::after { animation: none !important; }
  .logo-trigger:hover { animation: none !important; }
  .logo-svg .perigami { animation: none !important; }
  .logo-svg .borde { transition: none !important; }
  .project-item, .project-card, .hover-ring, .util-tri, .plus-trigger .cls-1 { transition: none !important; }
  .project-card.flash::after { animation: none !important; }
  .new-badge { animation: none !important; opacity: .85; }
  .modal, .modal-overlay.is-open { animation: none !important; }
  .topic, .about-cv, .modal-lang-toggle, .about-photo-tip { transition: none !important; }
  .project-item.enter { opacity: 1 !important; animation: none !important; }
  .util-tri.enter { opacity: 1 !important; animation: none !important; }
  /* Mismo criterio que .project-item.enter de arriba: quien pide menos
     movimiento se queda sin la animación de entrada, nunca sin el elemento.
     El JS ya revela el logo y la franja en este caso; esto lo garantiza
     igual si alguna vez ese camino cambia. */
  html:not(.no-js) #logoHeader { opacity: 1 !important; }
  html:not(.no-js) .util-strip { opacity: 1 !important; }
  * { scroll-behavior: auto !important; }
}
