/* ============================================================================
   KAIRO · Sistema de componentes
   ----------------------------------------------------------------------------
   ANGULAR 21 → cada bloque marcado con `ng-component:` es un componente
   standalone. Los estilos de esos bloques se mueven al `styles: []` del
   componente; lo que queda acá (layout, tipografía, tokens) va a global.
   ============================================================================ */

/* ── Layout base ─────────────────────────────────────── ng-component: ShellComponent */
.shell { min-height: 100dvh; display: flex; flex-direction: column; }
.page {
  width: 100%; max-width: var(--maxw);
  margin-inline: auto;
  padding: var(--sp-4) max(var(--sp-4), env(safe-area-inset-right))
           calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--sp-7))
           max(var(--sp-4), env(safe-area-inset-left));
  display: grid; gap: var(--sp-5);
  /* La columna DEBE ser minmax(0,1fr) y no el `auto` implícito: con `auto` se
     dimensiona por el max-content de la sección más ancha, así que cualquier
     bloque interno que no sepa achicarse arrastra la página entera y aparece
     scroll horizontal en el teléfono. Con esto, el techo lo pone el viewport y
     cada sección se las arregla adentro. */
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 960px) {
  .shell { flex-direction: row; }
  .shell > .main { flex: 1; min-width: 0; }
  .page { padding: var(--sp-6) var(--sp-6) var(--sp-8); gap: var(--sp-6); }
}

/* ── App bar ─────────────────────────────────────────── ng-component: AppBarComponent */
/* ── ZONA SEGURA ARRIBA — el recorte de la hora ─────────────────────────────
   Las 17 pantallas declaran `viewport-fit=cover`, que es lo que permite pintar
   hasta el borde físico de la pantalla. Eso está bien y es lo que queremos: el
   papel del álbum llega hasta el filo. Pero `cover` NO reserva nada: el sistema
   dibuja la hora, la señal y la batería ENCIMA de lo que haya ahí.

   Mientras el álbum se abre en el navegador no se nota, porque la barra del
   navegador ya ocupa esa franja. Instalado como webapp esa barra desaparece y
   la barra de la app sube a ocupar su lugar — debajo del reloj. Es el mismo
   error que te comió el header en el otro proyecto.

   `max()` y no una suma: en un teléfono sin notch el inset vale 0 y el padding
   tiene que seguir siendo el de diseño, no cero. Los costados también, porque
   en horizontal el notch se corre a un lado y se come el título o las monedas. */
.appbar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: max(var(--sp-3), env(safe-area-inset-top)) max(var(--sp-4), env(safe-area-inset-right))
           var(--sp-3) max(var(--sp-4), env(safe-area-inset-left));
  background: var(--bg-blur);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid var(--border-soft);
}
.appbar__brand {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--step-1); letter-spacing: -0.02em;
  display: flex; align-items: center; gap: 0.5rem;
  /* El que cede es el TÍTULO, no la fila. Al sumar los botones de tema y
     pantalla completa, en 360px la barra empujaba las monedas 9px afuera y
     aparecía scroll horizontal en toda la app. Un título recortado se entiende;
     una pantalla que se corre para el costado, no. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.appbar__brand b { color: var(--accent); }
.appbar__spacer { flex: 1; min-width: 0; }
/* OJO CON `padding-inline` ACÁ. Esta regla existe para apretar la barra en
   pantalla angosta, y en su primera versión escribía `padding-inline:
   var(--sp-3)` a secas — que PISA la zona segura lateral del bloque de arriba,
   justo en los teléfonos angostos, que son los que tienen muesca. Acostados, el
   título y las monedas se metían abajo del notch. Tiene que apretar
   CONSERVANDO el inset, no en vez de él. */
@media (max-width: 420px) {
  .appbar {
    gap: var(--sp-2);
    padding-inline: max(var(--sp-3), env(safe-area-inset-left)) max(var(--sp-3), env(safe-area-inset-right));
  }
}

/* Botón de icono de la barra. 40px de caja visible y 44 de área táctil real:
   el mínimo de dedo se cumple con el área, no agrandando el dibujo. */
.iconbtn {
  position: relative;
  width: 2.5rem; height: 2.5rem; flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-soft); background: var(--surface-2);
  color: var(--fg); cursor: pointer;
  transition: background .16s var(--ease), border-color .16s var(--ease), transform .16s var(--ease);
}
.iconbtn::after { content: ""; position: absolute; inset: -2px; }   /* 44px de dedo */
.iconbtn svg { width: 1.1rem; height: 1.1rem; }
.iconbtn:hover { background: var(--surface-3); border-color: var(--border); }
.iconbtn:active { transform: scale(.94); }
.iconbtn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.iconbtn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }

/* ── Panel de ajustes ─────────────────────── ng-component: AjustesPanelComponent
   Un `popover` nativo y no un menú a mano: el cierre al tocar afuera, el
   Escape y el manejo del foco vienen del navegador, y son exactamente las tres
   cosas que un menú casero hace mal. Se posiciona fijo bajo la barra en vez de
   anclado al botón porque el anclaje CSS todavía es de un solo navegador. */
.ajustes {
  position: fixed; inset: auto var(--sp-3) auto auto;
  /* `--appbar-h` es el alto de la barra SIN la zona segura. Con el inset
     sumado arriba la barra mide más, y sin esto el panel se abría pisándola. */
  top: calc(var(--appbar-h) + env(safe-area-inset-top) + var(--sp-2));
  width: min(20rem, calc(100vw - var(--sp-5)));
  margin: 0; padding: var(--sp-4);
  background: var(--surface); color: var(--fg);
  border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
  gap: var(--sp-4);
  /* CERRADO ESTÁ OCULTO, y esta línea es la que importa. El navegador oculta
     los `popover` cerrados con `display:none` desde su hoja de estilos, pero es
     una regla NORMAL: un `display:grid` del autor la pisa sin avisar, y el
     panel queda visible en todas las pantallas. Pasó exactamente eso. Se
     declara cerrado por defecto y sólo se muestra abierto — si un navegador no
     entendiera `:popover-open`, el panel no aparece, que es infinitamente mejor
     que aparecer siempre. */
  display: none;
}
.ajustes:popover-open { display: grid; }
.ajustes::backdrop { background: oklch(8% 0.03 268 / .28); }
.ajustes:popover-open { animation: ajustesIn .18s var(--ease-bounce) both; display: grid; }
@keyframes ajustesIn { from { opacity: 0; transform: translateY(-6px) scale(.98); } }
@media (prefers-reduced-motion: reduce) { .ajustes:popover-open { animation: none; } }

.ajustes__grupo { display: grid; gap: var(--sp-2); }
.ajustes__k {
  font-size: var(--step--2); color: var(--muted);
  text-transform: uppercase; letter-spacing: .1em;
}
.ajustes__fila {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: var(--sp-3); min-height: 2.8rem;
}
.ajustes__fila > span { font-size: var(--step--1); }
.ajustes hr { border: 0; border-top: 1px solid var(--border-soft); margin: 0; }

/* ── Interruptor ─────────────────────────── ng-component: SwitchComponent
   UNO SOLO para todo el álbum, y esto es la cicatriz de un bug: había dos
   `.switch` distintos —el del filtro «sólo las que me faltan» dentro de
   `album.html` y el del panel de ajustes acá— con el mismo nombre de clase.
   El de acá le puso `width: 52px` a un `<label>` que el otro nunca acotó, y el
   texto del filtro se partió en CINCO líneas con el riel encima, en todos los
   anchos de 320 a 1440. Ninguna de las dos hojas estaba mal por sí sola: el
   error fue que existieran dos.

   El ANCHO lo pone el riel, nunca la caja: así el mismo interruptor sirve con
   texto al lado (el filtro del álbum) y sin texto (el panel de ajustes, donde
   la etiqueta es un `<label for>` hermano).

   El área táctil son 44 de alto aunque el riel dibuje 30: el pulgar necesita
   44, el ojo no. Se separan el ÁREA y el DIBUJO en vez de engordar el riel,
   que al lado del texto quedaría tosco. */
.switch {
  position: relative; display: inline-flex; align-items: center; gap: 0.6rem;
  min-height: 2.8rem; cursor: pointer; font-size: var(--step--1);
}
/* El input cubre la caja entera: en el álbum la caja incluye el texto —y tocar
   el texto de una etiqueta tiene que alternarla— y en los ajustes es sólo el
   riel. Con `width: 0` el riel del panel no se podía tocar. */
.switch input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; margin: 0; cursor: pointer; }
.switch__track {
  flex: none; position: relative; width: 3.1rem; height: 1.9rem; border-radius: var(--r-pill);
  background: var(--surface-3); border: 1px solid var(--border);
  transition: background .18s var(--ease);
  /* El riel se dibuja DESPUÉS del input, así que sin esto queda encima y se
     come el clic. En el álbum no se notaba —la caja es un `<label>` y reenvía—
     pero en el panel de ajustes es un `<span>` y el interruptor no respondía. */
  pointer-events: none;
}
.switch__track::after {
  content: ""; position: absolute; top: 0.1rem; left: 0.1rem;
  width: 1.5rem; height: 1.5rem; border-radius: 50%;
  background: var(--fg); transition: transform .18s var(--ease-bounce);
}
.switch input:checked + .switch__track { background: var(--accent); border-color: transparent; }
.switch input:checked + .switch__track::after { transform: translateX(20px); background: var(--accent-ink); }
.switch input:focus-visible + .switch__track { outline: 3px solid var(--accent); outline-offset: 3px; }

/* Segmentado de tres: para el tema, tres opciones visibles ganan a un ciclo
   ciego — con el botón que rotaba, saber en qué modo estabas exigía contar. */
.seg { display: grid; grid-auto-flow: column; gap: 0.1rem; padding: 0.2rem;
       background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: var(--r-pill); }
.seg button {
  min-height: 2.8rem; padding: 0 var(--sp-3); border: 0; border-radius: var(--r-pill);
  background: none; color: var(--muted); cursor: pointer;
  font: inherit; font-size: var(--step--1);
}
.seg button[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); font-weight: 700; }
.seg button:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

.ajustes input[type="range"] { width: 100%; accent-color: var(--accent); min-height: 2.8rem; }

.pill {
  display: inline-flex; align-items: center; gap: 0.4rem;
  min-height: 2.1rem; padding: 0 0.8rem;
  border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--border-soft);
  font-family: var(--font-mono); font-size: var(--step--1);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.pill--coins { color: var(--accent-2); }
.pill--packs { color: var(--accent); }
.pill svg { width: 0.9rem; height: 0.9rem; }

/* ── Navegación colapsable ────────────────────────────── ng-component: NavComponent
   Móvil  → tab bar abajo que se esconde al scrollear hacia abajo.
   Desktop → rail que se contrae de 236px a 76px (sólo iconos) con un toggle.
   El estado vive en <html data-nav-state> y se persiste; ver js/ui.js.
   OJO: NO usar `data-nav` en <html> — colisiona con el selector de montaje
   `nav[data-nav]` y mountNav() terminaría escribiendo sobre el documento. */
