/* ═══════════════════════════════════════════════════════════════════════════
   COSMERIUM ERA III · NCRT-3
   components.css — LENGUAJE DE COMPONENTES

   Implementa DESIGN.md §6 (6.0 → 6.14) y el subconjunto de §7.2 que pertenece
   a los componentes. Es la cuarta hoja de la cadena:

        tokens.css → reset.css → chrome.css → components.css → pages/*.css

   REGLA DE ORO DE ESTE FICHERO
   Aquí NO se define ni un solo token de DESIGN.md §5. Se consumen. Las únicas
   variables declaradas son de ámbito de componente (prefijadas con el nombre
   del componente: --btn-*, --fld-*, --reg-*, --seg…) y existen porque son
   medidas internas de una pieza, no decisiones de sistema. Es el mismo criterio
   con el que chrome.css declara --rail-h.

   TRES DECISIONES DE IMPLEMENTACIÓN QUE CONVIENE LEER ANTES DE TOCAR NADA
   ─────────────────────────────────────────────────────────────────────────
   1. `clip-path` recorta TAMBIÉN el `outline` de foco y todo lo que asome del
      elemento (pseudoelementos incluidos). Por eso, en cada componente
      chaflanado, el anillo de foco se dibuja sobre un antecesor SIN recortar,
      o se sustituye por un `box-shadow: inset`. Nunca se deja un interactivo
      recortado sin sustituto: DESIGN §11.2 lo prohíbe explícitamente.

   2. El borde recortado en la diagonal es una concesión ACEPTADA y validada en
      prototipo/ncrt3.html (`.seal--stamp` lleva `border` + `clip-path` y el
      resultado es el correcto: el trazo muere en el corte). La doble capa de
      §6.0(b) se reserva para donde el trazo tiene que sobrevivir al chaflán.

   3. `position: sticky` dentro de un contenedor con `overflow-x: auto` se ancla
      al contenedor, no al viewport, y si ese contenedor no tiene altura acotada
      la cabecera pegajosa no se pega a nada. Es el motivo por el que
      `.reg-scroll` es una ventana ACOTADA y no un simple envoltorio. Ver §6.5.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════════
   §6.0 · EL CHAFLÁN COMO SISTEMA

   Un solo corte por elemento. Tres implementaciones, cada una para su caso:
   (a) clip-path para paneles y tarjetas
   (b) doble capa para bordes que deben sobrevivir al corte
   (c) border-image con SVG para botones — la técnica real de cyberpunk.net
   ═══════════════════════════════════════════════════════════════════════════ */

/* (a) PANELES Y TARJETAS ─────────────────────────────────────────────────── */

.chf{
  --c: var(--chf-md);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
}

/* El corte espejado. En data-zona="calle" las tarjetas hermanas alternan .chf
   y .chf--mirror: los chaflanes engranan y la rejilla parece maquinada. */
.chf--mirror{
  --c: var(--chf-md);
  clip-path: polygon(var(--c) 0, 100% 0, 100% 100%,
                     var(--c) 100%, 0 calc(100% - var(--c)));
}

/* Corte inferior-derecha — el de los sellos y el retrato. */
.chf--br{
  --c: var(--chf-md);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--c)),
                     calc(100% - var(--c)) 100%, 0 100%);
}

/* Corte inferior-izquierda — el del popover del selector. */
.chf--bl{
  --c: var(--chf-md);
  clip-path: polygon(0 0, 100% 0, 100% 100%,
                     var(--c) 100%, 0 calc(100% - var(--c)));
}

.chf--xs{ --c: var(--chf-xs) }
.chf--sm{ --c: var(--chf-sm) }
.chf--md{ --c: var(--chf-md) }
.chf--lg{ --c: var(--chf-lg) }
.chf--xl{ --c: var(--chf-xl) }

/* PERFORACIÓN PERMANENTE (§10.6) — la esquina superior derecha del expediente
   devuelto queda troquelada para siempre. No es un estado: es una cicatriz.
   Se combina con el chaflán del contenedor añadiendo un segundo recorte con
   `mask`, porque `clip-path` sólo admite una figura por elemento. */
