/* WZAPI design system — wzapi.ai style
 *
 * Colour tokens carry a contract: every token used as TEXT clears WCAG AA (4.5:1 body,
 * 3:1 large) against every surface it actually lands on — --bg, --bg2, --card, --card2 and
 * the tinted chip/verb backgrounds. The ratios are computed, not eyeballed; the audit script
 * that measures them walks each panel in both themes and composites the real backgrounds.
 * Changing a token means re-running that audit, because the tints stack. */
:root{
  /* Sin esto el navegador pinta sus propios widgets —scrollbars, selects, el
     calendario de un date input, el autocompletado— en claro sobre un panel
     oscuro. No lo cubre ningún token nuestro: es el motor quien decide, y sólo
     mira esta propiedad. */
  color-scheme: dark;
  --bg:#0a0a0a; --bg2:#0d0f0d; --card:#111311; --card2:#161916;
  --border:#232623; --border-hi:#2e3a2e;
  --text:#f4f6f4; --muted:#9aa39a; --dim:#818a81;
  --green:#00d26a; --green-soft:rgba(0,210,106,.12); --green-line:rgba(0,210,106,.35);
  /* Solid green surfaces (buttons, logo, status dots) are a separate token from the green
     used as text: on light theme the text green has to go dark enough to read on white,
     which would starve the near-black label sitting on top of a green button. */
  --green-fill:#00d26a;
  /* Color del indicador de foco. Token propio porque tiene un contrato distinto al del verde
     de texto: debe medir >=3:1 (SC 1.4.11) contra TODA superficie sobre la que se dibuja un
     anillo de foco —--bg, --bg2, --card, --card2— y no contra el texto. Aquí es el verde vivo:
     8.8:1 sobre --card2, la peor de las cuatro en oscuro. */
  --focus:#00d26a;
  --red:#ff5c5c; --amber:#ffb020; --blue:#4da3ff;
  --red-soft:rgba(255,92,92,.14); --red-line:rgba(255,92,92,.4);
  --amber-soft:rgba(255,176,32,.14); --amber-line:rgba(255,176,32,.4);
  --blue-soft:rgba(77,163,255,.14);
  --glow:0 0 24px rgba(0,210,106,.45);
  --radius:16px;

  /* ------------------------------------------------------------- escalas ---
     Antes de la #800 esta hoja daba tokens de COLOR y nada más: cada tamaño y
     cada hueco eran un literal, 151 rem sueltos repartidos por seis hojas. Sin
     una escala no hay jerarquía —dos huecos que quieren decir lo mismo acaban
     midiendo 14 y 16px porque los escribió otra persona otro día— y no hay
     densidad: un panel de operación se lee por columnas, y una columna con el
     interlineado de un párrafo ocupa el doble sin decir más.

     ESPACIO en px y no en rem, a propósito. Es una REJILLA: su trabajo es que
     dos cosas separadas queden alineadas con una tercera. En rem, subir el
     tamaño de fuente del navegador estira la rejilla y desalinea justo lo que
     la rejilla existe para alinear. El texto sí va en rem —ése tiene que
     seguir al usuario—, y por eso son dos escalas y no una.

     Base 4px. Ocho peldaños llegan a 64px, que es el hueco más grande que hay
     en los paneles (la separación entre secciones de la portada). */
  --space-1:4px;  --space-2:8px;  --space-3:12px; --space-4:16px;
  --space-5:24px; --space-6:32px; --space-7:48px; --space-8:64px;

  /* TEXTO, con el interlineado ACOPLADO. Cada --text-* trae su --leading-*
     porque un tamaño sin su interlineado hereda el del body y se lee mal en los
     dos extremos: 1.55 sobre un rótulo de 11px lo separa de su dato, y sobre un
     título de 24px lo despega de su subtítulo. La forma de consumirlos sin
     poder olvidarse de la mitad es la clase .t-* de más abajo, que emite los
     dos juntos.

     Los valores no son inventados: salen de agrupar los rem que ya estaban
     sueltos en las seis hojas (~.7 en rótulos, ~.8 en chips, ~.88 en texto
     corriente, 1 en títulos de tarjeta, 1.5 en títulos de sección). --text-md
     es 15px, EXACTAMENTE el tamaño con el que ya venía el body, para que
     adoptar la escala no repinte ninguna pantalla. */
  --text-xs:.72rem;   --leading-xs:1.45;
  --text-sm:.8rem;    --leading-sm:1.5;
  --text-md:.9375rem; --leading-md:1.55;
  --text-lg:1.125rem; --leading-lg:1.4;
  --text-xl:1.5rem;   --leading-xl:1.25;
}
:root[data-theme="light"]{
  color-scheme: light;
  --bg:#f7f8f7; --bg2:#ffffff; --card:#ffffff; --card2:#f1f4f1;
  --border:#e3e7e3; --border-hi:#bfe8d2;
  --text:#101410; --muted:#5a635a; --dim:#656e65;
  /* The dark-theme accents are ~1.7:1 on a white card — the PUT badge was effectively
     invisible. These are the same hues taken down in luminance until the worst surface
     each one lands on clears 4.5:1. --green-fill keeps the vivid brand green. */
  --green:#0b7139; --green-soft:rgba(0,168,84,.10); --green-line:rgba(0,168,84,.35);
  --green-fill:#00a854;
  /* En claro el verde vivo (--green-fill) sólo da 3.12:1 sobre blanco: pasa por 0.12 y
     suspende en cuanto el campo se apoya en --card2. El anillo usa el verde oscurecido:
     6.1:1 sobre blanco y 5.3:1 sobre --card2. */
  --focus:#0b7139;
  --red:#c02626; --amber:#8a5a00; --blue:#1a5fbf;
  --red-soft:rgba(255,92,92,.14); --red-line:rgba(192,38,38,.45);
  --amber-soft:rgba(255,176,32,.14); --amber-line:rgba(138,90,0,.45);
  --blue-soft:rgba(77,163,255,.14);
  --glow:0 0 18px rgba(0,168,84,.25);
}
/* Capa de compatibilidad del reparto --green / --green-fill.
   Los paneles pintan todavía var(--green) como relleno sólido con una etiqueta casi negra
   (#04140b) encima; con el verde de TEXTO del tema claro eso cae a 3.09:1 y suspende AA en
   los botones de acción principales (confirmar de los modales, Execute de Swagger). Aquí no
   se sobrescribe ninguna declaración ajena —perdería contra su !important y contra el orden
   de carga—: se redefine la variable EN EL ELEMENTO, y una custom property declarada sobre el
   propio nodo gana siempre a la de :root. En tema oscuro los dos tokens valen lo mismo, así
   que este bloque es un no-op ahí.
   Sólo entran nodos cuyo texto es un color fijo, nunca var(--green), o no tienen texto: si el
   nodo usara el token también como color acabaría con verde vivo sobre verde vivo.
   Es temporal: en cuanto cada hoja pase a var(--green-fill) en esas líneas, esto sobra. */