.nav {
  position: fixed; inset: auto 0 0 0; z-index: 45;
  display: flex; align-items: stretch;
  /* El alto CRECE con la franja del indicador de inicio, no se la come. Con
     `height` fijo y `box-sizing: border-box`, el padding de abajo salía de los
     68px y los íconos quedaban aplastados contra el indicador en vez de
     apoyados arriba de él. */
  height: calc(var(--nav-h) + env(safe-area-inset-bottom));
  padding: 0.4rem max(0.4rem, env(safe-area-inset-right)) max(0.4rem, env(safe-area-inset-bottom))
           max(0.4rem, env(safe-area-inset-left));
  background: var(--rail-bg);
  backdrop-filter: blur(16px);
  border-top: 1px solid var(--border-soft);
  transition: transform .28s var(--ease), width .24s var(--ease);
}
.nav__head { display: none; }
.nav__list { display: flex; flex: 1; min-width: 0; }
.nav__item {
  flex: 1; min-width: 0; min-height: 3.5rem;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.2rem;
  border-radius: var(--r-md);
  color: var(--muted); font-size: var(--step--2); font-weight: 600;
  text-decoration: none; position: relative;
  transition: color .18s var(--ease), background .18s var(--ease);
}
.nav__item svg { width: 1.4rem; height: 1.4rem; flex: 0 0 22px; }
/* LA ETIQUETA DE LA BARRA DE ABAJO NO SE PARTE. El `white-space: nowrap` vivía
   sólo dentro del `@media (min-width: 960px)` —o sea, sólo en el rail de
   escritorio— así que en el teléfono las etiquetas se envolvían: con ocho ítems,
   «Los cracks» se partía en dos líneas y «Trivia / Memotest / Pop it» quedaban
   pegadas sin aire. El `ellipsis` además hace visible el recorte: una etiqueta
   cortada al ras es indistinguible de una palabra corta. */
.nav__label { max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ── LA BARRA DE ABAJO NO LLEVA TODO ──────────────────────────────────────
   Sólo los que declaran `pie: true` en `NAV_ITEMS`. Con los nueve que hay, a
   360px cada pestaña caía en 38px: por debajo del mínimo táctil de 44 y con la
   etiqueta cortada. Con tres quedan 120px y no hay nada que leer con esfuerzo.

   El corte se hace ACÁ y no en el JS —el marcado es el mismo para las dos
   barras— porque en escritorio la lateral sí los lleva a todos: es la misma
   navegación mostrada con dos anchos distintos, no dos navegaciones. */
/* `data-pie` en el botón «Más» NO es decorativo: sin él lo agarraba
   `.nav__item:not([data-pie])` y el botón no aparecía en el pie, que es el
   único lugar donde tiene sentido. Y en el rail pasaba lo contrario —
   `:not([data-pie])` (0,2,1) le ganaba a `.nav__item--mas` (0,2,0) y lo
   mostraba—. Con la bandera puesta, cada regla agarra lo suyo. */
.nav__item:not([data-pie]) { display: none; }
.nav__item--mas { border: 0; background: none; font: inherit; cursor: pointer; }

@media (min-width: 960px) {
  .nav__item:not([data-pie]) { display: flex; }
  /* En el rail están los nueve: un «Más» no tendría qué mostrar. */
  .nav__item--mas { display: none; }
}

/* ══ RAREZA: LOS METALES, LA MONEDA Y EL MEDALLERO ═══ ng-component: MonedasComponent
   Vive acá y no en `album.html` porque ahora lo usan TRES pantallas: la hoja
   del álbum (el hueco con su moneda), la ficha abierta y el plantel. Estaba en
   el `<style>` del álbum, y copiarlo a las otras dos habría dejado tres
   recetas del mismo degradé esperando a desincronizarse.

   `[data-rareza]` sin clase adelante: lo lleva la figurita, la ficha abierta o
   el medallero, y de acá salen los tres tonos. Un solo lugar los reparte. */
[data-rareza="normal"]     { --m: var(--metal-bronce); --m-luz: var(--metal-bronce-luz); --m-sombra: var(--metal-bronce-sombra); }
[data-rareza="especial"]   { --m: var(--metal-plata);  --m-luz: var(--metal-plata-luz);  --m-sombra: var(--metal-plata-sombra); }
[data-rareza="legendaria"] { --m: var(--metal-oro);    --m-luz: var(--metal-oro-luz);    --m-sombra: var(--metal-oro-sombra); }

/* La moneda. El degradé radial fuera de centro es lo que la hace parecer metal
   — un relleno plano da un círculo de color. El anillo interior es el canto, y
   el de afuera es lo que la despega del fondo: el bronce sin él daba 1,79:1
   contra el papel azul y se leía como una mancha. */
.moneda {
  width: 2.2rem; height: 2.2rem; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, var(--m-luz), var(--m) 56%, var(--m-sombra));
  box-shadow: inset 0 0 0 2px var(--m-sombra),
              inset 0 2px 3px var(--m-luz),
              0 0 0 2px color-mix(in oklab, var(--m-luz) 75%, transparent),
              0 2px 4px oklch(15% .04 260 / .45);
  display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 800; line-height: 1;
  font-size: var(--step--1); color: var(--m-sombra);
}

/* --- El medallero ─────────────────────────────────────────────────────────
   TRES CASILLEROS ARRIBA A LA DERECHA de la lámina abierta, uno por VERSIÓN:
   normal, especial y legendaria, de izquierda a derecha. Los vacíos quedan
   punteados —el mismo idioma que el hueco del álbum: un contorno de puntos es
   algo que se puede llenar— y los llenos son BOTONES: tocar uno muestra esa
   versión de la figurita.

   Se muestra sólo con la lámina ABIERTA, no en la grilla. En la hoja, cuarenta
   medalleros son ciento veinte casilleros mirándote: la hoja es para ver
   cuántas llevás, no para comparar versiones.

   Va sobre la lámina y no debajo porque las versiones son DE esa lámina;
   puesto afuera se leería como un dato de la pantalla. */
.medallero {
  position: absolute; top: var(--sp-3); right: var(--sp-3); z-index: 2;
  display: flex; gap: 0.3rem;
  padding: 0.3rem;
  border-radius: var(--r-md);
  /* .82 y no .55: con el chip translúcido, el fondo del medallero es LA FOTO,
     así que el contraste del punteado cambiaba con cada lámina — el cobre daba
     1,88:1 sobre la camiseta blanca de un jugador. A .82 el fondo es
     previsible y el punteado se mide una vez para las tres rarezas.
     El `blur` se queda: es lo que evita que se vea el borde del chip como un
     recuadro pegado encima. */
  background: oklch(12% .05 260 / .82);
  backdrop-filter: blur(6px);
}
/* El casillero VACÍO no tiene metal todavía —no ganaste esa versión— así que su
   punteado es neutro. Poner el metal ahí sería mostrar de qué color va a ser
   una figurita que quizá no consigas nunca. */
.medallero__casilla {
  width: 2.6rem; height: 2.6rem;
  padding: 0; margin: 0;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  border: 2px dashed color-mix(in oklab, var(--slot) 55%, transparent);
  background: none;
}
/* El lleno sí: lo pinta su propia rareza, que el marcado le pone encima. */
.medallero__casilla[data-llena] {
  border-color: color-mix(in oklab, var(--m-luz) 62%, transparent);
  cursor: pointer;
}
.medallero__casilla[data-activa] {
  border-style: solid;
  box-shadow: 0 0 0 2px var(--m-luz);
}
.medallero__casilla:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* EL CASILLERO LLENO SIGUE PUNTEADO. La primera versión le sacaba el borde
   —«ya no es algo que se pueda llenar»— y con tres monedas el medallero se leía
   como una barra de botones: tres círculos pegados, sin la fila que los
   ordena. Con el punteado puesto se ve el CASILLERO y la moneda ADENTRO, que
   es lo que dice cuántos lugares hay en total incluso con todos llenos. El
   lleno se distingue por la moneda, que es bastante. */
.medallero .moneda { width: 1.9rem; height: 1.9rem; }

/* Sin ninguna versión registrada. NO se dibujan tres casilleros vacíos, que se
   leerían como «las perdiste todas»: se dice que no hay versión. Pasa con todo
   lo descubierto antes de que las versiones existieran y con lo que escribe el
   panel de demo, que pone láminas sin jugarlas. */
.medallero--sinmarca {
  padding: 0.35rem 0.6rem;
  font-family: var(--font-mono); font-size: var(--step--2);
  color: color-mix(in oklab, var(--slot) 82%, transparent);
  align-items: center;
}

@media (max-width: 26.5rem) {
  .medallero__casilla { width: 2.2rem; height: 2.2rem; }
  .medallero .moneda { width: 1.6rem; height: 1.6rem; }
}

/* --- La hoja de «Todo lo demás» ───────────── ng-component: NavMasComponent
   Sale desde la barra de abajo, no desde el borde de la pantalla: es el panel
   de ese botón y tiene que leerse como colgado de él. Va PEGADA al pie
   (`bottom: 100%` sobre la barra) para que el pulgar la alcance sin estirarse
   — es la mitad de abajo del teléfono, que es donde está la mano. */
.navmas {
  position: absolute; left: 0.6rem; right: 0.6rem; bottom: calc(100% + 0.4rem);
  z-index: 2;
  display: grid; gap: var(--sp-3);
  padding: var(--sp-4);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-2);
  animation: navmasEntra .18s var(--ease) both;
}
.navmas[hidden] { display: none; }
@keyframes navmasEntra { from { opacity: 0; transform: translateY(0.5rem); } }
@media (prefers-reduced-motion: reduce) { .navmas { animation: none; } }

.navmas__tit {
  font-family: var(--font-mono); font-size: var(--step--2);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
}
.navmas__grid {
  display: grid; gap: var(--sp-2);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 6.8rem), 1fr));
}
.navmas__item {
  min-height: 3.5rem;
  display: grid; place-items: center; gap: 0.2rem;
  padding: var(--sp-3) var(--sp-2);
  border-radius: var(--r-md);
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--muted); text-decoration: none; text-align: center;
  font-size: var(--step--2); font-weight: 600;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.navmas__item svg { width: 1.4rem; height: 1.4rem; }
.navmas__item:hover { color: var(--fg); border-color: var(--accent); }
.navmas__item[aria-current="page"] { color: var(--accent); border-color: var(--accent); }

@media (min-width: 960px) { .navmas { display: none; } }

/* La etiqueta baja un punto en pantallas muy angostas; el ícono, que es lo que
   un chico lee primero, no se toca. */
@media (max-width: 26.5rem) {
  .nav__item { font-size: 0.6rem; gap: 0.1rem; }
}
/* --- «Todo lo demás» del inicio ───────── ng-component: HomeLugaresComponent
   La contracara de haber achicado la barra de abajo: acá entran las pantallas
   que el pie no lleva. Grilla, no lista: son destinos equivalentes y ninguno
   manda sobre los otros. `auto-fit` con `minmax` para que a 360 entren dos por
   fila y en escritorio las que quepan, sin un solo `@media`. */