.is-punched{
  -webkit-mask-image:
    radial-gradient(circle 9px at calc(100% - 34px) 0, transparent 99%, #000 100%);
          mask-image:
    radial-gradient(circle 9px at calc(100% - 34px) 0, transparent 99%, #000 100%);
}

/* (b) BORDES CHAFLANADOS · DOBLE CAPA ─────────────────────────────────────
   `clip-path` mata `border`. Cuando el trazo TIENE que recorrer la diagonal
   entera (marcos de acento, marco del retrato) se pintan dos capas: la de
   fuera es el trazo, la de dentro la cara. El hijo reduce --c en 1.4px para
   que la diagonal interior quede paralela y no se coma el vértice. */
.chf-border{
  --c: var(--chf-md);
  background: var(--accent);
  padding: var(--bw-2);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
}
.chf-border > .face{
  background: var(--surface);
  clip-path: polygon(0 0, calc(100% - (var(--c) - 1.4px)) 0,
                     100% calc(var(--c) - 1.4px), 100% 100%, 0 100%);
  display: block;
  height: 100%;
}

/* (c) BOTONES · border-image con SVG ───────────────────────────────────────
   GEOMETRÍA OFICIAL (viewBox 232×48 en el original de cyberpunk.net):
   corte de 16,65 × 18,14 px — 42,5°, deliberadamente NO 45° — más una muesca
   de 12 × 2 px a media altura en el borde contrario. La muesca es el detalle
   que ninguna web fan copia y el que más autenticidad aporta.

        path original:  M232 0V29.8571L215.35 48H0V25H12V23H0V0H232Z

   POR QUÉ AQUÍ EL PATH NO ES ESE, SINO TRES:
   con `border-image-slice: 0 20 fill`, las rebanadas izquierda y derecha se
   estiran VERTICALMENTE hasta la altura del botón. Un SVG de 48 px de alto
   metido en un botón de 44 comprime el corte de 18,14 a 16,63 px — y el
   chaflán pasa a medir exactamente los 45° que la dirección de arte prohíbe.
   Por eso cada altura de botón lleva su path reescrito para que el corte
   RENDERIZADO mida 16,65 × 18,14 y la muesca 12 × 2 centrada:

        44 px (por defecto)   M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z
        54 px (barra de cmd)  M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z
        40 px (secundario)    M232 0V21.86L215.35 40H0V21H12V19H0V0H232Z

   Y POR QUÉ HAY UNA URI POR TEMA: un `data:` URI es un documento aparte, no
   hereda `currentColor` ni ve las custom properties. Como --accent cambia con
   `data-acceso` y con `data-zona`, la única forma honesta de que el botón
   primario siga al tema es tener la rebanada teñida de cada tema y conmutar
   la variable. Las variantes que NO son de acento (alert, deny, ghost,
   deshabilitado) tienen un color fijo por definición y sólo aparecen una vez.
   ═══════════════════════════════════════════════════════════════════════════ */

:root{
  /* Relleno sólido de acento — ROJO ARASAKA (nivel de acceso 0)
     EL RELLENO ES --k-red-brt (#FF3049) Y NO --k-red (#E6142D): la etiqueta
     del botón va en --accent-on (--k-void) a 14px/700, y 14px en negrita no
     es «texto grande» para WCAG (el umbral son 18,66px). Sobre #E6142D eso
     daba 4,32:1 medido; sobre #FF3049 da 5,54:1. Es el mismo criterio y el
     mismo par de rojos que --accent / --accent-fill en tokens.css: el trazo
     institucional no cambia, el relleno que lleva texto sí. */
  --btn-exec-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%23FF3049'/%3E%3C/svg%3E");
  --btn-exec-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M0 0V25.86L16.65 44H232V23H220V21H232V0H0Z' fill='%23FF3049'/%3E%3C/svg%3E");
  --btn-exec-cmd: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%23FF3049'/%3E%3C/svg%3E");
  --btn-exec-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M232 0V21.86L215.35 40H0V21H12V19H0V0H232Z' fill='%23FF3049'/%3E%3C/svg%3E");

  /* Trazo de acento sobre fondo casi opaco. NUNCA transparente puro: sobre
     una imagen el texto se pierde y el botón deja de leerse. */
  --btn-line-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231 1V25.45L214.9 43H1V24H13V20H1V1H231Z' fill='rgba(5,7,10,.6)' stroke='%23E6142D' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M1 1V25.45L17.1 43H231V24H219V20H231V1H1Z' fill='rgba(5,7,10,.6)' stroke='%23E6142D' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M231 1V21.45L214.9 39H1V22H13V18H1V1H231Z' fill='rgba(5,7,10,.6)' stroke='%23E6142D' stroke-width='2'/%3E%3C/svg%3E");

  /* Fijas: no dependen del nivel de acceso ni de la zona. */
  --btn-alert-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-alert-cmd: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-deny-img:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%238C0A1C'/%3E%3C/svg%3E");
  /* DEVOLVER CON DEFICIENCIAS es `.btn--cmd.btn--deny`: 54px. Sin rebanada
     propia, el SVG de 44 se estiraba a 54 y el corte pasaba de 16,65×18,14 a
     los 45° que la dirección de arte prohíbe, justo en el botón más grande de
     la pantalla del Verificador. */
  --btn-deny-cmd:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%238C0A1C'/%3E%3C/svg%3E");
  --btn-ghost-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231.5 .5V25.36L215.15 43.5H.5V23H12.5V21H.5V.5H231.5Z' fill='rgba(5,7,10,.55)' stroke='%232A343E' stroke-width='1'/%3E%3C/svg%3E");
  --btn-ghost-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M.5 .5V25.36L16.85 43.5H231.5V23H219.5V21H231.5V.5H.5Z' fill='rgba(5,7,10,.55)' stroke='%232A343E' stroke-width='1'/%3E%3C/svg%3E");
  --btn-ghost-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M231.5 .5V21.36L215.15 39.5H.5V21H12.5V19H.5V.5H231.5Z' fill='rgba(5,7,10,.55)' stroke='%232A343E' stroke-width='1'/%3E%3C/svg%3E");

  /* Deshabilitado: trazo propio en --text-off. NUNCA `opacity` — ensucia el
     trazo del SVG de forma imprevisible y convierte el chaflán en una mancha
     (DESIGN §6.1). El reset ya fija `:disabled{ opacity: 1 }` para eso. */
  --btn-off-img:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231.5 .5V25.36L215.15 43.5H.5V23H12.5V21H.5V.5H231.5Z' fill='rgba(5,7,10,.35)' stroke='%234A5058' stroke-width='1'/%3E%3C/svg%3E");
}

/* NIVEL DE ACCESO 1 y 4 — el sistema te acepta y se enfría: cian de operador */
:root[data-acceso="1"],
:root[data-acceso="4"]{
  --btn-exec-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%2300F0FF'/%3E%3C/svg%3E");
  --btn-exec-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M0 0V25.86L16.65 44H232V23H220V21H232V0H0Z' fill='%2300F0FF'/%3E%3C/svg%3E");
  --btn-exec-cmd: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%2300F0FF'/%3E%3C/svg%3E");
  --btn-exec-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M232 0V21.86L215.35 40H0V21H12V19H0V0H232Z' fill='%2300F0FF'/%3E%3C/svg%3E");
  --btn-line-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231 1V25.45L214.9 43H1V24H13V20H1V1H231Z' fill='rgba(5,7,10,.6)' stroke='%2300F0FF' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M1 1V25.45L17.1 43H231V24H219V20H231V1H1Z' fill='rgba(5,7,10,.6)' stroke='%2300F0FF' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M231 1V21.45L214.9 39H1V22H13V18H1V1H231Z' fill='rgba(5,7,10,.6)' stroke='%2300F0FF' stroke-width='2'/%3E%3C/svg%3E");
}

/* ZONA CALLE — el acento primario es SIEMPRE ámbar */
:root[data-zona="calle"]{
  --btn-exec-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-exec-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M0 0V25.86L16.65 44H232V23H220V21H232V0H0Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-exec-cmd: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-exec-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M232 0V21.86L215.35 40H0V21H12V19H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-line-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231 1V25.45L214.9 43H1V24H13V20H1V1H231Z' fill='rgba(5,7,10,.6)' stroke='%23FCEE0A' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M1 1V25.45L17.1 43H231V24H219V20H231V1H1Z' fill='rgba(5,7,10,.6)' stroke='%23FCEE0A' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M231 1V21.45L214.9 39H1V22H13V18H1V1H231Z' fill='rgba(5,7,10,.6)' stroke='%23FCEE0A' stroke-width='2'/%3E%3C/svg%3E");
}

/* COPIA IMPRESA — tinta de imprenta sobre papel: el relleno pierde el brillo
   de fósforo y el fondo del botón de trazo deja de ser casi negro. */
:root[data-medio="papel"]{
  --btn-exec-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%23B00F24'/%3E%3C/svg%3E");
  --btn-exec-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M0 0V25.86L16.65 44H232V23H220V21H232V0H0Z' fill='%23B00F24'/%3E%3C/svg%3E");
  --btn-exec-cmd: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%23B00F24'/%3E%3C/svg%3E");
  --btn-exec-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M232 0V21.86L215.35 40H0V21H12V19H0V0H232Z' fill='%23B00F24'/%3E%3C/svg%3E");

  /* LAS DOS REBANADAS QUE FALTABAN, Y SON LAS DEL BOTÓN QUE CIERRA EL TRÁMITE.
     `.btn--alert` es EXCLUSIVA de CURSAR EXPEDIENTE y DESCLASIFICAR (§6.1).
     Su tinta es --alert-ink, que en papel pasa a #FBF6C8, pero su relleno
     seguía siendo el data: URI con el amarillo de fósforo #FCEE0A cocido a
     fuego: 1,10:1 medido — un rectángulo amarillo sin texto. Aquí el relleno
     pasa al amarillo de imprenta --k-amber-2 (#F2E900) y la tinta la fija la
     regla `.btn--alert` de abajo en índigo, que es además el GESTO DE
     IMPRENTA que §3.4.5 exige: sobre amarillo, en papel, nunca negro. */
  --btn-alert-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%23F2E900'/%3E%3C/svg%3E");
  --btn-alert-cmd: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%23F2E900'/%3E%3C/svg%3E");

  --btn-line-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231 1V25.45L214.9 43H1V24H13V20H1V1H231Z' fill='rgba(255,255,255,.92)' stroke='%23B00F24' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M1 1V25.45L17.1 43H231V24H219V20H231V1H1Z' fill='rgba(255,255,255,.92)' stroke='%23B00F24' stroke-width='2'/%3E%3C/svg%3E");
  --btn-line-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M231 1V21.45L214.9 39H1V22H13V18H1V1H231Z' fill='rgba(255,255,255,.92)' stroke='%23B00F24' stroke-width='2'/%3E%3C/svg%3E");
  --btn-ghost-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231.5 .5V25.36L215.15 43.5H.5V23H12.5V21H.5V.5H231.5Z' fill='%23FFFFFF' stroke='%23A9A294' stroke-width='1'/%3E%3C/svg%3E");
  --btn-ghost-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M.5 .5V25.36L16.85 43.5H231.5V23H219.5V21H231.5V.5H.5Z' fill='%23FFFFFF' stroke='%23A9A294' stroke-width='1'/%3E%3C/svg%3E");
  --btn-ghost-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M231.5 .5V21.36L215.15 39.5H.5V21H12.5V19H.5V.5H231.5Z' fill='%23FFFFFF' stroke='%23A9A294' stroke-width='1'/%3E%3C/svg%3E");

  /* LA REBANADA DE DESHABILITADO, QUE TAMBIÉN FALTABA.
     `--btn-off-img` llevaba cocido `fill='rgba(5,7,10,.35)'`, pensado para
     componer sobre el carbón. Sobre el blanco del documento compone un gris
     #9F9E9A y la etiqueta a --text-3 quedaba en 2,1:1: un botón que dice qué
     NO puedes hacer y no se lee. En papel el relleno pasa al tono del propio
     papel y el trazo al gris de imprenta. */
  --btn-off-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231.5 .5V25.36L215.15 43.5H.5V23H12.5V21H.5V.5H231.5Z' fill='%23F2EFE8' stroke='%23A9A294' stroke-width='1'/%3E%3C/svg%3E");
}

/* Sobre el papel, --text-3 (#6A7078) da 4,33:1 contra #F2EFE8 y se queda a un
   pelo del suelo. La etiqueta del deshabilitado baja un escalón más de tinta
   —no de claridad— y sube a 5,52:1. El estado lo siguen comunicando el trazo
   apagado, el cursor y el aria-disabled, nunca la ilegibilidad. */
:root[data-medio="papel"] .btn:disabled,
:root[data-medio="papel"] .btn[aria-disabled="true"]{ color: #5A6069 }

/* GESTO DE IMPRENTA (§3.4.5): sobre relleno amarillo, EN PAPEL, la tinta es
   índigo. Nunca negro y nunca el crema de --alert-ink, que sobre #F2E900 da
   1,10:1. Índigo sobre el amarillo de imprenta da 11,17:1 medido, y es lo que
   distingue un impreso oficial de un PDF. Hasta ahora esto sólo estaba
   aplicado en zona calle; el botón que cierra el trámite lo necesita en las
   dos zonas. */
:root[data-medio="papel"] .btn--alert{ --btn-ink: var(--k-indigo) }

/* GESTO DE IMPRENTA: en papel y en la calle, tinta índigo sobre amarillo. */
:root[data-medio="papel"][data-zona="calle"]{
  --btn-exec-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M232 0V25.86L215.35 44H0V23H12V21H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-exec-rot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M0 0V25.86L16.65 44H232V23H220V21H232V0H0Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  /* Las alturas 54 y 40 también, o el primario de la barra de comandos y el
     secundario compacto salían del rojo del bloque de papel mientras su tinta
     ya era el índigo de la calle: índigo sobre #B00F24 es ilegible. Las cuatro
     rebanadas de --btn-exec-* tienen que virar juntas o ninguna. */
  --btn-exec-cmd: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='54' preserveAspectRatio='none'%3E%3Cpath d='M232 0V35.86L215.35 54H0V28H12V26H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-exec-sm:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='40' preserveAspectRatio='none'%3E%3Cpath d='M232 0V21.86L215.35 40H0V21H12V19H0V0H232Z' fill='%23FCEE0A'/%3E%3C/svg%3E");
  --btn-line-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='232' height='44' preserveAspectRatio='none'%3E%3Cpath d='M231 1V25.45L214.9 43H1V24H13V20H1V1H231Z' fill='%23FFFFFF' stroke='%231B2461' stroke-width='2'/%3E%3C/svg%3E");
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.1 · BOTÓN `.btn`

   CONTRATO DE MARCADO
   <button class="btn btn--exec" type="submit">
     <span class="btn__lbl">CURSAR EXPEDIENTE</span>
     <span class="btn__key" aria-hidden="true">[F7]</span>
   </button>

   `.btn__lbl` NO es opcional: es lo que el estado de carga oculta para poner
   el peine en su sitio. El código de operación va SIEMPRE en `.btn__key`, y
   el atajo tiene que funcionar de verdad — un `[F7]` decorativo es mentira,
   y la mentira es lo único que esta interfaz no se permite.

   REPARTO DE PSEUDOELEMENTOS (importa, porque no hay más):
     ::before  → peine de carga de 8 segmentos (`.is-loading`)
     ::after   → franja de peligro de `.btn--deny` · barrido `sweep` de exec
   Nunca coinciden: un botón de denegación no es nunca el primario que barre.
   ═══════════════════════════════════════════════════════════════════════════ */

.btn{
  --btn-img: var(--btn-ghost-img);
  --btn-rot: var(--btn-ghost-rot);
  --btn-ink: var(--text-2);

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);

  height: 44px;
  min-width: 200px;
  padding-inline: var(--sp-5);

  border-style: solid;
  border-width: 0 20px;
  border-image: var(--btn-img) 0 20 fill;
  background: transparent;
  color: var(--btn-ink);

  font-family: var(--ff-ui);
  font-weight: 600;
  font-size: var(--fs-sm);
  letter-spacing: var(--tr-wide);
  line-height: var(--lh-crush);
  text-transform: uppercase;
  text-align: center;
  cursor: pointer;

  transition:
    filter     var(--d-fast) var(--ez-term),
    box-shadow var(--d-fast) var(--ez-term);
}

.btn__lbl{ display: inline-block }

.btn__key{
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  font-weight: 400;
  letter-spacing: 0;
  opacity: .62;
  white-space: nowrap;
}

/* HOVER · 120 ms. Brillo y halo. NO se mueve. NO escala. Un botón de terminal
   no rebota: es un contacto que cierra, no un globo. */
.btn:hover{
  filter: brightness(1.10);
  box-shadow: var(--glow-accent);
}

/* ACTIVE · 80 ms. El único desplazamiento permitido, y es de 1 px. */
.btn:active{
  transform: translateY(1px);
  filter: brightness(.88);
  transition-duration: var(--d-instant);
}

/* El `outline` NO se recorta aquí porque el botón no lleva `clip-path`:
   el chaflán vive en el border-image. El anillo ámbar rodea la caja
   rectangular y eso es correcto — el foco tiene que ser inequívoco. */
.btn:focus-visible{
  outline: var(--bw-2) solid var(--focus);
  outline-offset: 2px;
}

/* ─── VARIANTES ─────────────────────────────────────────────────────────── */

/* Primaria. UNA POR PANTALLA. */
.btn--exec{
  --btn-img: var(--btn-exec-img);
  --btn-rot: var(--btn-exec-rot);
  --btn-ink: var(--accent-on);
  font-weight: 700;
}

/* Trazo de acento. La secundaria de verdad. */
.btn--line{
  --btn-img: var(--btn-line-img);
  --btn-rot: var(--btn-line-rot);
  --btn-ink: var(--accent-brt);
}

/* Terciaria, sin peso narrativo: abortar, volver, cerrar. */
.btn--ghost{
  --btn-img: var(--btn-ghost-img);
  --btn-rot: var(--btn-ghost-rot);
  --btn-ink: var(--text-2);
}

/* EXCLUSIVA de CURSAR EXPEDIENTE y DESCLASIFICAR. Ámbar, dos usos en toda la
   aplicación. Si aparece en un tercer sitio, el tercer sitio está mal. */
.btn--alert{
  --btn-img: var(--btn-alert-img);
  --btn-rot: var(--btn-alert-img);
  --btn-ink: var(--alert-ink);
  font-weight: 700;
}

/* DENEGAR. Relleno rojo profundo, TINTA HUESO y franja de peligro a 45° de
   3 px arriba. Es el único botón que se ve venir.

   LA TINTA NO ES --k-red-brt, Y ESO NO ES NEGOCIABLE: rojo brillante sobre
   --k-red-deep da 2,65:1 medido, y este es el botón de DESESTIMAR EXPEDIENTE
   y de DEVOLVER CON DEFICIENCIAS — las dos decisiones irreversibles del
   Oficial Verificador, y las dos ilegibles. --k-bone sobre #8C0A1C da 7,61:1,
   por encima del suelo de 6,2:1 de §11.1 y también en COPIA IMPRESA, donde el
   relleno es el mismo granate cocido en el data: URI.
   El gesto de §6.1 se conserva íntegro: el relleno sigue siendo rojo profundo
   y el rojo brillante sigue haciendo su trabajo en la franja de peligro del
   ::after de aquí abajo, que es donde de verdad se ve venir el botón. */
.btn--deny{
  --btn-img: var(--btn-deny-img);
  --btn-rot: var(--btn-deny-img);
  --btn-ink: var(--k-bone);
  font-weight: 700;
}
.btn--deny::after{
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: repeating-linear-gradient(45deg,
    var(--k-void) 0 8px,
    var(--k-red)  8px 16px);
  /* El recorte respeta la diagonal del chaflán: la franja no asoma en el aire. */
  clip-path: polygon(0 0, calc(100% - 3px) 0, 100% 3px, 100% 100%, 0 100%);
  pointer-events: none;
}

/* Espeja el chaflán. Dos botones adyacentes con y sin `--rot` engranan:
   el corte de uno abraza la muesca del otro. */
.btn--rot{ border-image-source: var(--btn-rot) }

/* ─── TAMAÑOS ───────────────────────────────────────────────────────────── */

/* Primario de la barra de comandos: 54 px, con su rebanada propia. */
.btn--cmd{
  height: 54px;
  min-width: 240px;
  font-size: var(--fs-md);
}
.btn--cmd.btn--exec { border-image-source: var(--btn-exec-cmd) }
.btn--cmd.btn--alert{ border-image-source: var(--btn-alert-cmd) }
.btn--cmd.btn--deny { border-image-source: var(--btn-deny-cmd) }

/* Secundario compacto: 40 px, con su rebanada propia. */
.btn--sm{
  height: 40px;
  min-width: 150px;
  font-size: var(--fs-xs);
  padding-inline: var(--sp-4);
}
.btn--sm.btn--ghost{ border-image-source: var(--btn-ghost-sm) }
.btn--sm.btn--line { border-image-source: var(--btn-line-sm) }
.btn--sm.btn--exec { border-image-source: var(--btn-exec-sm) }

.btn--block{ width: 100% }

/* ─── DESHABILITADO ─────────────────────────────────────────────────────── */

/* La etiqueta baja a --text-3 y NO a --text-off: un botón deshabilitado sigue
   diciendo QUÉ es lo que no se puede hacer, y eso es texto informativo, que
   el Anexo B.14.1.3 prohíbe en --text-off (2,39:1). Lo que comunica el estado
   es el trazo apagado del border-image, el cursor y el aria-disabled — nunca
   la ilegibilidad. */
.btn:disabled,
.btn[aria-disabled="true"]{
  border-image-source: var(--btn-off-img);
  color: var(--text-3);
  cursor: not-allowed;
  filter: none;
  box-shadow: none;
  transform: none;
}
.btn:disabled::after,
.btn[aria-disabled="true"]::after{ display: none }

/* ─── BARRIDO `sweep` DEL PRIMARIO ──────────────────────────────────────── */

.btn--exec::after,
.btn--alert::after{
  content: '';
  position: absolute;
  inset: 0 auto 0 0;   /* `inset:0` sobredefiniría la caja y anularía el ancho */
  background: linear-gradient(100deg,
    transparent 0,
    color-mix(in srgb, var(--k-bone) 34%, transparent) 50%,
    transparent 100%);
  width: 40%;
  transform: translateX(-160%);
  clip-path: polygon(0 0, calc(100% - 17px) 0, 100% 17px, 100% 100%, 0 100%);
  pointer-events: none;
  opacity: 0;
}
.btn--exec:hover::after,
.btn--alert:hover::after{
  opacity: 1;
  animation: sweep 220ms var(--ez-lin) both;
}

/* ─── CARGA · peine de 8 segmentos en bucle de 900 ms ───────────────────── */

.btn.is-loading{ cursor: progress; pointer-events: none }
.btn.is-loading .btn__lbl,
.btn.is-loading .btn__key{ visibility: hidden }

/* 8 segmentos de 6 px con 7 huecos de 2 px = 62 px exactos.
   La capa 1 es el relleno (se desplaza por `background-position`, que es una
   de las cuatro propiedades baratas de §8.1); la capa 2 es la vía apagada.
   La máscara talla los ocho dientes de una vez, y `steps(8)` hace que crezca
   diente a diente en lugar de deslizarse. */
.btn.is-loading::before{
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 62px; height: 10px;
  transform: translate(-50%, -50%);
  background-image:
    linear-gradient(90deg, currentColor 0 50%, transparent 50%),
    linear-gradient(color-mix(in srgb, currentColor 24%, transparent) 0 0);
  background-size: 200% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
  background-position: 100% 0, 0 0;
  -webkit-mask-image: repeating-linear-gradient(90deg, #000 0 6px, transparent 6px 8px);
          mask-image: repeating-linear-gradient(90deg, #000 0 6px, transparent 6px 8px);
  animation: comb-loop 900ms var(--ez-lin) infinite;
  animation-timing-function: steps(8, end);
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.2 · CAMPO `.field`

   Es UN HUECO EN EL FORMULARIO, no una caja flotante. Fondo hundido, sombra
   interior y un solo borde grueso abajo: la línea sobre la que se escribe.

   CONTRATO DE MARCADO
   <div class="field">
     <label class="field__lbl" for="handle">
       Handle<span class="field__req" aria-hidden="true">*</span>
       <span class="field__cnt">0/24</span>
     </label>
     <input id="handle" class="field__in" placeholder="p.ej. V, Johnny, Nadie">
     <p class="field__msg" id="handle-err">Ese handle ya está en el índice.</p>
   </div>

   EL ANILLO DE FOCO VA EN `.field`, NO EN EL INPUT: el input lleva `clip-path`
   para el chaflán y `clip-path` recorta el `outline` hasta hacerlo invisible.
   Rodear el grupo entero (etiqueta + hueco) además se lee mejor: lo que queda
   marcado es el campo del acta, no el rectángulo de texto.
   ═══════════════════════════════════════════════════════════════════════════ */

.field{
  --fld-line: var(--line-strong);   /* color del borde inferior */
  --fld-bw:   var(--bw-2);          /* grosor del borde inferior */
  position: relative;
  display: block;
}

.field:focus-within{
  outline: var(--bw-2) solid var(--focus);
  outline-offset: 2px;
  animation: focus-lock var(--d-fast) var(--ez-snap);
}

.field__lbl{
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
  font-family: var(--ff-ui);
  font-weight: 600;
  font-size: var(--fs-2xs);
  letter-spacing: var(--tr-wider);
  line-height: var(--lh-ui);
  text-transform: uppercase;
  color: var(--text-3);
}
.field__lbl::before{
  content: '▸';
  color: var(--accent);
  font-size: var(--fs-3xs);
}

/* El asterisco de obligatorio es ámbar, y sólo el asterisco. */
.field__req{ color: var(--alert); margin-left: 1px }

/* Contador / estado, siempre pegado a la derecha de la etiqueta: 0/240,
   VÁLIDO, REQUERIDO. Mono para que el número no baile al teclear. */
.field__cnt{
  margin-left: auto;
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-none);
  color: var(--text-3);
}

.field__in{
  --c: var(--chf-xs);
  display: block;
  width: 100%;
  height: 46px;
  padding-inline: var(--sp-3);
  background: var(--field);
  box-shadow: var(--sh-sunk);
  border: var(--bw-hair) solid var(--line);
  border-bottom: var(--fld-bw) solid var(--fld-line);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  color: var(--text);
  caret-color: var(--focus);
  font-family: var(--ff-mono);
  font-weight: 400;
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
  transition:
    border-color var(--d-fast) var(--ez-term),
    background   var(--d-fast) var(--ez-term);
}

/* El input ya está recortado: su `outline` no se vería. El foco lo dibuja
   `.field:focus-within`. Aquí sólo cambia la línea de escritura. */
.field__in:focus{ outline: none }

.field__in::placeholder{
  color: var(--text-off);
  font-style: italic;
  letter-spacing: var(--tr-none);
}

.field:focus-within{
  --fld-line: var(--focus);
  --fld-bw: var(--bw-3);
}

/* El resplandor va SÓLO bajo la línea, nunca alrededor de la caja: es una
   línea de tinta caliente, no un botón encendido. Sombra sin recorte porque
   el `clip-path` la mataría: se pinta sobre el grupo. */
.field:focus-within::before{
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 10px;
  background: radial-gradient(60% 100% at 50% 100%,
    color-mix(in srgb, var(--focus) 46%, transparent), transparent 72%);
  pointer-events: none;
}

/* CURSOR DE BLOQUE · 8 × 1,1em, parpadeo `caret` de 1000 ms `step-end`.
   Es una de las cuatro únicas animaciones permanentes de toda la aplicación
   (§7.1). Se ancla al extremo derecho del hueco y es ORNAMENTO: el cursor
   real de escritura es el nativo, teñido con `caret-color`. Ir a buscar la
   posición del cursor de texto desde CSS no se puede, y fingirlo con JS
   temblaría al escribir. Aquí no se finge nada. */
.field:focus-within::after{
  content: '';
  position: absolute;
  right: var(--sp-3);
  bottom: calc(var(--fld-bw) + 12px);
  width: 8px;
  height: 1.1em;
  background: var(--focus);
  animation: caret 1000ms step-end infinite;
  pointer-events: none;
}

/* ─── TEXTAREA · pauta de renglón ───────────────────────────────────────── */

.field__in--area,
.field--area .field__in{
  height: auto;
  min-height: 120px;
  padding-block: var(--sp-3);
  resize: vertical;
  line-height: 1.65;
  /* Escribir el trasfondo se siente como transcribir un acta. */
  background-image: repeating-linear-gradient(
    transparent 0 calc(1.65em - 1px),
    color-mix(in srgb, var(--accent) 6%, transparent) calc(1.65em - 1px) 1.65em);
  background-attachment: local;
}

/* En textarea el cursor de bloque estorba: la línea no termina a la derecha. */
.field--area:focus-within::after{ display: none }

/* ─── ESTADO VÁLIDO ─────────────────────────────────────────────────────── */

/* Micro-insignia cian a la derecha. Entra con `scramble` de 180 ms: el JS le
   pone `.is-scrambling` mientras resuelve los caracteres. */
.field__badge{
  display: none;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  padding-inline: 6px;
  height: 16px;
  border: var(--bw-hair) solid var(--system);
  background: color-mix(in srgb, var(--system) 10%, transparent);
  color: var(--system);
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wide);
  text-transform: uppercase;
}
.field.is-ok .field__badge{ display: inline-flex }
.field.is-ok .field__cnt{ display: none }
.field.is-ok{ --fld-line: var(--system) }

/* ─── ESTADO DE INCIDENCIA ──────────────────────────────────────────────── */

.field.is-bad{
  --fld-line: var(--danger);
  animation: error-nudge 160ms var(--ez-mech) 2;
}
/* EL FONDO DE INCIDENCIA NO SIGUE AL TEMA, y hay dos motivos separados.
   (1) `--accent-ink` vale cian en nivel 1 y ámbar en la calle: en cuanto el
   operador sella su expediente, un campo con incidencia se teñía del color de
   «estás dentro». §3.4.1 es explícito — el rojo es negativa, peligro, daño,
   deficiencia; un campo inválido es una deficiencia. Se usa --danger-ink, que
   es rojo en los tres niveles y en las dos zonas, y sólo cambia con el MEDIO,
   porque un granate sobre papel deja la escritura en negro sobre negro.
   (2) `background:` en forma abreviada borraba el `background-image` de pauta
   de renglón de `.field__in--area`: el textarea con incidencia perdía los
   renglones. Con `background-color:` la pauta sobrevive. */
.field.is-bad .field__in{ background-color: var(--danger-ink) }

/* El mensaje va en CAJA NORMAL. Nunca mayúsculas, nunca rojo puro a tamaño
   pequeño: el rojo --k-red da 4,3:1 y en microtexto es ilegible. Aquí se usa
   --danger, con prefijo `!!` para que el estado no dependa sólo del color
   (§11.2).
   --danger Y NO --k-red-brt A PELO: la negativa no sigue al tema (§3.4.1)
   pero sí sigue al MEDIO. Con la primitiva escrita a mano, en COPIA IMPRESA
   el #FF3049 de fósforo caía a 3,64:1 sobre el blanco del campo — el aviso
   que dice qué está mal, ilegible justo en el modo que se imprime y se
   archiva. --danger vale ese mismo rojo en terminal y el granate de imprenta
   en papel. */
.field__msg{
  display: none;
  margin-top: var(--sp-2);
  font-family: var(--ff-body);
  font-weight: 500;
  font-size: var(--fs-xs);
  line-height: var(--lh-body);
  text-transform: none;
  color: var(--danger);
}
.field.is-bad .field__msg{ display: block }
.field__msg::before{
  content: '!! ';
  font-family: var(--ff-mono);
  font-weight: 600;
}

/* Nota de ayuda neutra — no es una incidencia, no se pinta de rojo. */
.field__hint{
  margin-top: var(--sp-2);
  font-family: var(--ff-body);
  font-size: var(--fs-xs);
  line-height: var(--lh-body);
  text-transform: none;
  color: var(--text-3);
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.3 · SELECTOR `.picker`

   El `<select>` nativo está PROHIBIDO: no se puede pintar la opción, no se
   puede meter el identificador del mod a la derecha y en móvil abre una rueda
   del sistema operativo que rompe la ficción por completo.

   CONTRATO DE MARCADO
   <div class="picker">
     <button class="picker__btn" role="combobox" aria-expanded="false"
             aria-controls="rol-list" aria-haspopup="listbox">
       <span class="picker__val">SOLO</span>
     </button>
     <ul class="picker__pop" id="rol-list" role="listbox" hidden>
       <li class="picker__opt" role="option" style="--i:0">
         <span>Combate a distancia</span>
         <span class="picker__id">SkillDef 3110</span>
       </li>
     </ul>
   </div>

   Teclado obligatorio: ↑ ↓ Inicio Fin, escritura para saltar, Esc cierra y
   devuelve el foco al disparador.
   ═══════════════════════════════════════════════════════════════════════════ */

.picker{ position: relative }

.picker__btn{
  --c: var(--chf-xs);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  height: 46px;
  padding-inline: var(--sp-3);
  background: var(--field);
  box-shadow: var(--sh-sunk);
  border: var(--bw-hair) solid var(--line);
  border-bottom: var(--bw-2) solid var(--line-strong);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  color: var(--text);
  font-family: var(--ff-mono);
  font-size: var(--fs-md);
  text-align: left;
  cursor: pointer;
}
.picker__val{ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }

/* Caret ▼ de 8 px en acento, dibujado como triángulo: un glifo tipográfico
   cambia de forma según la fuente instalada y aquí nada puede depender de eso. */
.picker__btn::after{
  content: '';
  width: 8px; height: 5px;
  background: var(--accent);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  flex: none;
  transition: transform var(--d-fast) var(--ez-term);
}
.picker__btn[aria-expanded="true"]::after{ transform: scaleY(-1) }

/* El disparador está recortado: el anillo va en el contenedor. */
.picker__btn:focus-visible{ outline: none }
.picker:focus-within{
  outline: var(--bw-2) solid var(--focus);
  outline-offset: 2px;
  animation: focus-lock var(--d-fast) var(--ez-snap);
}

.picker__pop{
  --c: 14px;
  position: absolute;
  z-index: var(--z-dropdown);
  top: calc(100% + 2px);
  left: 0; right: 0;
  max-height: 320px;
  overflow-y: auto;
  margin: 0; padding: 0;
  list-style: none;
  background: var(--surface-2);
  border: var(--bw-hair) solid var(--line-strong);
  box-shadow: var(--sh-lift);
  /* Chaflán inferior-izquierda: el popover cuelga en sentido contrario al
     disparador y los dos cortes se miran. */
  clip-path: polygon(0 0, 100% 0, 100% 100%,
                     var(--c) 100%, 0 calc(100% - var(--c)));
  /* El origen es el borde superior: el popover cuelga del disparador y tiene
     que desplegarse desde él, no crecer desde su centro. */
  transform-origin: top center;
  animation: picker-open 160ms var(--ez-term) both;
}

/* LA APERTURA NO PUEDE ANIMAR `clip-path`, Y ESTE ES EL MOTIVO.
   El chaflán inferior-izquierda de §6.3 vive en el `clip-path` de arriba. Un
   valor ANIMADO de una propiedad siempre gana a su declaración base, y
   `fill-mode: both` extiende ese dominio antes y después de la animación,
   para siempre: con el keyframe en `inset(...)`, `getComputedStyle(pop)
   .clipPath` devolvía `inset(...)` y nunca el polígono. El chaflán de 14px
   no se había visto nunca.
   `transform` no colisiona con `clip-path`, sigue en la lista blanca de §8.1
   y es más barato: se compone, no se recorta cada fotograma. */
@keyframes picker-open{
  from{ transform: scaleY(0); opacity: 0 }
  to  { transform: scaleY(1); opacity: 1 }
}

.picker__opt{
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 38px;
  padding-inline: var(--sp-4);
  border-left: 3px solid transparent;
  color: var(--text-2);
  font-family: var(--ff-ui);
  font-weight: 500;
  font-size: var(--fs-sm);
  cursor: pointer;
  animation: row-stagger 180ms var(--ez-term) both;
  animation-delay: calc(var(--i, 0) * 18ms);
}

/* EL IDENTIFICADOR REAL DEL MOD, A LA DERECHA DE CADA OPCIÓN.
   `PerkDef 2913`, `SkillDef 3110`, `item.99299659`. Que el jugador vea el
   identificador con el que el servidor va a resolver su tirada es inmersión
   gratis, y sobre todo ES VERDAD.
   VA EN --text-3, EL SUELO LEGAL, y no en --text-off (1,97:1 medido): si el
   identificador es «la inmersión que da que los datos sean ciertos» (§12), no
   puede ser el elemento menos legible de la lista. La jerarquía frente al
   nombre de la opción la sostienen el cuerpo mono, el tamaño y el
   `letter-spacing: 0` — no la ceguera. */
.picker__id{
  margin-left: auto;
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-none);
  color: var(--text-3);
  white-space: nowrap;
}

.picker__opt:hover,
.picker__opt[aria-selected="true"],
.picker__opt.is-active{
  background: var(--accent-ink);
  border-left-color: var(--accent);
  color: var(--text);
}
.picker__opt[aria-selected="true"] .picker__id,
.picker__opt:hover .picker__id{ color: var(--text-2) }

/* Cabecera de grupo dentro del listbox — familias de habilidad, categorías
   del Mercado Nocturno. */
.picker__grp{
  padding: var(--sp-2) var(--sp-4);
  background: var(--surface);
  color: var(--text-3);
  font-family: var(--ff-ui);
  font-weight: 700;
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-widest);
  text-transform: uppercase;
  position: sticky;
  top: 0;
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.4 · TARJETA `.dossier`

   CONTRATO DE MARCADO
   <article class="dossier">
     <i class="dossier__peel" aria-hidden="true"></i>   <!-- sólo zona calle -->
     <header class="dossier__h">
       <span>/// .SkillDef.2902</span><span>ACTIVO_</span>
     </header>
     <div class="dossier__b"> … </div>
     <footer class="dossier__f">NC-RG-2077-0447-K │ SECCIÓN III • REV. 3</footer>
   </article>

   POR QUÉ LA SUPERFICIE ES UN `::before` Y NO EL FONDO DEL ARTÍCULO:
   la ménsula cuelga POR DEBAJO de la tarjeta, y si el chaflán viviera en el
   propio `.dossier` el `clip-path` se la comería junto con el anillo de foco.
   Se aísla la superficie recortada en una capa detrás del contenido —el mismo
   hallazgo que chrome.css documenta para la máscara de la rejilla— y el
   artículo queda libre para que le cuelguen cosas.
   ═══════════════════════════════════════════════════════════════════════════ */

.dossier{
  --c: var(--chf-md);
  position: relative;
  display: flex;
  flex-direction: column;
  margin-bottom: 22px;          /* sitio para la ménsula */
  background: transparent;
  color: var(--text-2);
}

.dossier::before{
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--surface);
  border: var(--bw-hair) solid var(--line);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  transition: border-color var(--d-base) var(--ez-term);
  pointer-events: none;
}

.dossier > *{ position: relative; z-index: 1 }

/* LA MÉNSULA. Una línea de 24 × 20 px colgando bajo el borde inferior
   izquierdo. Una sola línea, y firma la tarjeta. */
.dossier::after{
  content: '';
  position: absolute;
  top: 100%; left: 0;
  width: 24px; height: 20px;
  border-bottom: var(--bw-2) solid var(--text-2);
  pointer-events: none;
}

/* CABECERA de 28 px. A la izquierda el slug diegético GENERADO DE DATOS
   REALES; a la derecha el estado con guion bajo final, como un campo de
   formulario esperando a ser rellenado. */
.dossier__h{
  --c: var(--chf-md);
  position: relative;   /* ancla de la regla de hover */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  height: 28px;
  padding-inline: var(--sp-4);
  background: var(--surface-2);
  border-bottom: var(--bw-hair) solid var(--line);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-widest);
  text-transform: uppercase;
  color: var(--system);
  white-space: nowrap;
  overflow: hidden;
}
.dossier__h > span:last-child{ color: var(--text-3); letter-spacing: var(--tr-wider) }

/* La regla de acento que crece de 0 a 100% desde la izquierda al pasar por
   encima. `scaleX` sobre un elemento de 2 px: no hay reflujo, no hay salto,
   no hay elevación. Una tarjeta que se levanta al pasar el ratón es de otra
   dirección de arte. */
.dossier__h::before{
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: var(--bw-2);
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--d-base) var(--ez-term);
}
.dossier:hover .dossier__h::before,
.dossier:focus-within .dossier__h::before{ transform: scaleX(1) }
.dossier:hover::before,
.dossier:focus-within::before{ border-color: var(--accent) }

.dossier__b{
  padding: var(--sp-5);
  font-family: var(--ff-body);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  text-transform: none;
}

.dossier__f{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  min-height: 24px;
  padding: 0 var(--sp-5) var(--sp-3);
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wide);
  /* Número de expediente, sección y revisión: pie de documento, pero dato.
     En --text-off (2,31:1 sobre --surface) era ilegible; --text-3 lo deja en
     el suelo legal sin que compite con el cuerpo de la tarjeta. */
  color: var(--text-3);
}
/* Los metadatos se unen con ` │ ` y ` • `, nunca con comas: es un pie de
   documento, no una frase. */