.ui.positive.button, .ui.primary.button, .ui.green.button, .ui.blue.button,
.swagger-ui .btn.execute,
.pk-steps .step[data-state="done"] .n, .pk-tick, .pk-dot,
.big-logo, .ext-logo, .dot-ok{
  --green:var(--green-fill);
}
*{box-sizing:border-box}
/* Fomantic ships html{font-size:14px}, which silently rescales every rem in the design
   system on the two panels that load it (login, dashboard). wz.css is linked after
   semantic.min.css on both, so a plain rule wins without !important. */
html{font-size:16px}
html,body{margin:0;padding:0}
body{
  background:var(--bg); color:var(--text);
  /* Sale del token y no de un 15px literal: la escala describe lo que hay, y
     si algún día el peldaño base cambia, el body lo sigue solo en vez de
     quedarse como la única cosa de la hoja que mide otra cosa. */
  font:var(--text-md)/var(--leading-md) "Space Grotesk",system-ui,sans-serif;
  -webkit-font-smoothing:antialiased;
}

/* Las cinco clases de la escala de texto. Existen para que consumirla sea UNA
   declaración y no dos: quien escriba `class="t-sm"` no puede quedarse a medias
   con el tamaño y sin el interlineado, que es la única forma en que una escala
   acoplada se desacopla en la práctica. */