.lugares {
  display: grid; gap: var(--sp-3);
  /* 7.8rem y no 8.5: a 360px el panel mide 278px de adentro y con 8.5rem
     (136px) dos columnas piden 284 — se pasaba por SEIS PÍXELES y caía a una
     sola por fila, seis renglones de tarjeta llena. Medido a 360/414/768/1280:
     2 · 2 · 4 · 6 columnas. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.8rem), 1fr));
}
.lugar {
  min-height: 5rem;
  display: grid; place-items: center; gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-2);
  border-radius: var(--r-md); border: 1px solid var(--border);
  background: var(--surface-2); color: var(--muted);
  text-decoration: none; text-align: center;
  transition: color .18s var(--ease), border-color .18s var(--ease), background .18s var(--ease);
}
.lugar:hover { color: var(--fg); border-color: var(--accent); background: var(--surface-3); }
.lugar svg { width: 1.6rem; height: 1.6rem; }
.lugar__label { font-size: var(--step--1); font-weight: 600; }

.nav__item:hover { color: var(--fg); }
.nav__item[aria-current="page"] { color: var(--accent); background: oklch(80% 0.145 205 / .10); }
/* LA BARRA DE ABAJO NO SE ESCONDE. Acá vivía el auto-hide —`translateY(110%)`
   al bajar la página— que devolvía ~68px de alto a la hoja del álbum. Se fue
   por pedido: una barra que aparece y desaparece obliga a acordarse de que
   existe, y un chico que quiere volver al álbum mira abajo. Si a veces no
   está, lo que aprende es que el botón a veces no está.

   El listener se sacó de `js/ui.js` junto con esta regla: dejar el atributo
   `data-nav-hidden` puesto sin nadie que lo escriba sería dejar una perilla
   que no hace nada. */

@media (min-width: 960px) {
  .nav {
    position: sticky; inset: 0 auto 0 0; height: 100dvh; width: var(--rail-w);
    flex-direction: column; justify-content: flex-start; gap: 0.2rem;
    /* El rail va de borde a borde en vertical y pegado al filo izquierdo: le
       tocan las tres. En una tablet acostada con la muesca a la izquierda, el
       primer ícono quedaba debajo de ella. */
    padding: max(var(--sp-4), env(safe-area-inset-top)) var(--sp-3)
             max(var(--sp-4), env(safe-area-inset-bottom)) max(var(--sp-3), env(safe-area-inset-left));
    border-top: 0; border-right: 1px solid var(--border-soft);
    overflow: hidden;
  }
  .nav__head {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: 0 var(--sp-2) var(--sp-4); margin-bottom: var(--sp-2);
    border-bottom: 1px solid var(--border-soft);
  }
  .nav__logo {
    flex: 1; min-width: 0; overflow: hidden; white-space: nowrap;
    font-family: var(--font-display); font-size: var(--step-2); font-weight: 800;
    padding-left: var(--sp-2);
    transition: opacity .18s var(--ease), max-width .24s var(--ease);
  }
  .nav__logo b { color: var(--accent); }
  .nav__toggle {
    flex: 0 0 44px; width: 2.8rem; height: 2.8rem; display: grid; place-items: center;
    border-radius: var(--r-md); border: 1px solid var(--border);
    background: var(--surface-2); color: var(--muted); cursor: pointer;
    transition: color .18s var(--ease), border-color .18s var(--ease);
  }
  .nav__toggle:hover { color: var(--fg); border-color: var(--accent); }
  .nav__toggle svg { width: 1.1rem; height: 1.1rem; transform: rotate(180deg); transition: transform .24s var(--ease); }
  .nav__list { flex-direction: column; flex: 0 0 auto; gap: 0.2rem; }
  .nav__item {
    flex: 0 0 auto; flex-direction: row; justify-content: flex-start;
    gap: var(--sp-3); padding: 0 var(--sp-3); font-size: var(--step-0);
    min-height: 2.9rem;
  }
  .nav__label { overflow: hidden; white-space: nowrap; transition: opacity .18s var(--ease); }

  /* --- contraído --- */
  html[data-nav-state="collapsed"] .nav__logo { max-width: 0; opacity: 0; padding: 0; }
  html[data-nav-state="collapsed"] .nav__toggle svg { transform: rotate(0deg); }
  html[data-nav-state="collapsed"] .nav__head { justify-content: center; padding-inline: 0; }
  html[data-nav-state="collapsed"] .nav__item { justify-content: center; padding: 0; }
  html[data-nav-state="collapsed"] .nav__label { max-width: 0; opacity: 0; }
}

/* ── Botones ─────────────────────────────────────────── ng-component: ButtonComponent */
.btn {
  --btn-bg: var(--surface-2);
  --btn-fg: var(--fg);
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: 2.9rem; padding: 0 var(--sp-5);
  border: 1px solid var(--border); border-radius: var(--r-pill);
  background: var(--btn-bg); color: var(--btn-fg);
  font-family: var(--font-display); font-weight: 700; font-size: var(--step-0);
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: transform .16s var(--ease-bounce), filter .16s var(--ease), box-shadow .16s var(--ease);
}
.btn:hover:not(:disabled) { transform: translateY(-2px); filter: brightness(1.08); }
.btn:active:not(:disabled) { transform: translateY(0) scale(.97); }
.btn:disabled { opacity: .42; cursor: not-allowed; }
.btn svg { width: 1.1rem; height: 1.1rem; }
.btn--primary {
  --btn-bg: linear-gradient(135deg, var(--accent-hi), var(--accent));
  --btn-fg: var(--accent-ink);
  border-color: transparent; box-shadow: var(--shadow-glow);
}
.btn--lime { --btn-bg: var(--accent-2); --btn-fg: var(--accent-ink); border-color: transparent; }
.btn--ghost { --btn-bg: transparent; border-color: var(--border); }
.btn--danger { --btn-bg: transparent; --btn-fg: var(--danger); border-color: var(--danger); }
.btn--lg { min-height: 3.5rem; font-size: var(--step-1); padding: 0 var(--sp-6); }
.btn--block { width: 100%; }

/* ── Panel / módulo ──────────────────────────────────── ng-component: PanelComponent */
.panel {
  background: linear-gradient(180deg, var(--surface), var(--panel-2));
  border: 1px solid var(--border-soft); border-radius: var(--r-lg);
  padding: var(--sp-5); box-shadow: var(--shadow-1);
}
.panel__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); }
.panel__title { font-size: var(--step-1); }
.panel__hint { font-size: var(--step--1); color: var(--muted); }
.eyebrow {
  font-family: var(--font-mono); font-size: var(--step--2);
  letter-spacing: .14em; text-transform: uppercase; color: var(--accent);
}

/* ── Progreso ────────────────────────────────────── ng-component: ProgressBarComponent */
.bar { height: 0.8rem; border-radius: var(--r-pill); background: var(--bg-deep); overflow: hidden; border: 1px solid var(--border-soft); }
.bar__fill {
  /* `display: block` NO es de adorno: sin esto la barra sólo funciona si quien
     la usa eligió un <div>. Con un <span> —que es lo natural para algo que no
     es un bloque de contenido— el relleno queda `display: inline`, y en inline
     `width` y `height` no aplican: la barra se dibuja vacía, sin un error, sin
     un warning, con el `style="width:27%"` puesto y visible en el inspector.
     Pasó exactamente así en el álbum. Un componente que depende de qué etiqueta
     usaste es una trampa. */
  display: block;
  height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: width .6s var(--ease);
}
.ring { display: grid; place-items: center; position: relative; }
.ring svg { transform: rotate(-90deg); }
.ring__num { position: absolute; font-family: var(--font-display); font-weight: 800; font-size: var(--step-2); }

/* ── Chips / filtros ──────────────────────────────────── ng-component: ChipsComponent */
.chips { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.chip {
  min-height: 2.8rem; padding: 0 0.9rem;   /* 44px: objetivo táctil mínimo en móvil */
  display: inline-flex; align-items: center; gap: 0.4rem;
  border-radius: var(--r-pill); border: 1px solid var(--border);
  background: transparent; color: var(--muted);
  font-size: var(--step--1); font-weight: 600; cursor: pointer;
  transition: all .18s var(--ease);
}
.chip:hover { color: var(--fg); border-color: var(--accent); }
.chip[aria-pressed="true"] { background: var(--accent); border-color: transparent; color: var(--accent-ink); }
.chip__dot { width: 0.6rem; height: 0.6rem; border-radius: 50%; background: currentColor; }

/* ── Figurita ───────────────────────────────────────── ng-component: StickerComponent
   La figurita es una IMAGEN, no un SVG generado. Esquinas apenas redondeadas
   (10px), filete de 1px y nada de marco blanco grueso: el peso visual lo lleva
   la ilustración, no el envoltorio.

   `--slot-fs` sale de una container query sobre el propio hueco, no de un px
   multiplicado por --zoom: la tipografía queda como una PROPORCIÓN del hueco, así
   que escala sola con el zoom y con el breakpoint sin una sola fórmula más. Con
   el valor fijo anterior, un hueco de 160px seguía con la etiqueta de 9px y los
   nombres largos se cortaban en «CHARMANDE». */
.sticker {
  container-type: inline-size;
  --slot-fs: 8cqi;
  position: relative; aspect-ratio: 3 / 4;
  border-radius: 0.6rem; overflow: hidden;
  background: var(--slot);
  border: 1px solid var(--slot-line);
  display: grid; grid-template-rows: 1fr auto;
  transition: transform .18s var(--ease), box-shadow .18s var(--ease);
  cursor: pointer;
}
.sticker:hover { transform: translateY(-2px); box-shadow: 0 6px 18px oklch(10% 0.05 260 / .38); }
/* Una figurita es un OBJETO, no un párrafo. Arrastrar por la hoja tiene que
   pasar la hoja; sin esto, el gesto deja el número y el nombre pintados de azul
   —y en el álbum de un chico eso no se lee como «seleccionaste texto», se lee
   como que algo se rompió—. El clic no cambia: `user-select` no toca eventos.
   `-webkit-user-drag` es la otra mitad: sin él, arrastrar desde la ilustración
   levanta el fantasma de la imagen que el navegador ofrece para soltar en otra
   ventana, y el gesto de pasar hoja se pierde ahí. */
.sticker, .slot { user-select: none; -webkit-user-select: none; }
.sticker img, .slot img { -webkit-user-drag: none; }
.sticker__art {
  position: relative; display: grid; place-items: center; overflow: hidden;
  background: linear-gradient(168deg, var(--slot), var(--slot-2));
}
.sticker__art img {
  width: 88%; height: 88%; object-fit: contain;
  filter: drop-shadow(0 2px 3px oklch(20% 0.04 258 / .22));
}
/* El pie lleva SÓLO el nombre, como la banda impresa de una figurita de verdad.
   Antes traía además un punto de color y una letra ("• H") para la rareza: es
   información que el filete ya cuenta —neutro común, de color rara, iridiscente
   holo— y encima con un código que nadie explicó en ninguna pantalla. Duplicar
   el dato en un chip convertía la figurita en una tarjeta de UI.
   Tampoco lleva línea divisoria: la banda se apoya sobre la ilustración como
   tinta impresa, no como el footer de una card. */
.sticker__foot {
  display: flex; align-items: center; justify-content: center;
  padding: 0.3rem 0.4rem; background: var(--slot); color: var(--ink);
}
.sticker__name {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--slot-fs, 10px) * 1.05);
  text-transform: uppercase; letter-spacing: .01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
.sticker__count {
  position: absolute; top: 0.3rem; left: 0.3rem; z-index: 2;
  min-width: 1.6rem; height: 1.4rem; padding: 0 0.4rem;
  display: grid; place-items: center; border-radius: var(--r-pill);
  background: var(--accent-2); color: var(--accent-ink);
  font-family: var(--font-mono); font-weight: 700; font-size: calc(var(--slot-fs, 10px) * .95);
}

/* Rarezas: un filete, no un aura. Vocabulario del ADR: common | rare | holo.
   Desde que se fue el chip del pie, ESTE es el único lugar donde se lee la
   rareza en la hoja — filete neutro, filete de color, borde iridiscente — así
   que la escala tiene que sostenerse sola. Se fue también la variable --rar:
   sólo la consumía el punto de color que ya no existe. */