/* Separador puramente visual: `/ ''` para que el lector de pantalla no cante
   «línea vertical» entre cada par de datos del pie de la tarjeta. */
.dossier__f > * + *::before{ content: '│' / ''; margin-right: var(--sp-2); color: var(--line-strong) }

.dossier__t{
  font-family: var(--ff-display);
  font-weight: 700;
  font-size: var(--fs-xl);
  letter-spacing: var(--tr-snug);
  line-height: var(--lh-display);
  color: var(--text);
  margin-bottom: var(--sp-3);
}

/* VARIANTE DE ZONA CALLE — el sticker mal pegado. La copia fantasma en acento
   asoma desplazada y ligeramente torcida al pasar por encima. */
.dossier__peel{
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--accent);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  transform: translate(6px, -4px) skewX(-1deg);
  opacity: 0;
  transition: opacity var(--d-base) var(--ez-term);
  pointer-events: none;
}
.dossier--peel:hover .dossier__peel,
.dossier--peel:focus-within .dossier__peel{ opacity: .10 }

/* Entrada del panel: se materializa desde la banda central. */
.dossier.is-entering{ animation: panel-commit var(--d-slow) var(--ez-term) both }


/* ═══════════════════════════════════════════════════════════════════════════
   §6.5 · TABLA `.reg-table`

   EL COMPONENTE MÁS IMPORTANTE. Aquí viven los 65 SkillDef, los 744 objetos
   del `TESTDATATABLE` y las 22 heridas críticas.

   CONTRATO DE MARCADO
   <div class="reg-scroll">
     <table class="reg-table">
       <thead>
         <tr class="reg-formula">
           <th>Habilidad</th>
           <th class="num">CARACT.<i class="op">+</i>NIVEL<i class="op">+</i>MOD<i class="op">=</i>BASE</th>
         </tr>
       </thead>
       <tbody>
         <tr class="reg-group"><th colspan="2">
           <span class="chip-stat">5</span> REFLEJOS
         </th></tr>
         <tr style="--i:0"> … </tr>
       </tbody>
     </table>
   </div>

   LA CABECERA PEGAJOSA Y EL DESBORDAMIENTO HORIZONTAL SON EL MISMO PROBLEMA
   Un envoltorio con `overflow-x: auto` calcula `overflow-y` como `auto`: se
   convierte en contenedor de scroll, y a partir de ahí `position: sticky` se
   ancla a ÉL y no al viewport. Si además el envoltorio no tiene altura
   acotada, nunca hay scroll vertical dentro y la cabecera no se pega a nada:
   queda decorativa. Por eso `.reg-scroll` es una VENTANA ACOTADA que scrollea
   en los dos ejes, con la cabecera pegada a su propio borde superior — que
   está justo debajo del HUD. Para tablas cortas, `.reg-scroll--flow` quita el
   tope y renuncia a la cabecera pegajosa, que ahí no hace falta.
   ═══════════════════════════════════════════════════════════════════════════ */