.t-xs{font-size:var(--text-xs);line-height:var(--leading-xs)}
.t-sm{font-size:var(--text-sm);line-height:var(--leading-sm)}
.t-md{font-size:var(--text-md);line-height:var(--leading-md)}
.t-lg{font-size:var(--text-lg);line-height:var(--leading-lg)}
.t-xl{font-size:var(--text-xl);line-height:var(--leading-xl)}
.mono{font-family:"JetBrains Mono",ui-monospace,monospace}
a{color:inherit;text-decoration:none}
button{font:inherit;cursor:pointer}

/* accesibilidad: oculto hasta que recibe foco por teclado (Issue #501 D18) */
.skip-link{
  position:absolute;left:8px;top:-48px;z-index:1000;
  background:var(--green-fill);color:#04140b;font-weight:700;
  padding:10px 16px;border-radius:8px;transition:top .15s ease;
}
.skip-link:focus{top:8px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* topbar */
.topbar{
  display:flex;align-items:center;gap:28px;
  padding:14px 28px;border-bottom:1px solid var(--border);
  position:sticky;top:0;background:color-mix(in srgb,var(--bg) 88%,transparent);
  backdrop-filter:blur(10px);z-index:50;
}
.wordmark{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.06em}
.wordmark .logo{width:26px;height:26px;border-radius:8px;background:var(--green-fill);display:grid;place-items:center;color:#04140b;font-weight:800;font-size:15px}
/* La nav nunca envuelve su texto: entre ~930 y ~1179px un enlace como «Dev
   Bench» se partía en dos líneas y la barra crecía de 63 a 75px, empujando los
   últimos enlaces fuera de vista. El nowrap sólo existía en @media(max-width:640).
   Con min-width:0 encoge en vez de sacar los botones de la derecha del viewport,
   y overflow-x:auto es la red de seguridad si aun así no cabe (Issue #500 D16). */
.topnav{display:flex;gap:22px;font-size:.88rem;color:var(--muted);margin-left:8px;
  white-space:nowrap;min-width:0;overflow-x:auto;scrollbar-width:none}
.topnav::-webkit-scrollbar{display:none}
.topnav a:hover{color:var(--text)}
/* Página actual: aria-current es la fuente de verdad (lectores de pantalla y
   estilo salen del mismo atributo, así que no pueden desincronizarse como
   pasaba con el style="color" suelto que cada página repetía a mano). */
.topnav a[aria-current="page"]{color:var(--text)}
.topbar .spacer{flex:1}
/* La píldora no se parte: apretada entre la nav y los botones, su texto («gateway
   host:puerto») caía a dos líneas y estiraba la barra a 75px. nowrap + no-shrink
   la mantienen de una línea; la que encoge y hace scroll es la nav (Issue #500 D16). */
.badge-live{display:inline-flex;align-items:center;gap:7px;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--green);background:var(--green-soft);border:1px solid var(--green-line);border-radius:999px;padding:4px 12px;white-space:nowrap;flex:0 0 auto}
.badge-live::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--green-fill);box-shadow:0 0 8px var(--green-fill)}
.theme-btn{width:34px;height:34px;border-radius:50%;border:1px solid var(--border);background:var(--card);color:var(--muted);display:grid;place-items:center}
.theme-btn:hover{color:var(--text);border-color:var(--border-hi)}

/* layout */
.wrap{max-width:1180px;margin:0 auto;padding:0 28px}
.section{padding:44px 0}
.eyebrow{display:inline-flex;align-items:center;gap:8px;font-size:.7rem;letter-spacing:.18em;text-transform:uppercase;color:var(--green);background:var(--green-soft);border:1px solid var(--green-line);border-radius:999px;padding:4px 12px;margin-bottom:14px}
.eyebrow::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--green-fill)}
h1.display{font-size:clamp(2rem,4.5vw,3.2rem);font-weight:700;letter-spacing:-.02em;line-height:1.05;margin:0 0 10px}
h1.display .glow{color:var(--green);text-shadow:var(--glow)}
.lede{color:var(--muted);max-width:640px;margin:0 0 24px}
h2.sec{font-size:1.5rem;font-weight:700;letter-spacing:-.01em;margin:0 0 6px}
.sub{color:var(--muted);font-size:.92rem;margin:0 0 22px}