.sticker[data-rarity="rare"] { border-color: var(--r-rare); }
.sticker[data-rarity="holo"] {
  border: 2px solid transparent;
  background:
    linear-gradient(var(--slot), var(--slot)) padding-box,
    conic-gradient(from 180deg, var(--r-legend-a), var(--r-legend-b),
                   var(--r-legend-c), var(--r-legend-a)) border-box;
}

/* ── Slot del álbum ─────────────────────────────────────── ng-component: SlotComponent
   Hueco impreso: filete punteado y el número en el centro, como en la hoja real. */
.slot {
  container-type: inline-size;
  --slot-fs: 8cqi;
  position: relative; aspect-ratio: 3 / 4;
  border-radius: 0.6rem;
  border: 1px dashed color-mix(in oklab, var(--sheet-ink) 55%, transparent);
  background: color-mix(in oklab, var(--sheet-ink) 12%, transparent);
  display: grid; place-items: center;
  color: color-mix(in oklab, var(--sheet-ink) 80%, transparent);
  font-family: var(--font-mono); padding: 0; cursor: default;
}
/* Hueco listo para pegar. Antes era un "ping" de notificación; ahora respira y
   flota apenas, como una figurita apoyada esperando que la aprieten. La
   invitación es la flotación, no el destello. */
.slot--pending {
  border: 2px solid var(--accent-2); cursor: pointer;
  background: color-mix(in oklab, var(--accent-2) 16%, transparent);
  color: var(--accent-2);
  animation: pendingFloat 2.4s var(--ease) infinite;
  will-change: transform, box-shadow;
}
.slot--pending:hover { animation-play-state: paused; transform: translateY(-4px) scale(1.02); }
.slot--pending::after {
  content: "Pegar"; position: absolute; bottom: 0.4rem;
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--slot-fs, 10px) * 1.05); text-transform: uppercase; letter-spacing: .04em;
}
@keyframes pendingFloat {
  0%, 100% { transform: translateY(0);      box-shadow: 0 0 0 0 oklch(86% .19 132 / .38); }
  45%      { transform: translateY(-3.5px); box-shadow: 0 5px 14px oklch(86% .19 132 / .28); }
}

/* Entrada escalonada de la hoja. `--i` lo pone el render; el retraso está
   topeado para que una hoja de 20 no tarde más que una de 6. */
@keyframes slotIn {
  from { opacity: 0; transform: translateY(12px) scale(.94); }
}
.sheet__grid > * {
  animation: slotIn .38s var(--ease-bounce) both;
  animation-delay: calc(min(var(--i, 0) * 18, 320) * 1ms);
}
@keyframes pendingPulse {
  0%, 100% { box-shadow: 0 0 0 0 oklch(86% 0.19 132 / .5); }
  50%      { box-shadow: 0 0 0 10px oklch(86% 0.19 132 / 0); }
}
.slot__n { font-size: calc(var(--slot-fs, 10px) * 1.9); font-weight: 700; opacity: .55; }

/* ── Slap: pegar la figurita ────────────────────────────────────────────────
   Es EL gesto del producto, así que tiene los tres tiempos completos:
   anticipación (entra grande, alta y desenfocada), impacto (aplasta contra el
   papel en el fotograma 58%) y asentamiento (dos rebotes que decrecen).
   El anillo va por `box-shadow` y no por un pseudo-elemento porque la figurita
   tiene `overflow:hidden` — un ::after quedaría recortado. */
@keyframes slap {
  0%   { transform: translateY(-22px) scale(2.05) rotate(-11deg); opacity: 0; filter: blur(4px); }
  40%  { opacity: 1; filter: blur(0); }
  58%  { transform: translateY(0) scale(.86, .94) rotate(2.5deg); }   /* squash */
  72%  { transform: translateY(-3px) scale(1.07, 1.02) rotate(-1.4deg); } /* stretch */
  86%  { transform: translateY(0) scale(.975) rotate(.5deg); }
  100% { transform: translateY(0) scale(1) rotate(0); }
}
@keyframes slapRing {
  0%   { box-shadow: 0 0 0 0 oklch(86% .19 132 / 0); }
  30%  { box-shadow: 0 0 0 0 oklch(86% .19 132 / .8); }   /* recién en el impacto */
  100% { box-shadow: 0 0 0 26px oklch(86% .19 132 / 0); }
}
.is-slapping {
  animation: slap .62s var(--ease-bounce) both, slapRing .62s var(--ease) both;
  will-change: transform, box-shadow;
  z-index: 2;
}

/* ── Destello holo ──────────────────────────────────────────────────────────
   UNA pasada de luz, no un loop. Antes esto era un cónico girando en bucle
   sobre cada holo: llamaba la atención todo el tiempo y por eso no llamaba la
   atención nunca. Ahora aparece cuando la carta ES un evento (se revela, se
   pega) o cuando la señalás con el mouse. */
.sticker[data-rarity="holo"] .sticker__art::after {
  content: ""; position: absolute; inset: -30%;
  background: linear-gradient(68deg,
    transparent 38%,
    oklch(96% .10 190 / .55) 46%,
    oklch(92% .14 320 / .65) 50%,
    oklch(96% .12 130 / .55) 54%,
    transparent 62%);
  transform: translateX(-115%);
  pointer-events: none;
}
.sticker[data-rarity="holo"]:hover .sticker__art::after,
.sticker[data-rarity="holo"].is-slapping .sticker__art::after,
.sticker[data-rarity="holo"].is-revealing .sticker__art::after {
  animation: holoSweep .95s var(--ease) both;
}
@keyframes holoSweep {
  from { transform: translateX(-115%); }
  to   { transform: translateX(115%); }
}

/* ── Grilla ───────────────────────────────────────────────────────────────── */
.grid-cards { display: grid; gap: var(--sp-3); grid-template-columns: repeat(3, 1fr); }
@media (min-width: 600px)  { .grid-cards { grid-template-columns: repeat(4, 1fr); gap: var(--sp-4); } }
@media (min-width: 1280px) { .grid-cards { grid-template-columns: repeat(6, 1fr); } }
.grid-modules { display: grid; gap: var(--sp-4); grid-template-columns: 1fr; }
@media (min-width: 600px)  { .grid-modules { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1280px) { .grid-modules { grid-template-columns: repeat(3, 1fr); } }

/* ── Modal ───────────────────────────────────────────── ng-component: DialogComponent */
.modal[open] { display: grid; }
.modal {
  position: fixed; inset: 0; z-index: 90; display: none;
  /* El tamaño tiene que ser EXPLÍCITO de punta a punta. Un `<dialog>` viene del
     navegador con `width: fit-content` y un `max-width` propio, así que se medía
     por su contenido; y como la ficha declara `container-type: inline-size`, ese
     contenido dejó de contribuir a la medida y el modal colapsó a 50px de ancho
     —padding y bordes, nada más—. Con el ancho fijado acá, la columna vale lo
     que la pantalla y el `100%` de la ficha vuelve a significar algo. */
  width: 100%; height: 100%; max-width: none; max-height: none;
  /* Cubre la pantalla entera, así que le tocan las cuatro: arriba la franja del
     reloj, abajo la del gesto de inicio, y los costados la muesca acostado. */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
  /* Y el COLOR también hay que declararlo. El navegador le pone `color:
     CanvasText` a todo `<dialog>` —negro— y eso PISA lo que hereda del body, así
     que cada texto de la ficha que no traía color propio salía negro sobre el
     panel oscuro: el nombre de la figurita, «Su línea», los valores de la
     tabla y la ✕ quedaban en 1,24:1 de contraste, ilegibles. Los que sí traían
     color —la descripción, los rótulos— se veían bien, y por eso el problema
     pasaba por «tipografía rara» en vez de por «bug». */
  color: var(--fg);
  grid-template-columns: minmax(0, 1fr);
  place-items: center; padding: var(--sp-4);
  background: var(--scrim); backdrop-filter: blur(6px);
  border: 0;
}
.modal::backdrop { background: transparent; }
.modal__card {
  container-type: inline-size;                  /* la ficha se acomoda al modal, no al viewport */
  width: min(32.5rem, 100%); max-height: 88dvh; overflow: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-xl); padding: var(--sp-5);
  box-shadow: var(--shadow-2);
  animation: popIn .3s var(--ease-bounce) both;
}
@keyframes popIn { from { transform: scale(.9) translateY(14px); opacity: 0; } }
/* Pantalla baja —un teléfono acostado son 390px de alto— el aire cuesta caro:
   el padding y el margen del modal valen tres líneas de ficha. */
@media (max-height: 560px) {
  .modal { padding: var(--sp-2); }
  .modal__card { padding: var(--sp-4); max-height: 94dvh; }
}
.modal__close {
  min-width: 2.8rem; min-height: 2.8rem; border-radius: var(--r-pill);
  border: 1px solid var(--border); background: var(--surface-2); cursor: pointer;
}

/* ── Detalle de figurita ────────────────────── ng-component: CardDetailComponent
   El segundo nivel de zoom: la ilustración a tamaño grande. Se muestra el thumb
   al instante (`is-lowres`, apenas suavizado) y se cambia por la versión en alta
   cuando termina de cargar. Sin red, se queda el thumb y no rompe nada. */
/* La ficha entra en dos columnas apenas hay lugar. El corte lo decide el ancho
   del MODAL —no el del viewport— porque es el modal el que tiene que repartir.
   Debajo del corte sigue siendo una columna, que es lo correcto en un teléfono
   parado: la ilustración primero, el dato después. */
.modal__card:has(.detail) { width: min(55rem, 100%); }
.modal__card:has(.pieza-detalle) { width: min(47.5rem, 100%); }
.detail { display: grid; gap: var(--sp-4); align-items: start; }
@container (min-width: 620px) {
  .detail { grid-template-columns: minmax(0, 0.95fr) minmax(0, 1fr); column-gap: var(--sp-5); }
  .detail .detail__art { margin-bottom: 0; }
}
/* La ilustración tiene un TECHO EN ALTURA, no sólo un ancho. Con `aspect-ratio`
   sola, un teléfono acostado (390px de alto) le daba 520px de lado y empujaba
   el nombre y todo lo demás fuera de la pantalla: se veía media planta y nada
   más. Ahora el arte nunca se lleva más del 46% del alto disponible. */
.detail__art {
  margin: 0 0 var(--sp-4); position: relative;
  width: 100%; max-width: min(46dvh, 23.8rem); margin-inline: auto;
  aspect-ratio: 1; border-radius: var(--r-lg); overflow: hidden;
  background: radial-gradient(120% 90% at 50% 20%, var(--slot), var(--slot-2));
  display: grid; place-items: center;
  border: 1px solid var(--slot-line);
}
.detail__art img {
  width: 82%; height: 82%; object-fit: contain;
  transition: filter .3s var(--ease);
  filter: drop-shadow(0 6px 12px oklch(20% 0.04 258 / .28));
}
.detail__art img.is-lowres { filter: blur(.4px) drop-shadow(0 6px 12px oklch(20% 0.04 258 / .28)); }
.detail__num {
  position: absolute; right: 0.6rem; bottom: 0.5rem;
  /* El número va sobre la ilustración, que es papel claro: acá el texto claro no
     sirve. `--ink-soft` daba 3,08:1 a 12px; `--ink` da 8:1. */
  font-size: var(--step--2); color: var(--ink);
}
.detail__name { font-size: var(--step-2); margin-bottom: var(--sp-2); }
.detail__desc { color: var(--muted); font-size: var(--step--1); margin-bottom: var(--sp-4); }
.detail__facts {
  display: grid; grid-template-columns: auto 1fr; gap: 0.4rem var(--sp-4);
  margin: 0 0 var(--sp-4); font-size: var(--step--1);
}
.detail__facts dt { color: var(--muted); }
.detail__facts dd { margin: 0; }
.detail__subtitle { font-size: var(--step-0); margin-bottom: var(--sp-3); }
.evos { display: grid; gap: var(--sp-2); }
.evo {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 0.4rem 0.6rem 0.4rem 0.4rem; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--border-soft);
}
.evo[data-goto], .evo[data-locate] { cursor: pointer; }
.evo[data-goto]:hover, .evo[data-locate]:hover { border-color: var(--accent); }
.evo:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* La de su línea que todavía no conseguiste. El hueco se dibuja como en la hoja
   —filete punteado y el número— para que se lea "esto te falta" sin explicarlo,
   y el destino va a la derecha porque tocarla no abre una ficha: te lleva ahí. */