.reg-scroll{
  --reg-fade: 16px;
  max-height: calc(100dvh
    - var(--bar-top) - var(--bar-bottom)
    - var(--header-h) - var(--command-h) - var(--sp-8));
  overflow: auto;
  overscroll-behavior: contain;
  /* Máscara de desvanecido en el borde de fuga. El <body> NO hace scroll
     horizontal jamás: el desbordamiento vive aquí dentro y se anuncia. */
  -webkit-mask-image: linear-gradient(90deg,
    #000 0, #000 calc(100% - var(--reg-fade)), transparent 100%);
          mask-image: linear-gradient(90deg,
    #000 0, #000 calc(100% - var(--reg-fade)), transparent 100%);
}
/* El JS marca `.is-scrolled` en cuanto scrollLeft > 0 y aparece también el
   desvanecido de entrada. Sin esa clase la primera columna se lee entera. */
.reg-scroll.is-scrolled{
  -webkit-mask-image: linear-gradient(90deg,
    transparent 0, #000 var(--reg-fade),
    #000 calc(100% - var(--reg-fade)), transparent 100%);
          mask-image: linear-gradient(90deg,
    transparent 0, #000 var(--reg-fade),
    #000 calc(100% - var(--reg-fade)), transparent 100%);
}
.reg-scroll--flow{ max-height: none; overflow: visible auto }

.reg-table{
  width: 100%;
  border-collapse: collapse;
  font-family: var(--ff-ui);
  font-size: var(--fs-sm);
  line-height: var(--lh-data);
  color: var(--text-2);
}

/* CABECERA · relleno SÓLIDO de acento, tinta --accent-on, 32 px, pegajosa.
   Nada de cabeceras grises: la tabla es el registro y el registro lo firma
   el sistema.
   EL RELLENO ES --accent-fill Y NO --accent: es el mismo rojo institucional
   en nivel 1 y en la calle, pero en NIVEL 0 sube de #E6142D a #FF3049 porque
   estos 13px/700 en knockout daban 4,32:1 sobre el rojo oscuro — y la
   pantalla que peor se leía era exactamente la del operador nuevo. Ver la
   nota de --accent-fill en tokens.css. */
.reg-table thead th{
  position: sticky;
  top: 0;
  z-index: var(--z-raised);
  height: 32px;
  padding: 0 var(--sp-3);
  background: var(--accent-fill);
  color: var(--accent-on);
  font-family: var(--ff-ui);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--tr-wider);
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}

/* CABECERA-FÓRMULA · los encabezados son la aritmética literal del sistema,
   con los operadores como glifos reales apagados. La cabecera ENSEÑA la
   aritmética: `CARACT. + NIVEL + MOD = BASE`. */
.reg-formula .op{
  display: inline-block;
  margin-inline: var(--sp-2);
  font-family: var(--ff-mono);
  font-style: normal;
  font-weight: 400;
  color: color-mix(in srgb, var(--accent-on) 55%, transparent);
}

/* BARRA DE GRUPO · cuando la tabla agrupa por característica, el valor va en
   un chip chaflanado de hueso con tinta oscura al principio de la barra:
   `[5] REFLEJOS`. El jugador nunca tiene que buscar la stat en otro sitio. */
.reg-group th{
  position: sticky;
  top: 32px;
  z-index: var(--z-content);
  height: 30px;
  padding: 0 var(--sp-3);
  background: var(--surface-2);
  border-top: var(--bw-hair) solid var(--line-strong);
  border-bottom: var(--bw-hair) solid var(--line);
  color: var(--text);
  font-family: var(--ff-ui);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--tr-wider);
  text-transform: uppercase;
  text-align: left;
}

.chip-stat{
  --c: 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 20px;
  margin-right: var(--sp-2);
  padding-inline: 6px;
  background: var(--k-bone);
  color: var(--k-void);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  font-family: var(--ff-display);
  font-weight: 700;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}

/* FILAS de 36 px, cebra, divisor SÓLO horizontal. Una tabla con rejilla
   completa es una hoja de cálculo; esto es un registro. */
.reg-table tbody tr{ height: 36px }
.reg-table tbody td{
  padding: 0 var(--sp-3);
  border-bottom: var(--bw-hair) solid var(--line);
  /* El borde izquierdo del hover YA OCUPA SITIO en transparente: cuando
     aparece, la fila no salta ni un píxel. */
  border-left: 3px solid transparent;
}
.reg-table tbody td + td{ border-left: 0 }
.reg-table tbody tr:nth-child(odd)  > td{ background: var(--surface) }
.reg-table tbody tr:nth-child(even) > td{ background: var(--surface-2) }

.reg-table tbody tr:hover > td{
  background: var(--accent-ink);
  color: var(--text);
  transition: background 100ms var(--ez-term);
}
.reg-table tbody tr:hover > td:first-child{ border-left-color: var(--accent) }

.reg-table tbody tr:focus-within > td{ background: var(--accent-ink) }

/* Columna numérica: mono, a la derecha, cifras tabulares. Los números de un
   expediente no bailan. */
.reg-table .num,
.reg-table td.num,
.reg-table th.num{
  font-family: var(--ff-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
.reg-table td.num{ color: var(--text) }
/* La columna de resultado — la que el jugador mira — va en peso y en hueso. */
.reg-table td.num--base{
  font-weight: 600;
  font-size: var(--fs-md);
  color: var(--text);
}

/* ENTRADA ESCALONADA · máx. 12 filas. A partir de la 13 no hay animación:
   744 objetos entrando en cascada es una tarta, no una interfaz. */
.reg-table tbody tr.is-entering{
  animation: row-stagger 180ms var(--ez-term) both;
  animation-delay: calc(var(--i, 0) * 22ms);
}
.reg-table tbody tr.is-entering:nth-child(n+13){ animation: none }

/* TEXTO FANTASMA en filas vacías. Un expediente a medias sigue pareciendo un
   formulario impreso, no una tabla rota. La cadena se repite en el propio
   `content` porque CSS no sabe repetir texto como fondo.

   Y SE REPITE CINCO VECES, QUE ES EXACTAMENTE EL PROBLEMA: NVDA y VoiceOver
   leen el contenido generado, así que cada celda vacía de un catálogo de 744
   filas se anunciaba «personalizado personalizado personalizado personalizado
   personalizado». La barra `/ ''` es el TEXTO ALTERNATIVO de `content`
   (CSS Generated Content 3, en Chrome desde la 77, Safari 17.4 y Firefox
   122): la trama sigue viéndose y el árbol de accesibilidad recibe la cadena
   vacía, que es lo que una trama debe aportar. En un navegador que no
   entienda la sintaxis la declaración entera es inválida y la celda queda
   limpia — se pierde el adorno, nunca el dato.
   `aria-hidden` no vale aquí: no se puede poner sobre un pseudoelemento, y
   ponerlo en el `<td>` escondería también el valor real cuando lo haya. */
.reg-table td.is-empty{ position: relative; color: transparent }
.reg-table td.is-empty::after{
  content: 'PERSONALIZADO · PERSONALIZADO · PERSONALIZADO · PERSONALIZADO · PERSONALIZADO' / '';
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  padding-inline: var(--sp-3);
  opacity: .14;
  color: var(--text);
  font-family: var(--ff-ui);
  font-weight: 600;
  font-size: var(--fs-xs);
  letter-spacing: var(--tr-wider);
  white-space: nowrap;
  overflow: hidden;
  pointer-events: none;
}

/* Sin resultados — voz del sistema, con el término del glosario. */
.reg-table .reg-void td{
  height: 96px;
  text-align: center;
  color: var(--text-3);
  font-family: var(--ff-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tr-wider);
  text-transform: uppercase;
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.6 · PANEL INYECTADO `.inject` (modal)

   No es un diálogo: es un panel INYECTADO en la sesión. El copy nunca dice
   "ventana" ni "diálogo".

   CONTRATO DE MARCADO
   <div class="inject" role="dialog" aria-modal="true" aria-labelledby="ti">
     <div class="inject__scrim" data-close></div>
     <div class="inject__box">
       <i class="cbr cbr--tl"></i><i class="cbr cbr--tr"></i>
       <i class="cbr cbr--bl"></i><i class="cbr cbr--br"></i>
       <div class="inject__bar">
         <span id="ti">CANJE DE AVAL</span>
         <button class="inject__x" aria-label="Abortar"><i></i><i></i></button>
         <span class="inject__esc" aria-hidden="true">[ESC]</span>
       </div>
       <div class="inject__body"> … </div>
     </div>
   </div>

   Foco atrapado (`inert` en el resto del documento), Esc cierra, y el foco
   vuelve AL DISPARADOR. Sin eso el panel no está terminado.
   ═══════════════════════════════════════════════════════════════════════════ */

.inject{
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}

.inject__scrim{
  position: absolute;
  inset: 0;
  z-index: var(--z-scrim);
  background: rgba(5,7,10,.86);
  -webkit-backdrop-filter: blur(3px) saturate(.6);
          backdrop-filter: blur(3px) saturate(.6);
  animation: scrim-in 160ms var(--ez-term) both;
}
@keyframes scrim-in{ from{ opacity: 0 } to{ opacity: 1 } }

.inject__box{
  --c: var(--chf-xl);
  position: relative;
  z-index: calc(var(--z-modal) + 1);
  width: min(640px, 92vw);
  max-height: 86dvh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: var(--bw-2) solid var(--accent);
  box-shadow: var(--sh-modal);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  animation: panel-commit var(--d-slow) var(--ez-term) both;
}

/* CUATRO ESCUADRAS DE HUD de 44 × 44 px. Es el patrón `.cbr` que ya existe en
   home.html, conservado y reafinado: allí eran de 60 px al 18% sobre el
   lienzo; aquí marcan el panel. */
.cbr{
  position: absolute;
  width: 44px; height: 44px;
  opacity: .5;
  pointer-events: none;
}
.cbr--tl{ top: 6px; left: 6px;  border-top: var(--bw-hair) solid var(--accent); border-left:  var(--bw-hair) solid var(--accent) }
.cbr--tr{ top: 6px; right: 6px; border-top: var(--bw-hair) solid var(--accent); border-right: var(--bw-hair) solid var(--accent) }
.cbr--bl{ bottom: 6px; left: 6px;  border-bottom: var(--bw-hair) solid var(--accent); border-left:  var(--bw-hair) solid var(--accent) }
.cbr--br{ bottom: 6px; right: 6px; border-bottom: var(--bw-hair) solid var(--accent); border-right: var(--bw-hair) solid var(--accent) }

/* Franja superior de 26 px, relleno --accent-fill (ver tokens.css: en nivel 0
   el relleno sube a #FF3049 para que estos 13px/700 en knockout dejen de dar
   4,32:1), tinta oscura.
   `position: relative; z-index: 1` NO es cosmético: `.inject__body` es un
   hermano posterior y, sin contexto propio, pintaba por encima de todo lo que
   asomara de la franja — incluida el área de pulsación del único cierre del
   panel. */
.inject__bar{
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: 26px;
  padding-inline: var(--sp-4);
  background: var(--accent-fill);
  color: var(--accent-on);
  font-family: var(--ff-ui);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--tr-widest);
  text-transform: uppercase;
  flex: none;
}
.inject__esc{
  font-family: var(--ff-mono);
  font-weight: 400;
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-none);
  opacity: .72;
}

/* CIERRE · nunca un glifo. Dos barras rotadas ±45°. Una «×» tipográfica cambia
   de forma con cada fuente y parece un carácter perdido; esto parece un
   mecanismo.

   GEOMETRÍA: §6.6 pide a la vez una franja de 26px y un aspa dentro de un span
   de 37×50, y las dos cosas NO caben. Medido: el botón sobresalía 12px por
   arriba y 12 por abajo de la franja; los de arriba se los comía el clip-path
   de `.inject__box` y los de abajo quedaban tapados por `.inject__body`, así
   que casi la mitad del área de clic del único cierre del panel no respondía.
   Y el aspa de 34px rotada 45° mide 27,6px de alto real: tampoco cabía en 26.
   Gana la franja, que es medida de composición, y el aspa se reescala.

   EL ÁREA DE PULSACIÓN SE RECUPERA HACIA ABAJO con un ::after transparente:
   26 + 18 = 44px de alto y 45 de ancho, el objetivo táctil de §11.4, sin
   engordar la franja ni un píxel. Hacia arriba no serviría —el clip-path del
   panel recorta también los eventos— y hacia abajo cae dentro de los 24px de
   relleno de `.inject__body`, donde no hay nada que pulsar. */
.inject__x{
  position: relative;
  width: 37px; height: 26px;
  margin-left: auto;
  background: transparent;
  border: 0;
  cursor: pointer;
  flex: none;
}
.inject__x::after{
  content: '';
  position: absolute;
  inset: 0 -4px -18px -4px;
}
.inject__x i{
  position: absolute;
  top: 50%; left: 50%;
  width: 4px; height: 24px;
  background: var(--accent-on);
  transform-origin: center;
}
.inject__x i:nth-child(1){ transform: translate(-50%,-50%) rotate( 45deg) }
.inject__x i:nth-child(2){ transform: translate(-50%,-50%) rotate(-45deg) }
/* El hover NO puede ser --k-void: --accent-on ya vale --k-void en tres de los
   cuatro temas y el gesto era un no-op. Rebajar la propia tinta al 70% se ve
   en los cuatro, y sobre relleno de acento sigue leyéndose como aspa. */
.inject__x:hover i{ background: color-mix(in srgb, var(--accent-on) 70%, transparent) }
.inject__x:focus-visible{ outline: var(--bw-2) solid var(--focus); outline-offset: -2px }

.inject__body{
  padding: var(--sp-5);
  overflow-y: auto;
  font-family: var(--ff-body);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  color: var(--text-2);
  text-transform: none;
}

.inject__foot{
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  justify-content: flex-end;
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  border-top: var(--bw-hair) solid var(--line);
  flex: none;
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.7 · PESTAÑAS `.slots`

   `role="tablist"`, flechas, `aria-selected`. Y al cambiar de pestaña,
   `scrollTop = 0` OBLIGATORIO en el panel entrante: es el bug clásico que
   sufre siempre un catálogo de 744 ítems — cambias de pestaña y aterrizas en
   mitad de la nada porque el scroll anterior se quedó pegado.
   ═══════════════════════════════════════════════════════════════════════════ */

.slots{
  display: flex;
  gap: 2px;
  border-bottom: var(--bw-2) solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
}
.slots::-webkit-scrollbar{ display: none }

.slots__tab{
  --c: 12px;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: 38px;
  padding-inline: var(--sp-4);
  margin-bottom: calc(var(--bw-2) * -1);
  background: var(--surface-2);
  border: 0;
  border-bottom: var(--bw-2) solid var(--line);
  /* Solapa con el borde superior chaflanado a la derecha. */
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  color: var(--text-3);
  font-family: var(--ff-ui);
  font-weight: 600;
  font-size: var(--fs-xs);
  letter-spacing: var(--tr-wider);
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--d-fast) var(--ez-term),
              background var(--d-fast) var(--ez-term);
}

/* La pestaña está recortada: el `outline` moriría en el corte. Anillo interior. */
.slots__tab:focus-visible{
  outline: none;
  box-shadow: inset 0 0 0 var(--bw-2) var(--focus);
}

.slots__tab[aria-selected="true"]{
  background: var(--surface);
  color: var(--text);
  border-bottom-color: transparent;   /* se funde con el panel */
  box-shadow: inset 0 3px 0 var(--accent);   /* regla superior de 3 px */
}
.slots__tab[aria-selected="true"]:focus-visible{
  box-shadow: inset 0 3px 0 var(--accent), inset 0 0 0 var(--bw-2) var(--focus);
}

/* Recuento a la derecha: `HABILIDADES [65]`. Va en chip, no entre paréntesis
   sueltos, para que la cifra se lea como dato del registro. */
.slots__n{
  padding-inline: 5px;
  background: var(--field);
  color: var(--text-2);
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-none);
}
.slots__tab[aria-selected="true"] .slots__n{ background: var(--accent-ink); color: var(--accent-brt) }

.slots__panel{
  padding-top: var(--sp-5);
  overflow-y: auto;
}
.slots__panel[hidden]{ display: none }


/* ═══════════════════════════════════════════════════════════════════════════
   §6.8 · CHIP `.tag`

   DECISIÓN DE CONTRASTE, EXPLÍCITA Y DELIBERADA
   El chip mide 11 px. A ese tamaño el color de familia como TEXTO es ilegible
   o directamente ilegal: --k-red da 4,3:1 y necesita ≥14 px/600; --k-purple
   da 5,1:1 y necesita ≥16 px/700; --k-orange da 6,4:1 y necesita ≥14 px.
   Así que el color de familia vive en el TRAZO y en el relleno al 8%, y el
   texto va siempre en --text (15,5:1). Se conserva la codificación por color
   y se conserva la ley. Además el chip siempre lleva palabra: el estado no
   depende del color jamás (§11.2).
   ═══════════════════════════════════════════════════════════════════════════ */

.tag{
  --tag-c: var(--text-3);
  --c: var(--chf-xs);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 22px;
  padding-inline: 8px;
  background: color-mix(in srgb, var(--tag-c) 8%, transparent);
  border: var(--bw-hair) solid var(--tag-c);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  color: var(--text);
  font-family: var(--ff-ui);
  font-weight: 600;
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wide);
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Familias. Los nombres son los del dominio, no los del color. */
.tag--dmg    { --tag-c: var(--k-red) }
.tag--armor  { --tag-c: var(--ok) }
.tag--bonus  { --tag-c: var(--alert) }
.tag--loc    { --tag-c: var(--k-purple) }
.tag--hum    { --tag-c: var(--st-apuntar) }
.tag--ammo   { --tag-c: var(--warn) }
.tag--net    { --tag-c: var(--system) }
.tag--neutral{ --tag-c: var(--text-3) }

/* Chip filtrable: añade la aspa de 8 px. */
.tag__x{
  width: 8px; height: 8px;
  background: transparent;
  border: 0;
  cursor: pointer;
  position: relative;
  flex: none;
}
.tag__x::before,
.tag__x::after{
  content: '';
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: currentColor;
}
.tag__x::before{ transform: rotate( 45deg) }
.tag__x::after { transform: rotate(-45deg) }

/* `×2 PM` — pegado a las 6 habilidades difíciles del mod: Armas pesadas 2885,
   Disparo automático 2886, Artes marciales 2900, Electrónica/Seguridad 2906,
   Enfermería 2907, Pilotar vehículo aéreo 3083. SE LEE DEL FLAG `difficulty`
   DEL DATO, NUNCA DE UNA LISTA ESCRITA A MANO en la plantilla. */
.tag--x2{
  --tag-c: var(--warn);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  font-family: var(--ff-mono);
  font-weight: 600;
  letter-spacing: var(--tr-none);
}

/* Máximo 4 chips visibles por tarjeta: el quinto y siguientes se pliegan. */
.tag-row{ display: flex; flex-wrap: wrap; gap: var(--sp-2) }
.tag-row > .tag:nth-child(n+5){ display: none }
.tag-row.is-expanded > .tag{ display: inline-flex }


/* ═══════════════════════════════════════════════════════════════════════════
   §6.9 · SELLO DE ESTADO `.seal`

   Es UN OBJETO FÍSICO, no una etiqueta de estado.

   CONTRATO DE MARCADO
   <div class="seal seal--ok">
     <i class="seal__glyph" aria-hidden="true">✓</i>
     <span class="seal__cap">EXPEDIENTE<br>SELLADO</span>
   </div>

   El borde muere en la diagonal del chaflán: es la concesión aceptada y
   validada en el prototipo. Un trazo cortado en seco por el corte se lee como
   troquel, que es exactamente lo que queremos.
   ═══════════════════════════════════════════════════════════════════════════ */

.seal{
  --seal-c: var(--text-3);
  --c: 10px;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 44px; height: 44px;
  padding: 3px;
  background: var(--k-void);
  border: var(--bw-hair) solid var(--seal-c);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--c)),
                     calc(100% - var(--c)) 100%, 0 100%);
  color: var(--seal-c);
  text-align: center;
}