/* cards */
.grid{display:grid;gap:14px}
.g2{grid-template-columns:repeat(2,1fr)} .g3{grid-template-columns:repeat(3,1fr)} .g4{grid-template-columns:repeat(4,1fr)}
@media(max-width:900px){.g3,.g4{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.g2,.g3,.g4{grid-template-columns:1fr}}
.card{
  background:var(--card);border:1px solid var(--border);border-radius:var(--radius);
  padding:20px;position:relative;transition:border-color .2s, box-shadow .2s, transform .2s;
}
.card:hover{border-color:var(--green-line);box-shadow:0 0 0 1px var(--green-line), 0 8px 30px rgba(0,0,0,.35)}
.card .ic{width:38px;height:38px;border-radius:10px;background:var(--green-soft);border:1px solid var(--green-line);display:grid;place-items:center;color:var(--green);margin-bottom:12px}
.card h3{margin:0 0 4px;font-size:1rem}
.card .route{font-size:.76rem;color:var(--green)}
.card p{margin:8px 0 0;color:var(--muted);font-size:.86rem}
.card .go{position:absolute;top:18px;right:18px;font-size:.78rem;color:var(--dim)}
.card:hover .go{color:var(--green)}

/* stat cards */
.stat{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px 18px;text-align:center}
.stat b{display:block;font-size:1.6rem;letter-spacing:-.02em}
.stat span{font-size:.74rem;color:var(--muted)}
.stat .up{color:var(--green)}

/* buttons */
.btn{display:inline-flex;align-items:center;gap:8px;border-radius:999px;padding:10px 22px;font-weight:600;font-size:.9rem;border:1px solid transparent}
.btn-green{background:var(--green-fill);color:#04140b}
.btn-green:hover{filter:brightness(1.08);box-shadow:var(--glow)}
.btn-ghost{background:transparent;color:var(--text);border-color:var(--border)}
.btn-ghost:hover{border-color:var(--border-hi)}
.btn-red{background:transparent;color:var(--red);border-color:var(--red-line)}
.btn-sm{padding:7px 16px;font-size:.82rem}

/* chips + pills */
.chip{display:inline-flex;align-items:center;gap:6px;background:var(--card2);border:1px solid var(--border);border-radius:999px;padding:3px 12px;font-size:.78rem;color:var(--muted)}
.chip.on{color:var(--green);border-color:var(--green-line);background:var(--green-soft)}
.verb{font-size:.68rem;font-weight:700;letter-spacing:.06em;border-radius:6px;padding:2px 8px}
.verb.get{background:var(--blue-soft);color:var(--blue)}
.verb.post{background:var(--green-soft);color:var(--green)}
.verb.put{background:var(--amber-soft);color:var(--amber)}
.verb.del{background:var(--red-soft);color:var(--red)}

/* forms */
label.f{display:block;font-size:.78rem;color:var(--muted);margin:0 0 6px;letter-spacing:.04em}
.input,select.input,textarea.input{
  width:100%;background:var(--bg2);border:1px solid var(--border);border-radius:10px;
  color:var(--text);padding:11px 14px;font:inherit;
}
/* Indicador de foco. El borde verde translúcido + el halo NO son el indicador: compuestos
   sobre el campo miden 1.49:1 en claro y 2.10:1 en oscuro, y SC 1.4.11 exige 3:1. Se
   conservan como decoración y el indicador real es el outline de 2px en --focus, que sí
   despeja 3:1 contra todas las superficies donde cae un campo. El outline NO se anula
   (antes había un outline:none en la regla base): con outline-offset el anillo queda fuera
   del borde redondeado en vez de recortarse contra él. */
.input:focus,select.input:focus,textarea.input:focus{
  border-color:var(--green-line);box-shadow:0 0 0 3px var(--green-soft);
  outline:2px solid var(--focus);outline-offset:2px;
}
textarea.input{resize:vertical;min-height:90px}

/* tables */
table.tbl{width:100%;border-collapse:collapse;font-size:.86rem}
.tbl th{font-size:.7rem;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);text-align:left;padding:10px 14px;border-bottom:1px solid var(--border)}
.tbl td{padding:12px 14px;border-bottom:1px solid var(--border)}
.tbl tr:hover td{background:var(--card2)}

/* log console */
.console{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:14px;font-family:"JetBrains Mono",monospace;font-size:.78rem;line-height:1.7;max-height:260px;overflow:auto}
/* .t and .err never appear in any panel's markup — dev-bench.js writes them into the
   console at runtime, so a DOM sweep alone would wrongly flag them as dead. */
.console .t{color:var(--dim)} .console .k{color:var(--green)} .console .err{color:var(--red)}

/* stepper */
.step{display:flex;gap:16px;padding:18px 0;border-bottom:1px solid var(--border)}
.step:last-child{border-bottom:0}
.step .n{flex:none;width:34px;height:34px;border-radius:50%;background:var(--green-soft);border:1px solid var(--green-line);color:var(--green);display:grid;place-items:center;font-weight:700;font-family:"JetBrains Mono",monospace;font-size:.85rem}
.step h4{margin:2px 0 4px;font-size:.95rem}
.step p{margin:0;color:var(--muted);font-size:.85rem}

/* misc */
.note{border:1px solid var(--green-line);background:var(--green-soft);border-radius:12px;padding:14px 18px;font-size:.88rem}
.note.warn{border-color:var(--amber-line);background:rgba(255,176,32,.08)}
.divider{height:1px;background:var(--border);margin:34px 0}
.foot{border-top:1px solid var(--border);margin-top:50px;padding:22px 0;color:var(--dim);font-size:.8rem;display:flex;justify-content:space-between;align-items:center}
.dot-g{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--green-fill);box-shadow:0 0 8px var(--green-fill);margin-right:6px}
.kv{display:flex;justify-content:space-between;padding:9px 0;border-bottom:1px dashed var(--border);font-size:.85rem}
.kv:last-child{border-bottom:0}
.kv span{color:var(--muted)} .kv b{font-weight:500}