.evo--falta .evo__ph {
  background: transparent;
  border: 1px dashed color-mix(in oklab, var(--muted) 60%, transparent);
}
.evo__go {
  margin-left: auto; flex: 0 0 auto; padding-left: var(--sp-3);
  color: var(--accent); font-size: var(--step--2); white-space: nowrap;
}
.evo__go::after { content: " →"; }
.evo img { width: 2.8rem; height: 2.8rem; object-fit: contain; flex: 0 0 44px; }
.evo__ph {
  width: 2.8rem; height: 2.8rem; flex: 0 0 44px; display: grid; place-items: center;
  border-radius: var(--r-sm); background: var(--bg-deep); color: var(--muted); font-size: var(--step--2);
}
.evo__meta { display: grid; }
.evo__meta b { font-family: var(--font-display); font-size: var(--step--1); }
.evo__meta small { color: var(--muted); font-size: var(--step--2); }
/* Píldora de tipo. El color es dato (Fuego naranja, Agua azul: convención de la
   franquicia), así que se sale de la paleta fría a propósito — pero acotado a un
   punto de 8px y un borde teñido, nunca a un relleno que compita con la rareza. */
.tpill {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.1rem 0.6rem 0.1rem 0.4rem; border-radius: var(--r-pill);
  background: oklch(70% .16 var(--t-hue) / .13);
  border: 1px solid oklch(70% .16 var(--t-hue) / .5);
  font-size: var(--step--1);
}
.tpill i { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: oklch(70% .16 var(--t-hue)); }
/* La selección no es un puesto y no lleva color de puesto: si le pusiera hue,
   dos píldoras teñidas al lado se leerían como dos datos del mismo eje. Va en
   neutro, que es lo que la deja subordinada a la que sí codifica algo. */
.tpill--equipo {
  background: color-mix(in oklab, var(--fg) 7%, transparent);
  border-color: var(--border);
}
.tpill--equipo i { background: var(--muted); }

.detail__credit { margin-top: var(--sp-4); color: var(--muted); font-size: var(--step--2); }

/* ── EL HUECO DECLARADO: figurita pegada, lámina todavía no impresa ─────────
   Las 720 figuritas existen con su ficha, su selección y su puesto. Lo que no
   existe todavía es el DIBUJO: el arte va a ser de figuras de bloques y llega
   más adelante. `js/ui.js` marca la figurita con `data-arte="falta"` cuando el
   .webp no está en disco, y esto es lo que se dibuja en su lugar.

   POR QUÉ NO HAY UNA SILUETA DE JUGADOR ACÁ. Dibujar un muñequito genérico
   sería inventar el arte que falta, y un dibujo inventado es peor que un hueco:
   el chico lo aprende como si fuera la figurita, y el día que llegue la de
   verdad la cambia por otra. Lo que va es una trama de bloques —geometría, no
   figura— y el número bien grande. Se lee como cartón sin imprimir, que es
   exactamente lo que es.

   El día que aparezca `assets/cards/017.webp`, nada de esto se muestra: el
   `onerror` no dispara y la figurita se enciende sola. */
.sticker__falta,
.trofeo__falta,
.detail__falta { display: none; }

[data-arte="falta"] img { visibility: hidden; }

/* La trama de bloques. El punto sale de `--slot-line`, que es el mismo trazo con
   el que la hoja dibuja un hueco vacío: la figurita sin lámina y el hueco sin
   figurita hablan con la misma tinta, que es lo que los hace leer como parientes
   y no como dos errores distintos. */
.sticker[data-arte="falta"] .sticker__art,
.trofeo[data-arte="falta"] .trofeo__campana,
.detail__art[data-arte="falta"] {
  background-image:
    radial-gradient(circle at 50% 50%,
      color-mix(in oklab, var(--slot-line) 80%, transparent) 0 20%, transparent 21%),
    linear-gradient(168deg, var(--slot), var(--slot-2));
  background-size: 20% 20%, 100% 100%;
}
.sticker[data-arte="falta"] .sticker__falta,
.trofeo[data-arte="falta"] .trofeo__falta,
.detail__art[data-arte="falta"] .detail__falta {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-weight: 700; letter-spacing: .04em;
  color: var(--ink-soft);
}
.sticker[data-arte="falta"] .sticker__falta { font-size: calc(var(--slot-fs, 10px) * 2.4); }
.detail__art[data-arte="falta"] .detail__falta { font-size: var(--step-4); }
/* Sobre el vidrio de la vitrina la tinta oscura no se lee: ahí manda el fondo
   oscuro de la campana, no el papel blanco de la figurita. */
.trofeo[data-arte="falta"] .trofeo__falta { font-size: var(--step-4); color: var(--fg); }
/* El brillo de la brillante se apaga mientras no haya lámina: un iridiscente
   barriendo un cartón vacío promete un dibujo que no está. */
.sticker[data-arte="falta"][data-rarity="holo"] .sticker__art::after { display: none; }

/* Y la misma trama para cualquier OTRA imagen de figurita que quede sin lámina:
   el memotest, el pop it, la sala, la vitrina, el elenco de la historia. A esas
   `js/ui.js` les cambia el src por un píxel transparente, así que acá hay una
   caja del tamaño justo esperando que la pinten. Sin esto se vería el ícono de
   imagen rota del navegador, que dice «algo falló» cuando lo que pasa es que
   todavía no imprimimos. */
img[data-sin-lamina] {
  background-image:
    radial-gradient(circle at 50% 50%,
      color-mix(in oklab, var(--slot-line) 80%, transparent) 0 20%, transparent 21%),
    linear-gradient(168deg, var(--slot), var(--slot-2));
  background-size: 20% 20%, 100% 100%;
  border-radius: 0.4rem;
}

/* ── Control de zoom ──────────────────────────── ng-component: ZoomControlComponent */
.zoom {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 0.4rem; border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--border-soft);
  /* No puede ser más ancho que su fila. Sin esto, el control entero medía 305px
     de `min-content` dentro de un panel de 288 y empujaba la página 26px a la
     derecha en un teléfono de 320. Los dos botones NO se achican —44px es el
     mínimo táctil— así que el que cede es el riel. */
  max-width: 100%; flex-wrap: wrap; justify-content: center;
}
.zoom__btn {
  width: 2.8rem; height: 2.8rem; flex: 0 0 44px; display: grid; place-items: center;
  border-radius: var(--r-pill); border: 1px solid var(--border);
  background: transparent; color: var(--fg); cursor: pointer;
  font-family: var(--font-display); font-size: var(--step-1); line-height: 1;
}
.zoom__btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.zoom__btn:disabled { opacity: .35; cursor: not-allowed; }
/* `min-width: 0` y no 90: un `<input type="range">` trae un ancho intrínseco
   propio, y con un mínimo fijo era él quien no dejaba encoger al grupo. */
.zoom__range { flex: 1 1 90px; min-width: 0; accent-color: var(--accent); height: 2.8rem; }
.zoom__val { font-family: var(--font-mono); font-size: var(--step--1); color: var(--muted); min-width: 2.9rem; text-align: right; }

/* ── Toast ────────────────────────────────────────────── ng-component: ToastComponent */
.toasts {
  position: fixed; z-index: 95; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + 0.8rem);
  display: grid; gap: 0.5rem; width: min(26.2rem, calc(100% - 1.5rem));
}
@media (min-width: 960px) { .toasts { bottom: 1.2rem; left: auto; right: 1.2rem; transform: none; } }
.toast {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.8rem 1rem; border-radius: var(--r-pill);
  background: var(--surface-3); border: 1px solid var(--border);
  box-shadow: var(--shadow-2); font-size: var(--step--1); font-weight: 600;
  animation: toastIn .32s var(--ease-bounce) both;
}
.toast[data-type="ok"] { border-color: var(--ok); color: var(--ok); }
.toast[data-type="warn"] { border-color: var(--warn); color: var(--warn); }
@keyframes toastIn { from { transform: translateY(16px); opacity: 0; } }

/* ── Sobres ─────────────────────────────────────────── ng-component: PackCardComponent */
.pack {
  position: relative; aspect-ratio: 3 / 4; border-radius: var(--r-lg);
  border: 1px solid var(--border); overflow: hidden;
  display: grid; place-items: center;
  background: linear-gradient(160deg, var(--surface-3), var(--bg-deep));
}
.pack::before {
  content: ""; position: absolute; inset: -40%;
  background: linear-gradient(115deg, transparent 42%, oklch(100% 0 0 / .22) 50%, transparent 58%);
  animation: shimmer 3.4s var(--ease) infinite;
}
@keyframes shimmer { from { transform: translateX(-60%); } to { transform: translateX(60%); } }
.pack[data-kind="elite"]  { background: linear-gradient(160deg, oklch(40% 0.13 210), var(--bg-deep)); }
.pack[data-kind="prisma"] { background: linear-gradient(160deg, oklch(42% 0.16 305), var(--bg-deep)); }
.pack__label {
  position: relative; z-index: 1; text-align: center;
  font-family: var(--font-display); font-weight: 800; font-size: var(--step-2);
  text-transform: uppercase; letter-spacing: .04em;
}

/* ── Lista de items (repetidas / ofertas) ──────────── ng-component: ListRowComponent */
.rowcard {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3); border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--border-soft);
  transition: border-color .18s var(--ease);
}
.rowcard:hover { border-color: var(--accent); }
.rowcard__thumb { width: 3.4rem; flex: 0 0 54px; }
.rowcard__thumb .sticker { --slot-fs: 7px; }
.rowcard[aria-selected="true"] { border-color: var(--accent-2); background: oklch(30% 0.06 150 / .3); }

/* ── Chat ──────────────────────────────────────────── ng-component: TradeChatComponent */
.chat { display: grid; gap: var(--sp-3); max-height: 20rem; overflow-y: auto; padding-right: 0.2rem; }
.msg { max-width: 82%; padding: 0.6rem 0.9rem; border-radius: var(--r-lg); font-size: var(--step--1); }
.msg--them { background: var(--surface-2); border-bottom-left-radius: 6px; justify-self: start; }
.msg--me { background: var(--accent); color: var(--accent-ink); border-bottom-right-radius: 6px; justify-self: end; }
.chat-form { display: flex; gap: var(--sp-2); }
.input {
  flex: 1; min-height: 2.9rem; padding: 0 var(--sp-4);
  border-radius: var(--r-pill); border: 1px solid var(--border);
  background: var(--bg-deep); color: var(--fg);
}
.input:focus { border-color: var(--accent); outline: none; }

/* ── Estado vacío ──────────────────────────────────────────────────────────── */
.empty { display: grid; gap: var(--sp-3); place-items: center; padding: var(--sp-7) var(--sp-4); text-align: center; color: var(--muted); }
.empty__mark { width: 4rem; height: 4rem; opacity: .45; }