.seal__glyph{
  font-size: 18px;
  line-height: 1;
  font-weight: 300;      /* hairline: el glifo es una marca, no un icono gordo */
  font-style: normal;
}

/* La micro-leyenda va DENTRO de la caja, debajo del glifo. Dos líneas, mono,
   mayúsculas. Fuera de la caja sería una etiqueta; dentro es un troquel. */
.seal__cap{
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wider);
  line-height: 1.15;
  text-transform: uppercase;
}

/* Estados. Cada uno lleva glifo Y texto: el estado nunca es sólo color. */
.seal--ok     { --seal-c: var(--ok) }
.seal--wait   { --seal-c: var(--alert) }
.seal--deny   { --seal-c: var(--danger) }
.seal--void   { --seal-c: var(--text-off) }
.seal--critica{ --seal-c: var(--st-critica) }
.seal--grave  { --seal-c: var(--st-grave) }
.seal--mortal { --seal-c: var(--st-mortal) }
.seal--muerto { --seal-c: var(--st-muerto); color: var(--k-red-brt) }
.seal--psico  { --seal-c: var(--st-psico);  color: var(--st-apuntar) }

/* EL COLOR DEL ESTADO VIVE EN EL BORDE, NO EN LA MICRO-LEYENDA.
   Los RGBA del Master (§3.3) son colores de MARCA, elegidos para un icono de
   estado dentro del juego, no para llevar texto encima de --k-void. Medido
   sobre el fondo real del sello: --st-critica 2,58:1, --st-grave 2,77:1 y
   --st-mortal 1,32:1 — la leyenda de 11px que §6.9 obliga a meter DENTRO de
   la caja era literalmente invisible, y son los tres estados que la matriz de
   heridas de §10.9 usa y que el mod escribe 22 veces.
   Es exactamente el patrón que `.seal--muerto` y `.seal--psico` ya usaban dos
   líneas más arriba, y que a los tres estados de herida se les olvidó.
   `--seal-c` sigue pintando el `border-color`, que es donde §3.3 quiere ese
   color, y el glifo más el texto siguen cumpliendo §11.2: el estado nunca es
   sólo color.
   `.seal--void` entra en la misma regla por el mismo motivo: --text-off da
   2,48:1 y el Anexo B.14.1.3 lo prohíbe para texto informativo. */
.seal--critica,
.seal--grave,
.seal--mortal { color: var(--k-bone) }
.seal--void   { color: var(--text-3) }

/* EN COPIA IMPRESA EL SELLO SE IMPRIME, NO SE ILUMINA.
   `.seal` escribe `background: var(--k-void)` con la primitiva a fuego, así
   que en papel seguía siendo una pastilla casi negra sobre el documento: la
   ficción de COPIA IMPRESA es haber despegado el overlay y ver el impreso
   debajo, y un impreso no tiene pastillas negras — tiene tinta sobre papel.
   El color del estado se queda donde §3.3 lo quiere, en el borde; la leyenda
   pasa a tinta (#0B0E12 sobre #FFFFFF = 19,3:1). Se listan las variantes una
   a una porque cada una fija su `color` propio y hay que ganarles por
   especificidad, no por orden. */
:root[data-medio="papel"] .seal{
  background: var(--surface);
  color: var(--k-ink);
}
:root[data-medio="papel"] .seal--critica,
:root[data-medio="papel"] .seal--grave,
:root[data-medio="papel"] .seal--mortal,
:root[data-medio="papel"] .seal--muerto,
:root[data-medio="papel"] .seal--psico,
:root[data-medio="papel"] .seal--void{ color: var(--k-ink) }

/* VARIANTE GRANDE · el sello del dictamen. 96 × 96, anillo exterior punteado
   y el número de expediente cabalgando el borde inferior. */
.seal--stamp{
  --c: 18px;
  position: relative;
  width: 96px; height: 96px;
  padding: var(--sp-3);
  gap: 6px;
  border-width: var(--bw-2);
}
.seal--stamp .seal__glyph{ font-size: 30px }
.seal--stamp .seal__cap{ font-size: var(--fs-3xs) }

/* EL ENVOLTORIO NO ES DECORACIÓN, ES NECESIDAD.
   `clip-path` recorta TODO lo que pinta el elemento: outline, box-shadow y
   pseudoelementos que asomen. Un anillo punteado alrededor del sello, o el
   sangrado del tinte, dibujados sobre el propio `.seal--stamp`, mueren en el
   corte. Por eso ambos viven en `.seal-ring`, que no lleva chaflán.
   Para el sello pequeño de 44 px el envoltorio es opcional. */