/* narrow viewports
   The topbar was a single nowrap row: at 360px the language button ended at x=386 and put
   the whole document into horizontal scroll on every panel that did not patch it locally.
   Fixing it here rather than page by page is the point — /api, /login and /api/swagger had
   each grown their own copy of the workaround.
   Touch targets follow the same reasoning: .btn-sm/.theme-btn/.topnav links measured 22-36px
   tall, under the 44px minimum, and only the phone widths need the bigger boxes — the
   desktop sizing above is untouched. */
@media(max-width:640px){
  .topbar{flex-wrap:wrap;gap:10px 16px;padding:12px 16px}
  /* order + fila completa para que el nav tenga su propia línea y ahí desborde en horizontal.
     Las dos alternativas midieron peor: dejarlo encoger en la primera fila lo colapsa a 0px y
     los enlaces quedan inalcanzables, y dejarlo envolver apila un enlace por línea y sube la
     barra a 123px. Aquí no se declara flex-wrap a propósito — /extension declara el suyo y
     conserva su rejilla de píldoras.
     Va sobre `.topbar .topnav` porque /login, /api y /api/swagger declaran su propio
     `.topnav{flex:1 1 0;min-width:0}` en una hoja que carga DESPUÉS de ésta: con la misma
     especificidad ganaban ellos, el flex-basis 0 anulaba el width:100% y el nav se quedaba
     aplastado contra el borde derecho de la primera fila. */
  .topbar .topnav{order:10;flex:0 0 100%;width:100%;overflow-x:auto;white-space:nowrap;margin-left:0;scrollbar-width:none}
  .topnav::-webkit-scrollbar{display:none}
  /* Both of these buy back the first row: the spacer would otherwise strand the controls on
     a line of their own, and the :18080 badge is 133px of decoration that pushes the language
     button off a 360px row. Dropping them takes the bar from 123px to 69px. */
  .topbar .spacer{display:none}
  .topbar .badge-live{display:none}
  .wrap{padding:0 18px}

  /* El orden de estas tres declaraciones importa. Un flex item arranca con min-width:auto,
     cuyo suelo es su propio contenido, así que un min-width:44px suelto lo BAJA en vez de
     subirlo: en los paneles que dejan encoger el nav, "Dashboard" caía de 73px a 44px y con
     justify-content:center se recortaba por los dos lados ("ıshboard"). flex:none es lo que
     lo arregla —sin encogimiento no hay recorte, venga de donde venga el contenedor— y sólo
     por ir detrás de él el min-width vuelve a ser un suelo de verdad, para los paneles que
     re-declaran el padding del enlace (landing, /extension) y dejaban "API" en 39px.
     Si alguien quita el flex:none, hay que quitar también el min-width. */
  .topnav{gap:4px}
  .topnav a{display:inline-flex;align-items:center;justify-content:center;flex:none;min-height:44px;min-width:44px;padding:0 12px}
  .btn,.btn-sm{min-height:44px}
  /* flex:none or the topbar shrinks these back under 44px before it decides to wrap;
     min-width because three panels re-style the language pill with width:auto. */
  button.theme-btn{width:44px;min-width:44px;height:44px;flex:none}
  /* A plain .chip is a status label, not a control — only give the 44px box to the ones
     that are actually tappable, or every status row on mobile turns into a wall. */
  button.chip{min-height:44px}
  .chip{min-height:30px}

  /* El logotipo es un enlace a la portada y medía 85x26: alto de sobra en la fila —la
     barra ya mide 44px por el botón de tema— pero el área tocable era sólo la altura del
     cuadro verde. min-height lo estira hasta la fila sin cambiar nada de lo que se ve. */
  .wordmark{min-height:44px}
  /* Las pestañas de /dashboard/admin.html se quedaban en 43px, un píxel por debajo del
     mínimo, según qué pestaña estuviera activa (la activa gana 1px de borde). */
  button.tab{min-height:44px}
  /* Los botones de icono del Dev Bench. Uno de ellos, el de texto aleatorio, se declara
     en la página con width:30px;height:26px; min-* gana a width/height sea cual sea la
     especificidad, porque son propiedades distintas y el mínimo se aplica después. */
  button.icon-btn{min-width:44px;min-height:44px}

  /* Aire de la tabla en pantalla estrecha: 14px por lado en cada celda son 28px de ancho
     por columna que sólo sirven para alargar el desplazamiento lateral. */
  .tbl th{padding:9px 10px}
  .tbl td{padding:10px 10px}

  /* El pie es justify-content:space-between con dos bloques: a 360px se aprietan uno
     contra otro. Apilados se leen. */
  .foot{flex-direction:column;align-items:flex-start;gap:8px}

  /* Un par clave/valor con un JID largo a la derecha empujaba el valor contra la clave.
     min-width:0 es lo que deja que el valor encoja, y anywhere lo que le deja partirse. */
  .kv{gap:14px}
  .kv b{text-align:right;overflow-wrap:anywhere;min-width:0}
}