/* ══ FX · las celebraciones ═══════════════════════════ ng-service: FxService
   Todo vive en un host fijo sobre el resto de la app, sin capturar clics y
   fuera del árbol de accesibilidad: es decoración, y la información ya la dio
   el toast, la cinta o el número de la billetera. */
.fx-host {
  position: fixed; inset: 0; z-index: 90;
  pointer-events: none; overflow: hidden;
}

/* --- monedas volando --------------------------------------------------------
   Tres puntos, no dos: origen, cima del arco y destino. Con una interpolación
   recta la moneda viaja como un misil; el arco es lo que la hace pesar. */
.fx-coin {
  position: absolute; left: 0; top: 0; width: 1.4rem; height: 1.4rem; border-radius: 50%;
  background:
    radial-gradient(circle at 34% 30%, oklch(96% .14 95), oklch(80% .17 88) 58%, oklch(64% .15 80));
  box-shadow: 0 0 12px oklch(88% .17 95 / .6), inset 0 -2px 0 oklch(58% .13 78 / .8);
  animation: fxCoin 780ms cubic-bezier(.4, 0, .5, 1) both;
  will-change: transform, opacity;
}
@keyframes fxCoin {
  0%   { transform: translate(var(--x0), var(--y0)) translate(-50%, -50%) scale(.4); opacity: 0; }
  14%  { opacity: 1; transform: translate(var(--x0), var(--y0)) translate(-50%, -50%) scale(1); }
  55%  { transform: translate(var(--xm), var(--ym)) translate(-50%, -50%) scale(1.1) rotate(180deg); }
  100% { transform: translate(var(--x1), var(--y1)) translate(-50%, -50%) scale(.3) rotate(360deg); opacity: 0; }
}
/* El latido de la billetera: llega la moneda, el número acusa recibo. */
.fx-pulse { animation: fxPulse 380ms var(--ease-bounce); }
@keyframes fxPulse { 0%, 100% { transform: none; } 40% { transform: scale(1.22); } }

/* --- pegar una lámina ────────────────────────── ng-component: PegarOverlay
   La caja se planta EXACTAMENTE sobre el hueco de la hoja: `fx.js` le pasa el
   `getBoundingClientRect()` del botón, y como el host es `fixed; inset: 0`, las
   coordenadas del viewport sirven tal cual. No hay medida escrita acá que
   tenga que coincidir con la del álbum — si mañana la figurita cambia de
   tamaño, esto la sigue solo.

   Las tres rarezas se separan por CANTIDAD DE TIEMPOS, no por color: el bronce
   tiene uno (cae), la plata dos (cae y brilla), el oro tres (cae, brilla y se
   abre). El metal está, pero es la firma, no la diferencia. Ver `Fx.pegar()`. */
.fx-pega {
  position: absolute;
  display: grid; place-items: center;
  will-change: transform;
}
.fx-pega[data-metal="bronce"] { --m: var(--metal-bronce); --m-luz: var(--metal-bronce-luz); }
.fx-pega[data-metal="plata"]  { --m: var(--metal-plata);  --m-luz: var(--metal-plata-luz); }
.fx-pega[data-metal="oro"]    { --m: var(--metal-oro);    --m-luz: var(--metal-oro-luz); }

/* La lámina que llega. Cae desde arriba y torcida, y se endereza al tocar: una
   figurita que baja perfectamente derecha no parece pegada por una mano. */
.fx-pega__hoja {
  position: absolute; inset: 0;
  border-radius: var(--r-sm); overflow: hidden;
  border: 2px solid var(--m-luz);
  box-shadow: 0 10px 22px -6px oklch(15% .04 260 / .55);
  background: var(--slot);
  animation: fxPegaCae 520ms cubic-bezier(.34, .9, .38, 1) both;
  will-change: transform, opacity;
}
.fx-pega__hoja img { width: 100%; height: 100%; object-fit: cover; display: block; }
@keyframes fxPegaCae {
  0%   { transform: translate(6%, -46%) scale(1.5) rotate(-9deg); opacity: 0; }
  12%  { opacity: 1; }
  /* 80% de 520ms = 416ms: el golpe. `Fx.pegar` cambia el hueco por la figurita
     en 420ms, así que la lámina que vuela y la que queda se cruzan en el mismo
     cuadro y el cambio no se ve. */
  80%  { transform: none; opacity: 1; }
  88%  { transform: scale(1.05, .93); }
  100% { transform: none; opacity: 0; }
}

/* El golpe: un cuadrado del metal que se abre y se apaga. Es lo único que
   tienen las tres, y en el bronce es lo único que hay. */
.fx-pega__sello {
  position: absolute; inset: 0;
  border-radius: var(--r-sm);
  border: 2px solid var(--m-luz);
  animation: fxPegaSello 460ms 420ms ease-out both;
}
@keyframes fxPegaSello {
  0%   { transform: scale(1); opacity: .95; }
  100% { transform: scale(1.34); opacity: 0; }
}

/* El brillo de la plata y del oro: una banda que cruza en diagonal. */
.fx-pega__brillo {
  position: absolute; inset: 0;
  border-radius: var(--r-sm); overflow: hidden;
  animation: fxPegaBrillo 620ms 540ms ease-in-out both;
}
.fx-pega__brillo::before {
  content: ''; position: absolute; top: -60%; bottom: -60%; left: -40%; width: 45%;
  transform: rotate(18deg);
  background: linear-gradient(90deg,
    transparent,
    color-mix(in oklab, var(--m-luz) 82%, transparent),
    transparent);
}
@keyframes fxPegaBrillo {
  0%   { transform: translateX(-130%); opacity: 0; }
  18%  { opacity: 1; }
  82%  { opacity: 1; }
  100% { transform: translateX(130%); opacity: 0; }
}

/* Los ocho rayos: sólo el oro. Es el tercer tiempo, el que hace que la
   legendaria no se termine cuando las otras dos ya terminaron. */
.fx-pega__rayos { position: absolute; inset: 0; display: grid; place-items: center; }
.fx-pega__rayos i {
  position: absolute; width: 0.3rem; height: 46%;
  border-radius: 999px;
  background: linear-gradient(var(--m-luz), transparent);
  transform-origin: 50% 100%;
  animation: fxPegaRayo 700ms 500ms ease-out both;
}
.fx-pega__rayos i:nth-child(1) { --g: 0deg; }
.fx-pega__rayos i:nth-child(2) { --g: 45deg; }
.fx-pega__rayos i:nth-child(3) { --g: 90deg; }
.fx-pega__rayos i:nth-child(4) { --g: 135deg; }
.fx-pega__rayos i:nth-child(5) { --g: 180deg; }
.fx-pega__rayos i:nth-child(6) { --g: 225deg; }
.fx-pega__rayos i:nth-child(7) { --g: 270deg; }
.fx-pega__rayos i:nth-child(8) { --g: 315deg; }
@keyframes fxPegaRayo {
  0%   { transform: rotate(var(--g)) translateY(-10%) scaleY(.1); opacity: 0; }
  22%  { opacity: 1; }
  100% { transform: rotate(var(--g)) translateY(-92%) scaleY(1); opacity: 0; }
}

/* La figurita recién pegada queda marcada mientras dura el efecto: es lo que
   contesta «¿cuál era?» cuando la hoja tiene cuarenta y el chico miró para
   otro lado. Sobrevive a `prefers-reduced-motion` porque no se mueve. */
.fig[data-recien-pegada] { z-index: 2; }
.fig[data-recien-pegada="bronce"] { --mm: var(--metal-bronce-luz); }
.fig[data-recien-pegada="plata"]  { --mm: var(--metal-plata-luz); }
.fig[data-recien-pegada="oro"]    { --mm: var(--metal-oro-luz); }
.fig[data-recien-pegada] .fig__marco {
  box-shadow: 0 0 0 3px var(--mm), 0 0 18px -2px var(--mm);
}

@media (prefers-reduced-motion: reduce) {
  .fx-pega__hoja, .fx-pega__sello, .fx-pega__brillo, .fx-pega__rayos i { animation: none; }
}

/* --- panel de demo ─────────────────────── ng-component: DemoPanelComponent
   Deliberadamente FUERA del lenguaje del producto: monoespaciada, gris, sin
   redondeos grandes ni acento. Tiene que leerse como herramienta y no como una
   pantalla más del álbum. Sólo existe con `?demo=1`, y en Angular va al dominio
   `development` detrás de un guard — nunca a `public`.

   Vive acá y no en la pantalla porque lo usan DOS: el álbum y la historia. */
.demo {
  position: fixed; left: 0.8rem; bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + 0.8rem); z-index: 60;
  display: grid; gap: 0.4rem; padding: 0.6rem;
  border-radius: var(--r-sm); border: 1px dashed var(--border);
  background: oklch(14% .03 262 / .94); backdrop-filter: blur(8px);
  max-width: 11.9rem;
}
@media (min-width: 960px) { .demo { bottom: 0.8rem; } }
.demo__title { font-size: var(--step--2); letter-spacing: .16em; text-transform: uppercase; color: var(--muted); }
.demo .btn { min-height: 2.4rem; font-size: var(--step--1); padding-inline: 0.8rem; }
.demo__seg { display: flex; gap: 0.4rem; }
.demo__seg .chip { flex: 1; min-height: 2.4rem; font-size: var(--step--2); padding-inline: 0.6rem; }

/* --- el sobre que se compra -------------------------------------------------
   Mismo arco de tres puntos que la moneda, otra masa: el sobre es grande y
   pesado, así que tarda más, gira poco y llega chico. Una moneda revolotea; un
   sobre se guarda. Los fondos son los MISMOS que `.pack[data-kind]` — si el
   sobre que vuela no fuera del color del que apretaste, se lee como otra cosa. */
.fx-pack {
  position: absolute; left: 0; top: 0; border-radius: var(--r-md);
  background: linear-gradient(160deg, var(--surface-3), var(--bg-deep));
  border: 1px solid var(--border);
  box-shadow: 0 10px 30px oklch(8% .04 260 / .55);
  animation: fxPack 620ms cubic-bezier(.35, 0, .4, 1) both;
  will-change: transform, opacity;
}
.fx-pack[data-kind="elite"]  { background: linear-gradient(160deg, oklch(40% 0.13 210), var(--bg-deep)); }
.fx-pack[data-kind="prisma"] { background: linear-gradient(160deg, oklch(42% 0.16 305), var(--bg-deep)); }
@keyframes fxPack {
  0%   { transform: translate(var(--x0), var(--y0)) translate(-50%, -50%) scale(1); opacity: 1; }
  55%  { transform: translate(var(--xm), var(--ym)) translate(-50%, -50%) scale(.5) rotate(-8deg); }
  100% { transform: translate(var(--x1), var(--y1)) translate(-50%, -50%) scale(.06) rotate(4deg); opacity: 0; }
}

/* La chispa del remate: dice la rareza GARANTIZADA, no el precio. El color lo
   pone el JS con el hue de esa rareza. */
.fx-spark {
  position: absolute; left: 0; top: 0; width: 0.4rem; height: 0.4rem; border-radius: 50%;
  animation: fxSpark 520ms cubic-bezier(.2, .7, .3, 1) both;
  will-change: transform, opacity;
}
@keyframes fxSpark {
  0%   { transform: translate(var(--x0), var(--y0)) translate(-50%, -50%) scale(.3); opacity: 0; }
  20%  { opacity: 1; transform: translate(var(--x0), var(--y0)) translate(-50%, -50%) scale(1); }
  100% { transform: translate(calc(var(--x0) + var(--dx)), calc(var(--y0) + var(--dy)))
                    translate(-50%, -50%) scale(.2); opacity: 0; }
}