.seal-ring{
  position: relative;
  display: inline-block;
  padding: 6px;
  outline: var(--bw-hair) dashed color-mix(in srgb, currentColor 45%, transparent);
  outline-offset: 0;
}
.seal-ring__id{
  display: block;
  margin-top: 4px;
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wider);
  text-align: center;
  color: var(--text-3);
}

/* Al cambiar de estado: `tear` de 120 ms. Al sellar: `stamp-impact`. */
.seal.is-changing{ animation: tear 120ms steps(4) 1 }
.seal--stamp.is-hit{
  animation: stamp-impact 200ms var(--ez-snap) both;
}
/* EL TINTE SANGRA 900 ms DESPUÉS DEL GOLPE: una copia desenfocada del sello
   que aparece y se queda, como tinta que ha calado el papel.
   Se pinta en `.seal-ring`, no en el sello, por lo dicho arriba: sobre el
   sello el `clip-path` se la comería entera. El dictamen pone `.is-hit` en
   los DOS elementos — el sello para el golpe, el anillo para la mancha. */
.seal-ring.is-hit::before,
.seal-ring:has(.is-hit)::before{
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  filter: blur(6px);
  opacity: 0;
  animation: stamp-bleed 900ms var(--ez-term) 200ms both;
  pointer-events: none;
}
@keyframes stamp-bleed{ from{ opacity: 0 } to{ opacity: .22 } }


/* ═══════════════════════════════════════════════════════════════════════════
   §6.10 · PEINE `.comb`

   NUNCA UNA BARRA CONTINUA. Los recursos consumibles son peines: tiras
   subdivididas en ranuras estrechas. Gastar deja de ser un número que baja y
   pasa a ser un objeto que se vacía.

   USOS OBLIGATORIOS
     · 55 puntos de característica  (DiceConfig col. 2)
     · 50 puntos de habilidad       (DiceConfig col. 3)  ← el segundo
       presupuesto; sin él la ficha es una foto fija
     · Suerte · Salvaciones contra Muerte · PV · Humanidad (se vacía de
       derecha a izquierda) · munición por calibre · slots de cyberware

   CONTRATO DE MARCADO — INTERACTIVO (el jugador puede marcar)
   <div class="comb comb--live" role="group" aria-labelledby="pc-t">
     <span class="comb__tab" id="pc-t">CARACT.</span>
     <input class="comb__seg" type="checkbox" style="--i:0" aria-label="Punto 1 de 55">
     … 55 veces …
   </div>

   NO SE USA `<fieldset>` + `<legend>`: el `<legend>` se saca del flujo del
   fieldset en todos los motores y en un contenedor flex se coloca donde le
   parece. Un `<div role="group">` con `aria-labelledby` da exactamente el
   mismo árbol de accesibilidad y sí se deja maquetar.

   CONTRATO DE MARCADO — LECTURA (dato que no se toca: munición, PV)
   <div class="comb" role="img" aria-label="Puntos de vida: 28 de 40">
     <span class="comb__tab">PV</span>
     <i class="comb__seg is-spent"></i> …
   </div>

   Cada ranura marcable es un `<input type="checkbox">` REAL con
   `appearance: none`. Es lo único que la hace navegable con teclado, y por
   tanto lo único que la hace legal.

   SOBRE LOS 10 px DE LA ETIQUETA: DESIGN §6.10 pide la pestaña a 10 px, pero
   §4.2 y §11.2 fijan el suelo tipográfico ABSOLUTO en 11 px («Nada baja de
   11 px. Nunca»). Manda el suelo: la pestaña va a --fs-3xs. Un HUD ilegible
   no es inmersivo, es un HUD roto.
   ═══════════════════════════════════════════════════════════════════════════ */

.comb{
  --seg: 10px;        /* ancho de ranura */
  --comb-h: 16px;     /* alto del peine */
  --comb-c: var(--accent);
  display: flex;
  align-items: stretch;
  /* ENVUELVE SIEMPRE, Y ES DEL COMPONENTE, NO DE UNA PÁGINA.
     Los peines obligatorios de §6.10 son de 55 y de 50 ranuras y los
     segmentos llevan `flex: none`. Medido: el de 55 mide 731px de contenido
     dentro de la columna de 683px de master/review.html, y a 375px —con el
     `--seg: 14px` que impone el propio bloque móvil de más abajo— mide 951px
     dentro de 293: 40 ranuras cortadas y el <body> con scrollWidth 992.
     Un peine de 55 ranuras no cabe en una columna de 683px ni en un móvil:
     envolver es el comportamiento correcto para TODOS los usos, no un parche
     para los dos peines del asistente. */
  flex-wrap: wrap;
  gap: 2px;
  row-gap: 3px;
  min-height: var(--comb-h);
  border: 0;
  padding: 0;
  margin: 0;
}

/* Etiqueta en pestaña sólida en el extremo izquierdo, tinta en knockout.
   LA TINTA ES --bg Y NO --k-void. El relleno es --text-2, que sigue al medio:
   en vista de terminal es hueso claro y en COPIA IMPRESA es gris de imprenta.
   Con la primitiva a fuego, en papel quedaba #05070A sobre #3A4048 —1,93:1
   medido, negro sobre negro—. Con --bg la pestaña siempre pinta el fondo de
   su propio medio: 9,8:1 en terminal y 9,1:1 en papel, sin una regla extra ni
   una rebanada de `[data-medio]`. */
.comb__tab{
  display: inline-flex;
  align-items: center;
  padding-inline: 6px;
  margin-right: var(--sp-2);
  background: var(--text-2);
  color: var(--bg);
  font-family: var(--ff-mono);
  font-weight: 600;
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wide);
  text-transform: uppercase;
  float: none;
  flex: none;
}

.comb__seg{
  appearance: none;
  -webkit-appearance: none;
  width: var(--seg);
  min-width: var(--seg);
  height: var(--comb-h);
  margin: 0;
  padding: 0;
  background: var(--field);
  border: var(--bw-hair) solid var(--line-strong);
  box-shadow: var(--sh-sunk);
  flex: none;
  transition: background var(--d-instant) var(--ez-snap);
}
.comb--live .comb__seg{ cursor: pointer }

/* Ranura gastada: sólida del acento. Se llena con `comb-fill` — 90 ms por
   segmento, escalonado 40 ms, sonido `tick` estrangulado. */
.comb__seg:checked,
.comb__seg.is-spent{
  background: var(--comb-c);
  border-color: var(--comb-c);
  box-shadow: none;
}
.comb__seg.is-filling{
  animation: comb-fill 90ms var(--ez-snap) both;
  animation-delay: calc(var(--i, 0) * 40ms);
}

/* Ranura en curso — la que está a punto de gastarse. Parpadea con el mismo
   ritmo que el cursor de bloque del campo en foco, y sólo mientras el peine
   TIENE EL FOCO: es el cursor del peine, no un adorno que late solo. Así no
   se rompe la regla de las cuatro únicas animaciones permanentes (§7.1). */
.comb:focus-within .comb__seg.is-live{
  background: var(--comb-c);
  animation: caret 1000ms step-end infinite;
}

.comb__seg:focus-visible{
  outline: var(--bw-2) solid var(--focus);
  outline-offset: 2px;
  z-index: 1;
}
.comb__seg:disabled{ cursor: not-allowed; border-color: var(--line) }

/* HUMANIDAD · se vacía de derecha a izquierda. Perder humanidad no se lee
   como gastar puntos: se lee como que algo te está comiendo por el otro lado. */
.comb--rtl{ flex-direction: row-reverse; justify-content: flex-end }
.comb--rtl .comb__tab{ margin-right: 0; margin-left: var(--sp-2) }

/* Familias de color por recurso. */
.comb--hp   { --comb-c: var(--ok) }
.comb--hum  { --comb-c: var(--st-apuntar) }
.comb--ammo { --comb-c: var(--warn) }
.comb--luck { --comb-c: var(--alert) }
.comb--death{ --comb-c: var(--danger) }

/* Peines finos para barras densas (munición, slots de cyberware). */
.comb--thin{ --seg: 6px; --comb-h: 12px }
/* `.comb--wrap` se ha retirado: envolver ya es el comportamiento de `.comb`. */

/* VARIANTE DECORATIVA SIN INTERACCIÓN — la del §6.10 literal, un solo
   elemento con la vía pintada en gradiente. Sólo para adorno de HUD con
   `aria-hidden`: no es marcable, luego no puede llevar dato que importe. */
.comb--solid{
  display: block;
  height: var(--comb-h);
  background: repeating-linear-gradient(90deg,
    var(--field) 0 var(--seg),
    var(--line-strong) var(--seg) calc(var(--seg) + 2px));
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.11 · RETRATO `.mugshot`

   CONTRATO DE MARCADO
   <figure class="mugshot">
     <div class="mugshot__frame">
       <img class="mugshot__img" src="…" alt="Retrato de V">
       <i class="mugshot__cbr mugshot__cbr--tl"></i>
       <i class="mugshot__cbr mugshot__cbr--tr"></i>
       <i class="mugshot__cbr mugshot__cbr--bl"></i>
     </div>
     <figcaption class="mugshot__cap">
       REG. FOTOGRÁFICO · NC-RG-2077-0447-K · CAPT. 14.03.2077
     </figcaption>
   </figure>

   SÓLO TRES ESQUINAS DE MIRA. La cuarta falta a propósito — y la que falta es
   la inferior derecha, que es justo donde el chaflán se come el marco: la
   ausencia deja de parecer un descuido y pasa a ser la consecuencia del corte.

   Encuadre arrastrable con POINTER EVENTS (no sólo ratón), `clamp` 0–100%,
   listeners retirados en cada subida, y {focus_x, focus_y} persistidos en BD.
   La textura VHS externa de transparenttextures.com SE ELIMINA: dependencia
   de terceros que muere con una CSP estricta. Grano SVG en `data:`.
   ═══════════════════════════════════════════════════════════════════════════ */

.mugshot{ margin: 0; display: block }

.mugshot__frame{
  --c: var(--chf-xl);
  --shot-c: var(--line-strong);
  position: relative;
  aspect-ratio: 3 / 4;
  width: 100%;
  overflow: hidden;
  background: var(--field);
  border: var(--bw-2) solid var(--shot-c);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--c)),
                     calc(100% - var(--c)) 100%, 0 100%);
  touch-action: none;   /* el arrastre de encuadre es nuestro, no del scroll */
}
.mugshot--ok    .mugshot__frame{ --shot-c: var(--ok) }
.mugshot--wait  .mugshot__frame{ --shot-c: var(--alert) }
.mugshot--deny  .mugshot__frame{ --shot-c: var(--danger) }
.mugshot--muerto .mugshot__frame{ --shot-c: var(--st-muerto) }

.mugshot__img{
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--focus-x, 50%) var(--focus-y, 50%);
  display: block;
}

/* Scanlines locales + viñeta. La banda de tracking desciende cada 6 s durante
   900 ms: es un solo elemento y no cuenta contra las tres capas compuestas a
   pantalla completa de §8.1, porque no está a pantalla completa. */
.mugshot__frame::after{
  content: '';
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(5,7,10,.34) 0 1px,
    transparent 1px 3px);
  box-shadow: inset 0 0 40px -10px var(--k-void);
  pointer-events: none;
}
.mugshot__frame::before{
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: 22px;
  z-index: 2;
  background: linear-gradient(to bottom,
    transparent,
    color-mix(in srgb, var(--k-bone) 16%, transparent),
    transparent);
  transform: translateY(-40px);
  opacity: 0;
  pointer-events: none;
  animation: tracking-band 6000ms var(--ez-lin) infinite;
}
/* El recorrido va en px absolutos y no en porcentaje del contenedor: el
   elemento mide 22 px, así que un 100% serían 22 px y la banda no llegaría ni
   a asomar. 620 px cubre el retrato más alto que cabe en la columna del
   expediente, y lo que sobra lo recorta el `overflow: hidden` del marco. */
@keyframes tracking-band{
  0%,   84%{ opacity: 0;  transform: translateY(-40px) }
  85%     { opacity: .9;  transform: translateY(-40px) }
  99%     { opacity: .9;  transform: translateY(620px) }
  100%    { opacity: 0;   transform: translateY(620px) }
}

/* TRES esquinas de mira de 22 px, 1 px de acento al 50%. */
.mugshot__cbr{
  position: absolute;
  width: 22px; height: 22px;
  opacity: .5;
  pointer-events: none;
  z-index: 3;
}
.mugshot__cbr--tl{ top: 6px; left: 6px;  border-top: var(--bw-hair) solid var(--accent); border-left:  var(--bw-hair) solid var(--accent) }
.mugshot__cbr--tr{ top: 6px; right: 6px; border-top: var(--bw-hair) solid var(--accent); border-right: var(--bw-hair) solid var(--accent) }
.mugshot__cbr--bl{ bottom: 6px; left: 6px; border-bottom: var(--bw-hair) solid var(--accent); border-left: var(--bw-hair) solid var(--accent) }

/* ESTADO VACÍO · barras SMPTE desaturadas y SIN SEÑAL parpadeando a 2000 ms.
   El parpadeo es una excepción acotada a §7.1: sólo existe mientras NO hay
   retrato, desaparece en cuanto se sube uno, y cae bajo prefers-reduced-motion.
   Un monitor sin señal que se queda quieto no es un monitor sin señal. */
.mugshot--empty .mugshot__frame{
  background-image: linear-gradient(90deg,
    var(--smpte-1) 0      14.28%, var(--smpte-2) 14.28% 28.56%,
    var(--smpte-3) 28.56% 42.84%, var(--smpte-4) 42.84% 57.12%,
    var(--smpte-5) 57.12% 71.40%, var(--smpte-6) 71.40% 85.68%,
    var(--smpte-7) 85.68% 100%);
  filter: saturate(.45);
}
.mugshot--empty .mugshot__frame::after{ box-shadow: inset 0 0 60px -6px var(--k-void) }
.mugshot__nosig{
  position: absolute;
  inset: auto 0 34% 0;
  z-index: 4;
  text-align: center;
  font-family: var(--ff-osd);
  font-size: var(--fs-sm);
  letter-spacing: var(--tr-widest);
  color: var(--k-bone);
  text-shadow: 0 0 8px var(--k-void);
  animation: nosignal 2000ms step-end infinite;
}
@keyframes nosignal{ 0%,49%{ opacity: 1 } 50%,100%{ opacity: .08 } }