/* Las tarjetas de cifra son estrechas por definición: apilarlas de una en una a 360px
   convierte cuatro números en una pantalla entera de scroll. Dos por fila caben de sobra.
   El :has() limita la regla a las rejillas que de verdad contienen .stat — una .g4 de
   tarjetas normales sigue apilándose como antes. */
@media(max-width:600px){
  .grid.g4:has(> .stat){grid-template-columns:repeat(2,1fr)}
}

/* Aquí NO va una red del tipo `html,body{overflow-x:clip}`. Se probó y se descartó con
   la medida delante: esta hoja la cargan trece páginas, no sólo el panel, y
   /api/swagger.html desborda hoy 207px a 360px de ancho (scrollWidth 567). Recortar
   desde aquí no arreglaría ese desbordamiento: lo volvería inalcanzable. Las seis
   páginas del panel miden cero desbordamiento SIN ninguna red —lo aguanta la estructura,
   cada cosa ancha dentro de su propio contenedor desplazable—, así que una red global
   sólo serviría para esconder la próxima regresión en vez de enseñarla. */

/* ---------------------------------------------------------------------------
   Sombras de desplazamiento
   ---------------------------------------------------------------------------
   Un contenedor que se desplaza de lado sin ninguna señal no parece cortado:
   parece que la tabla se acaba justo ahí. Estas sombras dicen "hay más" y se
   apagan solas al llegar al extremo, sin una línea de JavaScript.

   Cómo: cuatro capas. Las dos primeras son TAPAS pintadas del color de la
   superficie, con `background-attachment: local`, así que viajan con el
   contenido; las dos últimas son las SOMBRAS, con `background-attachment:
   scroll`, así que quedan clavadas a los bordes del contenedor. En reposo cada
   tapa está encima de su sombra y no se ve nada; al desplazar, la tapa se va y
   la sombra aparece. Si no hay nada que desplazar las dos tapas están puestas y
   no se ve ninguna sombra, que es exactamente lo que se quiere.

   El color sale de tokens y no de literales a propósito: una sombra con un
   gradiente a blanco fijo se ve como una mancha en el tema oscuro. --text es
   casi negro en claro y casi blanco en oscuro, así que el borde se lee en los
   dos temas con la misma declaración. La tapa usa --wz-superficie, que cada
   contenedor ajusta al fondo sobre el que se apoya de verdad. */