/* --- papelitos -------------------------------------------------------------- */
.fx-bit {
  position: absolute; left: 0; top: 0; width: 0.6rem; height: 0.9rem; border-radius: 0.1rem;
  animation-name: fxBit; animation-timing-function: cubic-bezier(.2, .6, .3, 1);
  animation-fill-mode: both; will-change: transform, opacity;
}
@keyframes fxBit {
  0%   { transform: translate(var(--x0), var(--y0)) scale(.5); opacity: 0; }
  12%  { opacity: 1; }
  /* Cae más de lo que sube: sin esa asimetría el papelito flota y no pesa. */
  100% { transform: translate(calc(var(--x0) + var(--dx)), calc(var(--y0) + var(--dy) + 300px))
                    rotate(var(--rot)) scale(.85); opacity: 0; }
}

/* --- campeones -------------------------------------------------------------- */
.fx-champs {
  position: absolute; inset: 0; display: grid; align-content: center; justify-items: center;
  gap: var(--sp-6); padding: var(--sp-5);
  background: radial-gradient(80% 60% at 50% 45%, oklch(20% .08 262 / .82), oklch(10% .04 260 / .94));
  backdrop-filter: blur(4px);
  pointer-events: auto; cursor: pointer;      /* tocar y salir: nadie espera 4 s */
  animation: fxFade 320ms var(--ease) both;
}
@keyframes fxFade { from { opacity: 0; } }
.fx-champs__row { display: flex; gap: clamp(0.4rem, 2.4vw, 1.4rem); align-items: flex-end; justify-content: center; flex-wrap: nowrap; }
.fx-champ {
  margin: 0; display: grid; justify-items: center; gap: 0.4rem;
  width: clamp(4.4rem, 20vw, 10.8rem);
  /* Entran de a una, alternando de qué lado: cuatro llegando juntas se leen
     como una tira, no como cuatro que te vienen a felicitar. */
  animation: fxChamp 620ms var(--ease-bounce) both;
  animation-delay: calc(var(--i) * 170ms);
}
.fx-champ:nth-child(odd)  { --lado: -1; }
.fx-champ:nth-child(even) { --lado: 1; }
.fx-champ img {
  width: 100%; height: auto; display: block;
  filter: drop-shadow(0 10px 26px oklch(8% .04 260 / .8));
}
.fx-champ figcaption {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(9px, 2vw, 14px); letter-spacing: .04em; color: var(--fg);
}
@keyframes fxChamp {
  from { opacity: 0; transform: translate(calc(var(--lado, 1) * 120px), 40px) scale(.6) rotate(calc(var(--lado, 1) * 14deg)); }
  to   { opacity: 1; transform: none; }
}
.fx-champs__msg { display: grid; justify-items: center; gap: 0.4rem; text-align: center;
  animation: fxChamp 620ms var(--ease-bounce) 700ms both; --lado: 0; }
.fx-champs__msg b {
  font-family: var(--font-display); font-weight: 800; line-height: .95;
  font-size: clamp(30px, 8vw, 68px); color: var(--accent-2);
  text-shadow: 0 0 30px oklch(86% .19 132 / .45);
}
.fx-champs__msg span { color: var(--fg); opacity: .85; font-size: var(--step-0); max-width: 34ch; }

.fx-champs[data-quieto] .fx-champ,
.fx-champs[data-quieto] .fx-champs__msg { animation: none; opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .fx-champs { animation: none; }
}

/* --- lluvia por rareza -------------------------------------------------------
   Cae de arriba, no estalla desde un punto: la diferencia de FORMA es la que la
   distingue del estallido de la hoja completa. Dos cosas distintas se separan
   mejor por su gesto que por su tamaño.
   El dorado es el mismo de las monedas a propósito — en este álbum el oro
   significa valor, y la holo llueve el mismo metal que cobrás. */
.fx-drop {
  position: absolute; left: 0; top: 0; border-radius: 0.1rem;
  animation-name: fxDrop; animation-timing-function: cubic-bezier(.35, .1, .6, 1);
  animation-fill-mode: both; will-change: transform, opacity;
}
.fx-drop--rara {
  background: linear-gradient(150deg, oklch(92% .09 205), oklch(74% .15 210));
  box-shadow: 0 0 7px oklch(80% .15 205 / .5);
}
.fx-drop--holo {
  background: linear-gradient(150deg, oklch(96% .14 95), oklch(80% .17 88) 55%, oklch(64% .15 80));
  box-shadow: 0 0 10px oklch(88% .17 95 / .65);
}
@keyframes fxDrop {
  0%   { transform: translate(var(--x), -8vh) rotate(0deg); opacity: 0; }
  8%   { opacity: 1; }
  85%  { opacity: 1; }
  /* 112vh y no 100: tiene que salir de cuadro, no desvanecerse a mitad de la
     pantalla como si se hubiera arrepentido. */
  100% { transform: translate(calc(var(--x) + var(--drift)), 112vh) rotate(var(--rot)); opacity: 0; }
}

/* ══ Vitrina ══════════════════════════════════ ng-component: TrophyCaseComponent
   Una pieza en exhibición: base de metal, campana de vidrio y chapa grabada.
   Vive acá y no en favoritos.html porque la usan DOS lugares —el estante y el
   modal— y si el vidrio se define dos veces, un día dejan de ser el mismo vidrio.

   El metal sigue la RAREZA: acero para común, acero cian para rara, dorado para
   holo. Así el estante se lee de un vistazo y reusa el vocabulario que el álbum
   ya tiene, en vez de inventar una jerarquía nueva para la misma colección. */
.trofeo {
  --metal-a: oklch(78% .012 250);
  --metal-b: oklch(52% .014 250);
  --metal-c: oklch(38% .016 250);
  --vidrio:  oklch(86% .04 210);
  position: relative; display: grid; justify-items: center;
}
.trofeo[data-rar="rare"] {
  --metal-a: oklch(84% .06 205); --metal-b: oklch(58% .09 205); --metal-c: oklch(40% .07 205);
  --vidrio:  oklch(88% .07 205);
}
.trofeo[data-rar="holo"] {
  --metal-a: oklch(92% .11 92); --metal-b: oklch(70% .15 86); --metal-c: oklch(48% .12 80);
  --vidrio:  oklch(92% .08 95);
}

/* La campana. El vidrio se construye con tres capas: el tinte del cuerpo, un
   brillo especular en diagonal y el filo del borde. Con una sola no hay vidrio,
   hay un rectángulo translúcido. */
.trofeo__campana {
  position: relative; width: 100%; aspect-ratio: 3 / 3.4;
  border-radius: 0.9rem 0.9rem 0.2rem 0.2rem;
  background:
    linear-gradient(155deg, oklch(100% 0 0 / .16) 0%, oklch(100% 0 0 / .03) 38%, transparent 55%),
    linear-gradient(200deg, color-mix(in oklab, var(--vidrio) 16%, transparent), transparent 70%);
  border: 1px solid oklch(100% 0 0 / .22);
  border-bottom: 0;
  box-shadow:
    inset 0 1px 0 oklch(100% 0 0 / .5),          /* filo superior */
    inset -1px 0 0 oklch(100% 0 0 / .14),
    inset 1px 0 0 oklch(100% 0 0 / .14),
    0 10px 30px oklch(8% .04 260 / .5);
  backdrop-filter: blur(1.5px);
  display: grid; place-items: center; overflow: hidden;
}
/* El reflejo que barre el vidrio. Es lo que lo delata como vidrio y no como
   plástico: un plano de luz que cruza y no se mueve con el contenido. */
.trofeo__campana::after {
  content: ""; position: absolute; inset: -30% -10%;
  background: linear-gradient(74deg, transparent 40%, oklch(100% 0 0 / .18) 49%, transparent 57%);
  pointer-events: none;
}
.trofeo__campana img {
  width: 78%; height: 78%; object-fit: contain;
  filter: drop-shadow(0 6px 14px oklch(8% .04 260 / .6));
}

/* La base: tres bandas de metal cepillado. Una sola banda se lee plano; el
   contraste entre el filo claro de arriba y el zócalo oscuro es lo que le da
   volumen sin una sola sombra externa. */
.trofeo__base {
  width: 100%; border-radius: 0.2rem 0.2rem 0.5rem 0.5rem;
  padding: 0.4rem 0.6rem 0.7rem;
  /* El zócalo oscuro va al FONDO, no a media chapa. Con el corte al 62% el
     número quedaba partido justo sobre el borde claro→oscuro y se leía como
     recortado: media cifra en gris claro y media en gris oscuro. El zócalo es
     el canto de la base, no la mitad de la placa. */
  background:
    linear-gradient(180deg, var(--metal-a) 0 2px, var(--metal-b) 2px 86%, var(--metal-c) 86% 100%);
  box-shadow: 0 6px 16px oklch(8% .04 260 / .55), inset 0 1px 0 oklch(100% 0 0 / .55);
  text-align: center;
}
/* La chapa grabada: texto oscuro con un realce claro un pixel abajo. Es la
   ilusión de bajorrelieve más barata que existe y la única que funciona a
   este tamaño. */