/* El pie va SIEMPRE. Sin él es una foto; con él es un registro fotográfico. */
.mugshot__cap{
  margin-top: var(--sp-2);
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wide);
  color: var(--text-3);
  text-transform: uppercase;
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.12 · AVISO `.osd` (toast)

   CONTRATO DE MARCADO
   <div class="osd-stack" role="status" aria-live="polite">
     <article class="osd osd--info">
       <header class="osd__h">TRANSMISIÓN // NODO 447 <i class="osd__dot"></i></header>
       <p class="osd__b"><b>[INFO]</b> tu aval sigue vivo. te quedan 4 días.</p>
     </article>
   </div>

   Vive 6000 ms. Los de INCIDENCIA no se autodescartan nunca. Máximo 3
   apilados: el cuarto empuja al primero.
   ═══════════════════════════════════════════════════════════════════════════ */

.osd-stack{
  position: fixed;
  right: var(--sp-5);
  bottom: calc(var(--bar-bottom) + var(--command-h) + var(--sp-4));
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  width: 340px;
  max-width: calc(100vw - var(--sp-8));
  pointer-events: none;
}
.osd-stack > .osd:nth-child(n+4){ display: none }

.osd{
  --osd-c: var(--system);

  /* LA TINTA DEL CANAL NO PUEDE SER EL COLOR DEL CANAL, Y ESTABA SIÉNDOLO.
     --osd-c hace dos trabajos: es la marca (filete de 3 px y punto que late)
     y era también el color del `[CANAL]` de 14 px. Medido sobre --surface-2:
     RUMOR (--st-apuntar) 4,01:1 —por debajo del 4,5:1 de WCAG AA—, ALERTA
     (--danger) 4,82:1 y AVISO (--warn) 5,78:1, los tres por debajo del 6,2:1
     de §11.1. En COPIA IMPRESA era peor: 2,76:1 y 3,32:1.
     --osd-ink mezcla el color del canal con la tinta del medio en un 55 %:
     sobre carbón aclara, sobre papel oscurece, y en las SEIS familias y en
     los DOS medios el resultado queda entre 6,5 y 13:1 sin perder el tono.
     El color saturado no se pierde: sigue entero en el filete y en el punto,
     a dos milímetros de la etiqueta. */
  --osd-ink: color-mix(in srgb, var(--osd-c) 55%, var(--text));

  --c: var(--chf-sm);
  pointer-events: auto;
  background: var(--surface-2);
  border-left: 3px solid var(--osd-c);
  box-shadow: var(--sh-lift);
  clip-path: polygon(0 0, calc(100% - var(--c)) 0, 100% var(--c),
                     100% 100%, 0 100%);
  padding: var(--sp-3) var(--sp-4);
  /* Entra deslizando 24 px. NADA DE REBOTE ELÁSTICO: esto es una terminal,
     no una aplicación de móvil pidiendo cariño. */
  animation: osd-in 220ms var(--ez-term) both;
}
@keyframes osd-in{
  from{ transform: translateX(24px); opacity: 0 }
  to  { transform: translateX(0);    opacity: 1 }
}
.osd.is-leaving{ animation: osd-out 160ms var(--ez-in) both }
@keyframes osd-out{
  from{ transform: translateX(0);    opacity: 1 }
  to  { transform: translateX(24px); opacity: 0 }
}

.osd__h{
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: 4px;
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wider);
  text-transform: uppercase;
  color: var(--text-3);
}
/* Punto pulsante de 6 px — uno de los pocos latidos permitidos, y sólo
   mientras el aviso está en pantalla. */
.osd__dot{
  width: 6px; height: 6px;
  margin-left: auto;
  background: var(--osd-c);
  animation: pulse 2200ms ease infinite alternate;
  flex: none;
}
.osd__b{
  font-family: var(--ff-body);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  text-transform: none;
  color: var(--text-2);
}
.osd__b b{
  font-family: var(--ff-mono);
  font-weight: 600;
  color: var(--osd-ink);
  margin-right: 4px;
}

/* Las seis familias. El prefijo entre corchetes va SIEMPRE: el tipo de aviso
   nunca depende sólo del color. */
.osd--alerta { --osd-c: var(--danger) }
.osd--aviso  { --osd-c: var(--warn) }
.osd--info   { --osd-c: var(--system) }
.osd--rumor  { --osd-c: var(--st-apuntar) }
.osd--sistema{ --osd-c: var(--alert) }
.osd--evento { --osd-c: var(--ok) }

/* FUERA DEL MURO LOS MENSAJES SE VEN CIFRADOS, NO AUSENTES. Es el mejor
   detalle del repositorio actual y se conserva: el que no tiene nivel de
   acceso ve que hay tráfico, y ve que no lo entiende.
   El cifrado se lee: va en --text-3, no en --text-off (2,15:1 medido a 14px).
   Que el tráfico esté cifrado es la información; que no se vea el renglón es
   un fallo de render. Lo que dice «no lo entiendes» es el hexadecimal, no el
   contraste. */
.osd--hex .osd__b{
  font-family: var(--ff-mono);
  color: var(--text-3);
  word-break: break-all;
  letter-spacing: var(--tr-wide);
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.13 · REDACCIÓN `.redact` — LA FIRMA DEL SISTEMA

   El contenido clasificado se renderiza CON CARACTERES REALES ALEATORIOS
   DEBAJO DE LA BARRA. No es un `display:none` con una barra encima: hay texto
   debajo, y por eso al retraerse la barra hay algo que resolver.

   ESTA ÚNICA TÉCNICA RESUELVE SEIS PROBLEMAS DE PERMISOS: el portal, el feed
   sin ficha, el precio sobre reputación en el Mercado Nocturno, las secciones
   bloqueadas del Manual de Operación Ciudadana, el contenido de otros
   distritos y la clave de zona.

   El no autorizado NUNCA ve una página vacía ni un 403: VE EL MUNDO TACHADO.

   <span class="redact" aria-label="Material clasificado">A7F3-9K2Q-XX41</span>
   ═══════════════════════════════════════════════════════════════════════════ */

.redact{
  position: relative;
  color: transparent;
  user-select: none;
  -webkit-user-select: none;
}
.redact::after{
  content: '';
  position: absolute;
  inset: -1px -3px;
  background: var(--k-void);
  border-bottom: var(--bw-hair) solid var(--accent-deep);
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--d-declass) var(--ez-term);
}

/* DESCLASIFICADO. Ocurre exactamente una vez en la vida del jugador, dura
   1400 ms, y es la única animación larga del sitio precisamente porque todo
   lo demás es seco y corto. */
.redact.is-open{
  color: var(--system);
  user-select: text;
  -webkit-user-select: text;
}
.redact.is-open::after{ clip-path: inset(0 0 0 100%) }

/* Rótulo obligatorio junto a cualquier bloque redactado. Sin él, la barra es
   un fallo de render; con él, es una decisión del Estado.

   EL COLOR DE CLASIFICACIÓN MARCA, NO ESCRIBE. `--classif` es un relleno de
   banda: en nivel 0 vale --k-red-deep, y 11px de #8C0A1C sobre carbón dan
   2,02:1 medido en aval.html y 2,09:1 en normativa.html. A 2:1 el rótulo no
   se lee, la barra vuelve a parecer un fallo de render y con ella se cae la
   firma del sistema entera (§12.1). El texto pasa a --text-2 (9,43:1) y el
   color de clasificación baja al filete izquierdo, donde sigue identificando
   el nivel de un vistazo y además gana el gesto tipográfico. */
.redact-label{
  display: block;
  border-left: var(--bw-3) solid var(--classif);
  padding-left: var(--sp-2);
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-widest);
  text-transform: uppercase;
  color: var(--text-2);
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6.14 · ANOTACIÓN `.marker` — NOTA DEL OFICIAL VERIFICADOR

   El Verificador NO rellena un textarea de comentarios: ANOTA EL EXPEDIENTE.

   CONTRATO DE MARCADO
   <div class="marker-layer" aria-hidden="true">      <!-- capa de trazos -->
     <svg class="marker__trace"><path d="M8 40 L8 88 L96 88"/></svg>
   </div>
   <i class="marker__pin" style="top:320px"></i>       <!-- en el raíl -->
   <article class="ri ri--warn">
     <header class="ri__h">● MARCADOR 02 · CAMPO: stats.EMP</header>
     <p class="ri__b">empatía 2 con ese trasfondo no cuadra. …</p>
     <footer class="ri__f">— SECCIÓN III</footer>
     <button class="btn btn--line btn--sm">
       <span class="btn__lbl">Subsanar este campo</span></button>
   </article>

   ANCLAJE: el SVG se recalcula en ResizeObserver + scroll con estrangulado
   por rAF. En <900 px degrada a LISTA SIMPLE CON SALTO AL CAMPO, sin línea:
   anclar coordenadas a campos que se mueven con el scroll en 375 px no es
   viable y fingirlo se ve peor que no hacerlo.
   ═══════════════════════════════════════════════════════════════════════════ */

.marker-layer{
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  pointer-events: none;
  overflow: visible;
}

/* Punto magenta de 8 px pinchado en el raíl izquierdo, a la altura del campo. */
.marker__pin{
  position: absolute;
  left: calc(var(--rail-w) / 2 - 4px);
  width: 8px; height: 8px;
  background: var(--st-apuntar);
  box-shadow: 0 0 10px -1px var(--st-apuntar);
  z-index: var(--z-raised);
}

/* LÍNEA GUÍA · hairline de 1,5 px, UN SOLO CODO a 45° o 90°, terminada en
   círculo abierto de r=4 sobre el campo. Dos codos ya no es una anotación
   técnica: es un cable. */
.marker__trace{
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  fill: none;
  stroke: var(--st-apuntar);
  stroke-width: 1.5;
  stroke-linecap: square;
}
.marker__trace path{
  stroke-dasharray: var(--len, 400);
  stroke-dashoffset: var(--len, 400);
  /* §6.14 fija 320 ms para la línea del marcador; §7.2 define `trace-draw` a
     520 ms para la traza larga del sellado. Se respetan las dos: el keyframe
     es común y aquí se acorta la duración. */
  animation: trace-draw 320ms var(--ez-term) both;
}
.marker__trace circle{ fill: none; stroke: var(--st-apuntar); stroke-width: 1.5 }

/* ANILLO PERSISTENTE del campo señalado: mientras hay un marcador activo, el
   campo al que apunta queda rodeado en magenta.
   El LATIDO de §10.6 («el campo señalado pulsa») no vive aquí a propósito: es
   un acuse de recibo puntual del rebobinado del raíl, no un estado, y por eso
   lo dispara la pantalla del dictamen con su propia clase de un solo uso.
   Ponerlo en `.is-marked` sería un bucle permanente contra §7.1 y además
   pintaría un anillo alrededor de los pasos del scrubber, que usan esta misma
   clase para otra cosa. */
.is-marked{ box-shadow: 0 0 0 2px var(--st-apuntar) }

/* PATRÓN `.ri` — el que ya existe en home.html, conservado. Barra izquierda
   de 6 px en --warn, numeral en mono, y EL TEXTO EN CAJA NORMAL, Barlow 500 a
   17 px. La nota del Verificador es la lectura más densa y más cargada
   emocionalmente del producto: aquí no se está en el suelo de contraste ni se
   grita en mayúsculas. --k-orange da 6,4:1 y a 17 px va sobrado. */
.ri{
  --ri-c: var(--warn);
  border-left: var(--bw-seal) solid var(--ri-c);
  background: color-mix(in srgb, var(--ri-c) 3%, transparent);
  padding: var(--sp-4) var(--sp-5);
}
.ri--warn{ --ri-c: var(--warn) }
.ri--deny{ --ri-c: var(--danger) }
.ri--ok  { --ri-c: var(--ok) }
.ri--sys { --ri-c: var(--system) }

.ri__h{
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
  font-family: var(--ff-mono);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tr-wider);
  text-transform: uppercase;
  color: var(--ri-c);
}
.ri__b{
  font-family: var(--ff-body);
  font-weight: 500;
  font-size: var(--fs-lg);
  line-height: var(--lh-body);
  text-transform: none;
  color: var(--text);
}
.ri__f{
  margin-top: var(--sp-3);
  font-family: var(--ff-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tr-wider);
  color: var(--text-3);
}

/* MÓVIL · el trazo desaparece y el marcador se convierte en una lista con
   salto al campo. Se pierde el dibujo y se gana que funcione. */
@media (max-width: 899px){
  .marker-layer,
  .marker__pin{ display: none }
  .ri{ padding: var(--sp-3) var(--sp-4) }
  /* AQUÍ NO SE TOCA `.ri__b`. §2.2 regla 3 no se negocia: «el contenido
     escrito por un jugador nunca lleva mayúsculas forzadas, nunca lleva
     monoespaciada, y NUNCA BAJA DE 17px. Ni en la zona oficial ni en la
     calle». La nota del Verificador es prosa escrita por una persona y es,
     además, la lectura más densa y más cargada emocionalmente del producto
     (§11.1). Lo que degrada en móvil es la capa de trazos, no el cuerpo. */
}


/* ═══════════════════════════════════════════════════════════════════════════
   §7.2 · CATÁLOGO DE ANIMACIONES DE COMPONENTE

   Las de chasis (sys-boot, page-in, page-out, pulse, marquee, noise-shift,
   caret, chroma, bleed-red) viven en chrome.css y reset.css. Aquí sólo lo que
   pertenece a los componentes.

   REGLA DE ORO (§7.1): fuera del arranque, del sello y de la desclasificación,
   NADA SE ANIMA EN BUCLE. Ese contraste es lo único que hace que el sello,
   cuando cae, sea un acontecimiento.
   ═══════════════════════════════════════════════════════════════════════════ */

/* panel-commit · 240 ms · el panel se materializa desde la banda central.
   Nada de escalados ni de desvanecidos: el panel se ABRE, como una persiana
   de datos que termina de recibirse. */
@keyframes panel-commit{
  from{ clip-path: inset(50% 0 50% 0) }
  to  { clip-path: inset(0) }
}

/* row-stagger · 180 ms, +22 ms por fila, máximo 12 filas. */
@keyframes row-stagger{
  from{ transform: translateX(-6px); opacity: 0 }
  to  { transform: translateX(0);    opacity: 1 }
}

/* focus-lock · 120 ms · el anillo ámbar se cierra sobre el elemento.
   Definido idéntico en reset.css, donde lo aplica `:where(:focus-visible)`.
   Se repite aquí, con el mismo cuerpo, para que components.css sea legible
   suelto: al ser idéntico, el orden de cascada es irrelevante. */
@keyframes focus-lock{
  from{ outline-offset: 6px }
  to  { outline-offset: 2px }
}