.tbl-wrap,.tbl-scroll,.pg-tblwrap,.topbar .topnav,.console{
  /* Los contenedores de tabla viven dentro de una .card. Quien no, lo redefine. */
  --wz-superficie:var(--card);
  --wz-sombra:color-mix(in srgb,var(--text) 22%,transparent);
  background-image:
    linear-gradient(to right,var(--wz-superficie),transparent),
    linear-gradient(to left,var(--wz-superficie),transparent),
    radial-gradient(farthest-side at 0 50%,var(--wz-sombra),transparent),
    radial-gradient(farthest-side at 100% 50%,var(--wz-sombra),transparent);
  background-position:left center,right center,left center,right center;
  background-repeat:no-repeat;
  background-size:26px 100%,26px 100%,13px 100%,13px 100%;
  background-attachment:local,local,scroll,scroll;
  /* Que arrastrar una tabla de lado no dispare el gesto de "atrás" del navegador. */
  overscroll-behavior-x:contain;
}
/* La barra de navegación no se apoya en una tarjeta sino en la barra superior, que es
   --bg al 88%; la tapa tiene que ser de ese color o se ve el parche. */
.topbar .topnav{--wz-superficie:var(--bg)}
/* La consola se apoya en --bg2 y ya traía su fondo como atajo `background:`, que habría
   borrado estas capas: aquí se separa en color e imagen. */
.console{--wz-superficie:var(--bg2);background-color:var(--bg2)}

/* Cifras que se comparan en columna: sin tabular-nums cada dígito trae su propio ancho y
   los millares bailan de fila en fila. */
.tbl td.num,.tbl th.num,.tbl td.mono,.stat b,.kv b,.pager,.console,.mono{
  font-variant-numeric:tabular-nums;
}

/* ------------------------------------------------------------- confirmación ---
   El diálogo compartido de wz-confirm.js (#680). Vive aquí, y no en el <style>
   de una página, porque lo usan el panel de administración y el dashboard de
   sesión: una copia por página es exactamente cómo se separan dos diálogos que
   tenían que verse igual. Es un <dialog> nativo — se lleva el foco, se cierra
   con Escape y bloquea el resto de la página sin bloquear el hilo, que es lo
   que hacía window.confirm(). */
.hidden{display:none !important}
dialog.confirm{border:1px solid var(--red-line);background:var(--card);color:var(--text);border-radius:16px;padding:26px;max-width:560px;width:calc(100% - 32px)}
dialog.confirm::backdrop{background:rgba(0,0,0,.6)}
dialog.confirm h3{margin:0 0 10px;font-size:1.05rem}
dialog.confirm p{color:var(--muted);font-size:.88rem;margin:0 0 14px;line-height:1.6}
dialog.confirm .target{font-family:"JetBrains Mono",monospace;color:var(--red);font-size:.95rem}
dialog.confirm .row{display:flex;gap:10px;justify-content:flex-end;margin-top:18px;flex-wrap:wrap}