.trofeo__chapa {
  display: block; font-family: var(--font-display); font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em;
  font-size: clamp(9px, 2.1cqi, 13px); line-height: 1.15;
  color: oklch(24% .02 250);
  text-shadow: 0 1px 0 oklch(100% 0 0 / .45);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trofeo__num {
  display: block; font-family: var(--font-mono); font-size: clamp(7px, 1.7cqi, 10px);
  color: oklch(30% .02 250); margin-top: 0.1rem;
  text-shadow: 0 1px 0 oklch(100% 0 0 / .35);   /* mismo grabado que la chapa */
}

/* La ficha de pieza: el objeto manda, el dato se corre al pie. */
.pieza-detalle { container-type: inline-size; display: grid; gap: var(--sp-5); justify-items: center; }
/* La campana también tiene techo de ALTURA: en un teléfono acostado se comía la
   pantalla entera y había que scrollear 370px para leer el nombre de la pieza. */
.pieza-detalle .trofeo { width: min(18.8rem, 82%); max-width: min(52dvh, 18.8rem); }
.pieza-detalle .trofeo__campana { aspect-ratio: 3 / 3.2; }
.pieza-detalle .trofeo__chapa { font-size: clamp(14px, 5cqi, 22px); }
.pieza-detalle .trofeo__num { font-size: clamp(10px, 3cqi, 13px); }
.pieza-detalle__tipos { display: flex; gap: var(--sp-2); flex-wrap: wrap; justify-content: center; }
.pieza-detalle .detail__desc { max-width: 44ch; text-align: center; }
/* La campana y la placa son DOS bloques, no cinco filas de la misma grilla: si
   fueran filas, la campana —que es alta— las estiraría y la pieza quedaría con
   tres agujeros de aire adentro. */
.pieza-detalle__info { display: grid; gap: var(--sp-4); justify-items: center; }
/* Con ancho, la pieza se abre en dos: campana a la izquierda, placa a la
   derecha. Como en la ficha, el que decide es el ancho del modal. */
@container (min-width: 620px) {
  .pieza-detalle { grid-template-columns: minmax(0, .85fr) minmax(0, 1fr);
                   column-gap: var(--sp-5); align-items: center; justify-items: stretch; }
  .pieza-detalle__info { justify-items: start; }
  .pieza-detalle .detail__desc { text-align: left; }
  .pieza-detalle__tipos { justify-content: flex-start; }
  .pieza-detalle__info .btn { width: 100%; }
}

/* ══ Dorso ═══════════════════════════════════════════ ng-component: CardBackComponent
   La cara de atrás de una figurita. Vive acá porque la usan la apertura de
   sobre y el memotest, y una carta dada vuelta tiene que ser LA MISMA carta
   en los dos lados de la app — si cada pantalla dibuja su propio dorso, un día
   dejan de ser el mismo mazo.

   No es un color liso ni una inicial: es el cartón de la tapa del álbum con el
   emblema de la serie. Eso es lo que lo vuelve referente — la carta boca abajo
   sigue diciendo de qué álbum es. */
.dorso {
  --sec-hue: 262;
  position: relative; display: grid; place-items: center; overflow: hidden;
  background:
    repeating-linear-gradient(58deg, oklch(100% 0 0 / .05) 0 2px, transparent 2px 9px),
    radial-gradient(120% 80% at 78% 0%, oklch(58% .17 var(--sec-hue) / .85), transparent 60%),
    linear-gradient(165deg, oklch(38% .15 var(--sec-hue)), oklch(24% .11 var(--sec-hue)));
  color: oklch(100% 0 0 / .55);
}
/* Filete interior: el troquel del dorso impreso. Sin él el cartón se lee como
   un fondo y no como el reverso de algo. */
.dorso::before {
  content: ""; position: absolute; inset: 7%;
  border: 1px solid oklch(100% 0 0 / .22); border-radius: 0.4rem;
}
.dorso__emblema { position: relative; width: 42%; max-width: 5.8rem; }
.dorso__emblema svg { width: 100%; display: block; }
.dorso__serie {
  position: absolute; bottom: 8%; left: 0; right: 0;
  font-family: var(--font-mono); font-size: clamp(7px, 2.4cqi, 11px);
  letter-spacing: .22em; text-transform: uppercase; text-align: center;
  color: oklch(100% 0 0 / .42);
}

/* ══ Chasquido ══════════════════════════════════════ ng-component: SnapOpenComponent
   El arranque de la función. Dos bloques entran —uno de arriba, otro de abajo—,
   se encuentran de golpe en el medio, y al abrirse dejan la primera figurita
   dando la vuelta.

   Toda la fuerza está en el ENCUENTRO: los bloques ACELERAN al final en vez de
   frenar, y en el impacto tiembla la pantalla entera. Si llegaran suave sería un
   telón cerrándose; lo que lo vuelve un chasquido es que lleguen de golpe y algo
   se sacuda.

   Y se abren MÁS LENTO de lo que se cerraron: cerrar es el golpe, abrir es
   mostrar. Al mismo ritmo las dos mitades del gesto se pisarían. */
.chasquido {
  position: absolute; inset: 0; z-index: 30; pointer-events: none;
  animation: chasGolpe 260ms 320ms var(--ease) both;
}
/* Los bloques son el CARTÓN DEL ÁLBUM, el mismo del dorso de las figuritas: dos
   tapas que se cierran sobre la pantalla. En un tono oscuro cualquiera no se
   leen como objetos —se confunden con el fondo y sólo se ve la juntura brillar,
   que es un efecto de luz, no dos cosas chocando—. Con el cartón se ve QUÉ se
   está cerrando. */
.chasquido__bloque {
  position: absolute; left: 0; right: 0; height: 50%;
  background:
    repeating-linear-gradient(58deg, oklch(100% 0 0 / .045) 0 2px, transparent 2px 9px),
    linear-gradient(var(--dir, 180deg), oklch(40% .15 262), oklch(22% .10 262));
  box-shadow: 0 0 46px oklch(8% .04 262 / .85);
}
/* Filo de luz sobre el canto: el grosor de la tapa. Sin él el bloque es una
   mancha plana y el choque no tiene contra qué pegar. */
.chasquido__bloque::before {
  content: ""; position: absolute; left: 0; right: 0; height: 0.1rem;
  background: oklch(100% 0 0 / .3);
}
.chasquido__bloque[data-lado="arriba"] {
  top: 0; --dir: 0deg;
  animation: chasCierraArriba 320ms cubic-bezier(.55, 0, .95, 1) both,
             chasAbreArriba 460ms 560ms cubic-bezier(.35, 0, .2, 1) forwards;
}
.chasquido__bloque[data-lado="arriba"]::before { bottom: 0; }
.chasquido__bloque[data-lado="abajo"] {
  bottom: 0; --dir: 180deg;
  animation: chasCierraAbajo 320ms cubic-bezier(.55, 0, .95, 1) both,
             chasAbreAbajo 460ms 560ms cubic-bezier(.35, 0, .2, 1) forwards;
}
.chasquido__bloque[data-lado="abajo"]::before { top: 0; }

@keyframes chasCierraArriba { from { transform: translateY(-100%); } }
@keyframes chasCierraAbajo  { from { transform: translateY(100%); } }
@keyframes chasAbreArriba   { to { transform: translateY(-100%); } }
@keyframes chasAbreAbajo    { to { transform: translateY(100%); } }
@keyframes chasGolpe {
  0%, 100% { transform: none; }
  18% { transform: translate(-5px, 4px) rotate(-.5deg); }
  44% { transform: translate(4px, -3px) rotate(.4deg); }
  72% { transform: translate(-2px, 1px); }
}
/* ── La chapa: qué se descubrió ────────────────────────────────────────────
   Entra cuando las tapas se separan (620ms de retraso, que es cuando el CSS de
   arriba las abre) y se va sola. Dice el nombre de lo que apareció.

   Va en los tokens del PAPEL (`--ink` / `--slot`) y no en los de superficie, por
   la misma razón que las pastillas de los números del tablero: se apoya sobre
   una lámina cualquiera y nadie puede saber si abajo hay cielo blanco o una
   camiseta oscura. Los tokens del papel no se dan vuelta con el tema, así que
   la chapa dice lo mismo en claro y en oscuro, sobre cualquier lámina. */
.chasquido__nombre {
  /* ARRIBA, no en el medio. Centrada tapaba la cara del jugador, que es
     justamente lo que se acaba de descubrir. En el borde superior la lámina
     tiene fondo —el motivo va centrado, es la regla de assets/niveles— y de
     paso queda lejos del cartel de monedas, que entra por abajo. */
  position: absolute; top: 7%; left: 50%; z-index: 31;
  width: max-content; max-width: min(86%, 38rem);
  display: grid; justify-items: center; gap: 0.2rem;
  padding: var(--sp-3) var(--sp-5);
  border-radius: var(--r-lg);
  background: color-mix(in oklab, var(--ink) 82%, transparent);
  box-shadow: var(--shadow-2);
  backdrop-filter: blur(6px);
  pointer-events: none; text-align: center; text-wrap: balance;
  animation: chasNombre 1.9s 620ms var(--ease) both;
}
.chasquido__nombre i {
  font-style: normal; font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: .18em; text-transform: uppercase;
  color: color-mix(in oklab, var(--slot) 70%, transparent);
}
.chasquido__nombre b {
  font-family: var(--font-display); font-weight: 800; line-height: 1.05;
  font-size: clamp(26px, 6vw, 58px);
  color: var(--slot);
}
/* En una caja chica —el cierre de la trivia es una lámina de 300px— el
   `clamp` de arriba, que se mide contra el VIEWPORT, da un nombre más ancho
   que la caja que lo contiene. `Fx.chasquido()` mide la caja y pone esta
   variante: mismo componente, dos tamaños, y ninguno depende de adivinar en
   qué pantalla va a estar. */
.chasquido__nombre--chico {
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-md); max-width: 92%;
}
.chasquido__nombre--chico i { font-size: clamp(9px, 2.6vw, 11px); letter-spacing: .14em; }
.chasquido__nombre--chico b { font-size: clamp(17px, 4.6vw, 24px); }

@keyframes chasNombre {
  0%   { opacity: 0; transform: translateX(-50%) scale(1.16); }
  11%  { opacity: 1; transform: translateX(-50%) scale(1); }
  78%  { opacity: 1; transform: translateX(-50%) scale(1); }
  100% { opacity: 0; transform: translateX(-50%) scale(1); }
}

/* Sin movimiento no hay chasquido que valga: la pantalla no se tapa, la figurita
   aparece y listo. El JS ni monta los bloques — si los montara, esta misma regla
   los dejaría tapando la pantalla para siempre.

   La CHAPA es la excepción y no es un descuido: el nombre es información, no
   decoración, así que se muestra igual. Lo que se le saca es la entrada. */
@media (prefers-reduced-motion: reduce) {
  .chasquido, .chasquido__bloque { animation: none; }
  .chasquido__nombre { animation: none; opacity: 1; transform: translateX(-50%); }
}

/* ── El glitch ─────────────────────────────── ng-component: GlitchDirective
   Lo usan DOS pantallas: la apertura del sobre y el revelado de la historia.
   Vivía dentro de `pack-opening.html` y de ahí no lo podía usar nadie más.

   Se aplica a la caja que se rompe, no a una capa encima: `.is-rota` filtra el
   elemento ENTERO, así que lo que se parte en canales es su contenido real. Una
   capa de barras de colores por arriba habría sido más barata y se ve como un
   cartel de neón puesto delante de una escena intacta.

   Las tres definiciones SVG (`#gl-1`, `#gl-2`, `#gl-3`) las inyecta
   `Kairo.filtrosGlitch()` una sola vez por documento: un filtro SVG no puede
   vivir en una hoja de estilos.

   `steps(1, end)` es la decisión central: NO interpola, salta de un filtro al
   otro. Con easing el mismo efecto se ve como un barrido líquido; a saltos se
   ve como cuadros perdidos. Y entre salto y salto hay cuadros SIN filtro, que
   es lo que lo hace leer como intermitencia y no como una textura encima.
   ============================================================== */
.is-rota { animation: escenaRota 300ms steps(1, end) both; will-change: filter; }
@keyframes escenaRota {
0% { filter: url(#gl-1) brightness(1.5) saturate(1.5);transform: translateX(-6px); }
22%{ filter: none;transform: none; }
40%{ filter: url(#gl-2) brightness(1.75) saturate(1.7); transform: translateX(10px); }
58%{ filter: none;transform: none; }
74%{ filter: url(#gl-3) brightness(1.35) saturate(1.35); transform: translateX(-4px); }
88%{ filter: none;transform: none; }
100% { filter: none;transform: none; }
}
/* Las líneas de barrido, en luz y no en sombra. Arrancaron en `multiply`, o
 sea oscureciendo, y sobre un fondo casi negro eso no se ve: oscurecer lo
 oscuro no hace nada. Van sumando luz. */
.rotura {
position: absolute; inset: 0; z-index: 12; pointer-events: none; opacity: 0;
background: repeating-linear-gradient(0deg,
oklch(88% .09 205 / .10) 0 2px, transparent 2px 6px);
mix-blend-mode: screen;
}
.is-rota .rotura { animation: roturaLineas 300ms steps(1, end) both; }
@keyframes roturaLineas {
0% { opacity: .9; } 22% { opacity: 0; } 40% { opacity: 1; } 58% { opacity: 0; }
74% { opacity: .7; } 88% { opacity: 0; } 100% { opacity: 0; }
}
/* Sin movimiento no hay rotura: la pantalla no parpadea ni se desgarra. */
@media (prefers-reduced-motion: reduce) {
.is-rota { animation: none; filter: none; }
.rotura, .is-rota .rotura { animation: none; opacity: 0; }
}