/* hover-rule · 180 ms · la regla de acento crece desde la izquierda.
   Se aplica por transición en `.dossier__h::before`; el keyframe existe para
   los casos en los que hay que dispararla sin hover (llegada de dato nuevo). */
@keyframes hover-rule{
  from{ transform: scaleX(0) }
  to  { transform: scaleX(1) }
}

/* comb-fill · 90 ms por segmento, +40 ms de escalón. */
@keyframes comb-fill{
  from{ clip-path: inset(0 100% 0 0) }
  to  { clip-path: inset(0) }
}

/* comb-loop · 900 ms · el peine de 8 segmentos del botón en carga.
   Sólo se mueve `background-position`: una de las cuatro propiedades baratas. */
@keyframes comb-loop{
  from{ background-position: 100% 0, 0 0 }
  to  { background-position: 0    0, 0 0 }
}

/* trace-draw · 520 ms · la polilínea a 45°/90° se dibuja terminando en vía.
   `--len` lo escribe el JS con getTotalLength(): sin eso, el guion no cuadra
   y la línea aparece a trompicones. */
@keyframes trace-draw{
  to{ stroke-dashoffset: 0 }
}

/* stamp-impact · 200 ms · EL DICTAMEN FAVORABLE.
   Llega desde 1,7× rotado −8°, aterriza en −3°, y a los 90 ms la caja tiembla
   3 px en pasos. Después el tinte sangra 900 ms (`stamp-bleed`, en §6.9). */
@keyframes stamp-impact{
  0%  { transform: scale(1.7) rotate(-8deg); opacity: 0 }
  45% { transform: scale(1.06) rotate(-4deg); opacity: 1 }
  55% { transform: scale(1) rotate(-3deg) translate(3px, -2px) }
  70% { transform: scale(1) rotate(-3deg) translate(-3px, 2px) }
  85% { transform: scale(1) rotate(-3deg) translate(2px, 1px) }
  100%{ transform: scale(1) rotate(-3deg); opacity: 1 }
}

/* reject-shear · 180 ms · LA DEVOLUCIÓN. TRES SALTOS Y UNA SOLA ITERACIÓN.
   `steps(1)` para que no interpole: el corte tiene que ser un fallo de señal,
   no una transición. Máximo 1 por devolución (§7.4).

   PRESUPUESTO DE GLITCH, MEDIDO. La versión anterior tenía siete paradas y
   `3` iteraciones: seis cortes duros por ciclo × 3 = 18 destellos en 540 ms,
   ~33 Hz, sobre `.doc` — que en master/review.html es el expediente entero
   renderizado, el 60% del ancho y toda la altura. §7.4 fija «ningún efecto
   supera 3 destellos por segundo» y «máximo el 25% del viewport», y 3
   destellos por segundo es además el umbral de WCAG 2.3.1: fotosensibilidad,
   no estética. Lo único que lo salvaba era `prefers-reduced-motion`, que no
   todo el mundo tiene puesto.
   §7.2 especifica «cinco bandas, TRES SALTOS», no tres repeticiones de seis.
   Así queda: 3 destellos en un único disparo de 180 ms. */
@keyframes reject-shear{
  0%,  100%{ clip-path: inset(0);           transform: translateX(0) }
  33%      { clip-path: inset(38% 0 44% 0); transform: translateX(-3px) }
  66%      { clip-path: inset(62% 0 22% 0); transform: translateX(3px) }
}
.is-rejected{ animation: reject-shear 180ms steps(1) 1 both }

/* scramble · 40 ms por carácter · EL SCRAMBLE QUE DICE LA VERDAD.
   CSS pone el parpadeo; los CARACTERES los pone el JS, y no son ruido: son
   los valores realmente posibles de ese campo según el mod. Ver la Humanidad
   pasar por 42 → 38 → 30 → 12 → 5 antes de aterrizar en 40 enseña, sin una
   línea de texto, que por debajo de 5 el servidor aplica StatusDef 2972 ·
   CYBERPSICÓPATA.
   CONDICIÓN CRÍTICA: dispara SÓLO en `change` y en `blur`. NUNCA en `input`.
   Si dispara en `input`, repartir 55 puntos lo lanza 55 veces y el raíl de
   derivadas tiembla mientras se escribe. */
@keyframes scramble{
  0%  { opacity: 1;   transform: translateX(0) }
  50% { opacity: .55; transform: translateX(1px) }
  100%{ opacity: 1;   transform: translateX(0) }
}
.is-scrambling{ color: var(--accent-brt) }
.is-scrambling .ch{ animation: scramble 40ms var(--ez-lin) both }

/* declassify · 1400 ms · la barra de censura se retrae de izquierda a derecha
   mientras los caracteres se resuelven detrás. Se aplica sobre `.redact::after`
   por transición; el keyframe queda para dispararla sin cambio de clase. */
@keyframes declassify{
  from{ clip-path: inset(0 0 0 0) }
  to  { clip-path: inset(0 0 0 100%) }
}

/* error-nudge · 160 ms × 2 ciclos · ±4 px. El campo dice que no con la cabeza. */
@keyframes error-nudge{
  0%, 100%{ transform: translateX(0) }
  25%     { transform: translateX(-4px) }
  75%     { transform: translateX(4px) }
}

/* tear · 120 ms · tres bandas horizontales a ±6 px y vuelta. Imperceptible de
   uno en uno, por eso es el único glitch sin límite de uso por sesión (§7.4). */
@keyframes tear{
  0%  { clip-path: inset(0);           transform: translateX(0) }
  25% { clip-path: inset(0 0 66% 0);   transform: translateX(6px) }
  50% { clip-path: inset(38% 0 38% 0); transform: translateX(-6px) }
  75% { clip-path: inset(70% 0 0 0);   transform: translateX(6px) }
  100%{ clip-path: inset(0);           transform: translateX(0) }
}

/* sweep · 220 ms · barra de luz del 40% de ancho cruzando el botón primario. */
@keyframes sweep{
  from{ transform: translateX(-160%) }
  to  { transform: translateX(320%) }
}


/* ═══════════════════════════════════════════════════════════════════════════
   MOVIMIENTO REDUCIDO · §11.3

   LA CLAVE: LA TEXTURA NO ES MOVIMIENTO. El 80% de esta dirección está en la
   composición, la tipografía, el chaflán, la paleta y la voz escrita, y todo
   eso se conserva intacto. Aquí sólo se desactiva lo que se mueve, y cada
   pieza aterriza en su ESTADO FINAL, no en su estado inicial: el sello
   aparece rotado −3° y sangrado, simplemente no cae.

   tokens.css ya aplica la regla global de duración. Esto es lo que hace falta
   además para que ningún componente se quede a medias.

   LAS DOS GUARDAS DE `data-render` (ver la nota larga de tokens.css):
     A · dentro de la media query, `:root:not([data-render="completo"])`
     B · fuera, `:root[data-render]:not([data-render="completo"])`
   Sin B, elegir REDUCIDO o TEXTO PLANO en la cabecera dejaba el sello
   cayendo, el barrido del primario cruzando y la barra de censura
   retrayéndose. Sin A, quien tiene el sistema en reducido y elige COMPLETO
   seguía sin poder verlo. El cuerpo está duplicado a propósito: CSS no sabe
   compartir un bloque entre una condición de media query y una de atributo.
   ═══════════════════════════════════════════════════════════════════════════ */

/* A · preferencia del sistema, anulable con RENDER: COMPLETO */
@media (prefers-reduced-motion: reduce){

  :root:not([data-render="completo"]) .btn--exec::after,
  :root:not([data-render="completo"]) .btn--alert::after{ display: none }

  :root:not([data-render="completo"]) .mugshot__frame::before{ display: none }
  :root:not([data-render="completo"]) .mugshot__nosig{ animation: none; opacity: 1 }

  :root:not([data-render="completo"]) .comb:focus-within .comb__seg.is-live{ animation: none }
  :root:not([data-render="completo"]) .field:focus-within::after{ animation: none }   /* el cursor queda encendido */

  :root:not([data-render="completo"]) .osd__dot{ animation: none }

  /* El sello no cae: ya está puesto. Y el tinte ya ha calado. */
  :root:not([data-render="completo"]) .seal--stamp.is-hit{ animation: none; transform: rotate(-3deg) }
  :root:not([data-render="completo"]) .seal-ring.is-hit::before,
  :root:not([data-render="completo"]) .seal-ring:has(.is-hit)::before{ animation: none; opacity: .22 }
  :root:not([data-render="completo"]) .seal.is-changing{ animation: none }

  /* La barra de censura no se retrae: desaparece con un fundido de 140 ms. */
  :root:not([data-render="completo"]) .redact::after{ transition: opacity 140ms var(--ez-term) }
  :root:not([data-render="completo"]) .redact.is-open::after{ clip-path: inset(0); opacity: 0 }

  /* La línea guía aparece completa. */
  :root:not([data-render="completo"]) .marker__trace path{ animation: none; stroke-dashoffset: 0 }

  :root:not([data-render="completo"]) .is-rejected{ animation: none; outline: var(--bw-2) solid var(--danger) }

  /* El popover del selector aparece desplegado: `transform` a su estado final
     y el chaflán de §6.3 intacto. */
  :root:not([data-render="completo"]) .picker__pop{ animation: none; transform: none; opacity: 1 }
}

/* B · RENDER: REDUCIDO y RENDER: TEXTO PLANO, forzado desde la cabecera */
:root[data-render]:not([data-render="completo"]) .btn--exec::after,
:root[data-render]:not([data-render="completo"]) .btn--alert::after{ display: none }

:root[data-render]:not([data-render="completo"]) .mugshot__frame::before{ display: none }
:root[data-render]:not([data-render="completo"]) .mugshot__nosig{ animation: none; opacity: 1 }

:root[data-render]:not([data-render="completo"]) .comb:focus-within .comb__seg.is-live{ animation: none }
:root[data-render]:not([data-render="completo"]) .field:focus-within::after{ animation: none }

:root[data-render]:not([data-render="completo"]) .osd__dot{ animation: none }

:root[data-render]:not([data-render="completo"]) .seal--stamp.is-hit{ animation: none; transform: rotate(-3deg) }
:root[data-render]:not([data-render="completo"]) .seal-ring.is-hit::before,
:root[data-render]:not([data-render="completo"]) .seal-ring:has(.is-hit)::before{ animation: none; opacity: .22 }
:root[data-render]:not([data-render="completo"]) .seal.is-changing{ animation: none }

:root[data-render]:not([data-render="completo"]) .redact::after{ transition: opacity 140ms var(--ez-term) }
:root[data-render]:not([data-render="completo"]) .redact.is-open::after{ clip-path: inset(0); opacity: 0 }

:root[data-render]:not([data-render="completo"]) .marker__trace path{ animation: none; stroke-dashoffset: 0 }

:root[data-render]:not([data-render="completo"]) .is-rejected{ animation: none; outline: var(--bw-2) solid var(--danger) }

:root[data-render]:not([data-render="completo"]) .picker__pop{ animation: none; transform: none; opacity: 1 }


/* ═══════════════════════════════════════════════════════════════════════════
   MÓVIL · §11.4 · BIFURCACIÓN DURA EN 1024 px
   No se adapta el escritorio: se sirve otro componente.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1023px){

  /* Objetivo táctil ≥44 × 44 px en todo lo que se pulsa. */
  .btn{ width: 100%; min-width: 0 }
  .btn--sm{ height: 44px }

  /* Los +/− de característica a 48 px y las ranuras del peine más anchas:
     a 10 px una ranura no se acierta con el pulgar. */
  .comb{ --seg: 14px; --comb-h: 20px }
  .comb--thin{ --seg: 10px; --comb-h: 16px }

  /* EL PEINE NO SE PULSA EN TÁCTIL, SE NAVEGA. Ni con 14px de ranura se
     reparten 55 puntos con el pulgar, y el objetivo táctil de §11.4 es de
     44px: una ranura no puede cumplirlo sin dejar de ser una ranura. El gesto
     principal en móvil ya son los steppers de 48px del asistente. Los
     segmentos siguen siendo `<input type="checkbox">` reales y siguen
     recibiendo foco y teclado, que es lo que los hace accesibles según el
     contrato de §6.10: lo que se retira es sólo el puntero. */
  .comb--live .comb__seg{ pointer-events: none }

  /* CHIPS DE FILTRO A OBJETIVO TÁCTIL COMPLETO.
     Medido: `.tag` es un <button> de 85×22px, la mitad del mínimo de §11.4 y
     por debajo incluso de los 24px de WCAG 2.5.8. La caja crece a 44px en
     táctil y el aspa de cierre a 24, con las dos barras recogidas para que la
     × siga siendo pequeña. No se usa un ::after de área extendida —que es el
     truco habitual— porque `.tag` lleva `clip-path` para el chaflán, y
     clip-path recorta también los eventos de puntero: el área extendida
     moriría en el corte sin dejar rastro. */
  .tag{ height: 44px; padding-inline: var(--sp-3) }
  .tag__x{ width: 24px; height: 24px }
  .tag__x::before,
  .tag__x::after{ inset: 50% 6px auto 6px }

  /* TABLAS → PILAS DE DEFINICIÓN. Etiqueta arriba a 12 px, valor abajo en
     mono. Es otro componente, no la misma tabla estrujada.
     Las que deban seguir siendo tabla llevan `.reg-table--keep`. */
  .reg-table:not(.reg-table--keep) thead{
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .reg-table:not(.reg-table--keep) tbody tr{
    display: block;
    height: auto;
    padding: var(--sp-3) var(--sp-4);
    border-bottom: var(--bw-hair) solid var(--line);
  }
  .reg-table:not(.reg-table--keep) tbody td{
    display: flex;
    justify-content: space-between;
    gap: var(--sp-4);
    padding: 3px 0;
    border: 0;
    background: transparent !important;
  }
  /* La etiqueta la pone el `data-lbl` de la propia celda: sin duplicar
     cadenas en la plantilla y sin listas paralelas que se desincronizan. */
  .reg-table:not(.reg-table--keep) tbody td::before{
    content: attr(data-lbl);
    font-family: var(--ff-ui);
    font-weight: 600;
    font-size: var(--fs-2xs);
    letter-spacing: var(--tr-wider);
    text-transform: uppercase;
    color: var(--text-3);
  }
  .reg-scroll{ max-height: none }

  /* El aviso ocupa el ancho y sube: abajo está el teclado virtual. */
  .osd-stack{
    left: var(--sp-4); right: var(--sp-4);
    width: auto;
    bottom: calc(var(--bar-bottom) + var(--sp-4));
  }

  .inject__box{ width: 100%; max-height: 92dvh }

  /* El marcador ya degradó a lista en 900 px; aquí sólo se ajusta el raíl. */
  .marker__pin{ left: var(--sp-2) }
}