/* ------------------------------------------------------------------ aviso ---
   El toast compartido de wz-toast.js (#800). Vive aquí por la misma razón que
   el diálogo de arriba: lo usan el dashboard de sesión, el panel de admin y
   /login, y una copia por página es exactamente cómo se separan tres avisos que
   tenían que verse igual.

   Reemplaza a tres mecanismos. El toast de Fomantic del dashboard clásico —que
   además obligaba a una capa de veinte líneas de !important en app.css para
   repintar el componente ajeno—, el banner inline de NODO ÚNICO del panel de
   admin —donde el segundo aviso pisaba al primero y el operador se quedaba sin
   saber cuál estaba leyendo—, y la mezcla de los dos que había en /login.

   Todo lo que mide algo sale de la escala: es la primera hoja de la casa que se
   escribe entera con --space-* y --text-*, y por eso el trinquete de
   test/js/sistema-diseno.test.mjs no le sube un solo rem suelto. */
.wz-toasts{
  position:fixed;top:var(--space-4);left:50%;transform:translateX(-50%);
  z-index:200;display:flex;flex-direction:column;gap:var(--space-2);
  width:min(440px,calc(100% - var(--space-6)));
  /* La región no intercepta clics: sólo los avisos que hay dentro. Un
     contenedor de 440px fijo sobre la barra superior se comería el botón de
     tema cuando no hay ningún aviso en pantalla. */
  pointer-events:none;
}
.wz-toast{
  pointer-events:auto;
  display:flex;align-items:flex-start;gap:var(--space-3);
  background:var(--card);color:var(--text);
  border:1px solid var(--border);border-left-width:3px;
  border-radius:12px;padding:var(--space-3) var(--space-4);
  box-shadow:0 12px 40px rgba(0,0,0,.35);
  font-size:var(--text-sm);line-height:var(--leading-sm);
  animation:wz-toast-in .18s ease-out;
}
/* El color NO es la única señal: cada tipo lleva además su glifo, que lo pone
   el JS. Un aviso que sólo se distingue por el color deja fuera a quien no
   distingue rojo de verde, que es 1 de cada 12 hombres (SC 1.4.1). */
.wz-toast__ic{flex:none;font-weight:700;font-size:var(--text-md);line-height:var(--leading-sm)}
.wz-toast__msg{flex:1;min-width:0;overflow-wrap:anywhere}
.wz-toast__x{
  flex:none;background:transparent;border:0;color:var(--dim);
  font-size:var(--text-md);line-height:1;padding:0 var(--space-1);
}
.wz-toast__x:hover{color:var(--text)}
.wz-toast--ok{border-left-color:var(--green-line)}
.wz-toast--ok .wz-toast__ic{color:var(--green)}
.wz-toast--err{border-left-color:var(--red-line)}
.wz-toast--err .wz-toast__ic{color:var(--red)}
/* Ni verde ni rojo: la petición salió bien y el resultado NO es lo que se
   pedía. Existe porque decir en verde «sesión arrancada» de una sesión que se
   quedó pidiendo QR es la mentira de la #783. */
.wz-toast--warn{border-left-color:var(--amber-line)}
.wz-toast--warn .wz-toast__ic{color:var(--amber)}
.wz-toast--info{border-left-color:var(--border-hi)}
.wz-toast--info .wz-toast__ic{color:var(--muted)}
@keyframes wz-toast-in{from{opacity:0;transform:translateY(calc(var(--space-2) * -1))}to{opacity:1;transform:none}}
/* Un aviso que entra deslizándose es decoración; el aviso es el texto. Quien
   pidió menos movimiento lo recibe igual, quieto. */
@media(prefers-reduced-motion:reduce){.wz-toast{animation:none}}
@media(max-width:640px){
  .wz-toasts{top:var(--space-2);width:calc(100% - var(--space-4))}
  /* Cerrar es un control táctil como cualquier otro (SC 2.5.8). */
  .wz-toast__x{min-width:44px;min-height:44px}
}
