/* Estilos del panel admin-ui. Plano, sin framework (fiel a la simplicidad de
   ClusterODM). Servido estático bajo /static; permitido por la CSP 'self'.

   Reglas de diseño (docs/plans/usabilidad-panel-plan.md), vigiladas por tests:
   - Color: TODO color es un token de los bloques de abajo; las reglas solo usan
     var(--…) o color-mix(…, var(--…), transparent) para los tintes, que así
     siguen al tema solos (test/theme-css.test.ts). Los contrastes WCAG de cada
     par (texto ≥ 4.5, bordes y aristas ≥ 3) los calcula test/contrast.test.ts
     desde estos mismos tokens: si cambias un color, ese test dice si sigue
     siendo legible.
   - Espaciado y radios: escala de 4/8 px (--space-*, --radius-*),
     test/ui-css.test.ts.
   - Mobile-first: las reglas base son las del móvil; las @media (min-width)
     añaden layout cuando hay sitio. El body nunca se desplaza en horizontal:
     tablas y grafo tienen su propio contenedor con scroll.
   - Objetivos táctiles ≥ 44 px (botones, navegación, pestañas, campos).

   Tema: data-theme en <html> (lo pone /static/theme.js desde localStorage) o,
   sin él, prefers-color-scheme. Sin preferencia del sistema, oscuro. */

/* Tema oscuro (por defecto). */
:root {
  color-scheme: dark;
  --bg: #0f1419;
  --panel: #1a2129;
  --panel-2: #222b35;
  --text: #e6edf3;
  --muted: #9aa5b1;
  --stroke-strong: #6b7785;
  --stroke-weak: #2d3742;
  --hover-fill: #343f4b;
  --link: #60a5fa;
  --accent: #3b82f6;
  --danger: #f87171;
  --ok: #22c55e;
  --warn: #f59e0b;
  --do: #06b6d4;
  --on-accent: #0f1419;
  --accent-text: #93c5fd;
  --danger-text: #fca5a5;
  --ok-text: #22c55e;
  --warn-text: #f59e0b;
  --log-bg: #0a0e12;
  --log-ln: #7d8894;
  --log-hover: rgba(255,255,255,.03);
  --edge: #6a7683;
  --edge-error: #d45555;
  --edge-recovery: rgba(245,158,11,.55);
}

/* Tema claro. Los dos bloques son IDÉNTICOS (CSS sin preprocesador no permite
   compartirlos): el primero sigue al sistema salvo que se fuerce oscuro, el
   segundo es la elección explícita del operador. test/theme-css.test.ts falla
   si divergen o si falta algún token del oscuro. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --bg: #f4f6f8;
    --panel: #ffffff;
    --panel-2: #eaeef2;
    --text: #1f2328;
    --muted: #4f5760;
    --stroke-strong: #7a838d;
    --stroke-weak: #d0d7de;
    --hover-fill: #dde3e9;
    --link: #2563eb;
    --accent: #2563eb;
    --danger: #b91c1c;
    --ok: #15803d;
    --warn: #b45309;
    --do: #0e7490;
    --on-accent: #ffffff;
    --accent-text: #1d4ed8;
    --danger-text: #991b1b;
    --ok-text: #166534;
    --warn-text: #92400e;
    --log-bg: #ffffff;
    --log-ln: #6e7781;
    --log-hover: rgba(0,0,0,.04);
    --edge: #8c959f;
    --edge-error: #dc2626;
    --edge-recovery: #c2410c;
  }
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f4f6f8;
  --panel: #ffffff;
  --panel-2: #eaeef2;
  --text: #1f2328;
  --muted: #4f5760;
  --stroke-strong: #7a838d;
  --stroke-weak: #d0d7de;
  --hover-fill: #dde3e9;
  --link: #2563eb;
  --accent: #2563eb;
  --danger: #b91c1c;
  --ok: #15803d;
  --warn: #b45309;
  --do: #0e7490;
  --on-accent: #ffffff;
  --accent-text: #1d4ed8;
  --danger-text: #991b1b;
  --ok-text: #166534;
  --warn-text: #92400e;
  --log-bg: #ffffff;
  --log-ln: #6e7781;
  --log-hover: rgba(0,0,0,.04);
  --edge: #8c959f;
  --edge-error: #dc2626;
  --edge-recovery: #c2410c;
}

/* Escala de espaciado y radios. No depende del tema, así que va en su propio
   :root DESPUÉS de los de color (theme-css.test.ts localiza el bloque oscuro
   por su color-scheme y no debe mezclar estos tokens). */
:root {
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --radius-s: 4px;
  --radius-m: 8px;
  --radius-l: 12px;
  --radius-pill: 999px;
}

/* --- Base ------------------------------------------------------------------ */
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 16px;
  line-height: 1.5;
}
/* Los enlaces van subrayados: el color solo no basta para distinguirlos del
   texto (WCAG 1.4.1). Navegación, pestañas y botones-enlace lo quitan porque su
   forma ya indica que son pulsables. */
a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
a:hover { text-decoration-thickness: 2px; }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-s); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .875em; background: var(--panel-2); padding: 1px 5px; border-radius: var(--radius-s); }

h1 { font-size: 24px; line-height: 1.25; margin: 0 0 var(--space-4); }
h2 { font-size: 18px; line-height: 1.3; margin: var(--space-5) 0 var(--space-3); }
.muted { color: var(--muted); }
.empty { padding: var(--space-5) 0; }
.danger { color: var(--danger); font-weight: 700; }

/* --- Barra superior ----------------------------------------------------------
   Móvil: fila 1 = marca · selector de tema · Salir; fila 2 = navegación, que se
   desplaza en horizontal dentro de su fila (con un degradado en el borde que
   sugiere que hay más). Desde 768 px todo va en una fila. El orden del HTML es
   marca → nav → selector → Salir (orden de lectura); `order` solo lo recoloca. */
.topbar {
  display: flex; flex-wrap: wrap; align-items: center;
  column-gap: var(--space-3);
  padding: var(--space-2) var(--space-4) 0;
  background: var(--panel); border-bottom: 1px solid var(--stroke-weak);
}
.brand { font-weight: 700; margin-right: auto; }
.nav {
  order: 3; flex-basis: 100%;
  display: flex; gap: var(--space-1);
  overflow-x: auto;
  margin: 0 calc(-1 * var(--space-4));
  padding: var(--space-1) var(--space-4) var(--space-2);
  mask-image: linear-gradient(to right, black calc(100% - var(--space-6)), transparent);
}
.nav a {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 var(--space-3); border-radius: var(--radius-m);
  color: var(--muted); text-decoration: none; white-space: nowrap;
}
.nav a[aria-current="page"], .nav a:hover { color: var(--text); background: var(--panel-2); }
.nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.logout-form { display: flex; align-items: center; gap: var(--space-3); }
.user-email { display: none; color: var(--muted); font-size: 14px; }
/* En móvil, fila 1 = marca + tema + Salir: a 360 px solo caben sin el sufijo de
   la marca y con el selector más estrecho. */
.brand-suffix { display: none; }

/* Selector de tema: el servidor pinta las tres etiquetas y la CSS muestra solo
   la del estado actual, así el texto es correcto desde la primera pintura (sin
   JS que lo sincronice) y el nombre accesible del botón es el texto visible. */
.theme-toggle { min-height: 44px; }
/* Con .topbar delante: .btn se declara más abajo con la misma especificidad y
   pisaría relleno, tamaño y color. */
.topbar .theme-toggle { font-size: 14px; padding: 0 var(--space-2); color: var(--muted); }
.topbar .theme-toggle:hover { color: var(--text); }
.theme-opt { display: none; }
:root:not([data-theme]) .theme-opt--auto,
:root[data-theme="light"] .theme-opt--light,
:root[data-theme="dark"] .theme-opt--dark { display: inline; }

.content { padding: var(--space-4); max-width: 1100px; margin: 0 auto; }

@media (min-width: 640px) {
  .user-email { display: inline; }
  .brand-suffix { display: inline; }
  .topbar .theme-toggle { padding: 0 var(--space-3); }
}
@media (min-width: 768px) {
  .topbar { flex-wrap: nowrap; column-gap: var(--space-5); padding: var(--space-1) var(--space-5); }
  .brand { margin-right: 0; }
  .nav { order: 0; flex: 1; flex-basis: auto; margin: 0; padding: 0; mask-image: none; }
  .content { padding: var(--space-5); }
}

/* --- Pestañas (vista de /nodes) ---------------------------------------------
   La activa la marca aria-current="page" (lectores de pantalla) y el estilo
   cuelga de ese atributo, no de una clase: no pueden desincronizarse. */
.tabs {
  display: flex; gap: var(--space-1);
  overflow-x: auto;
  border-bottom: 1px solid var(--stroke-weak);
  margin-bottom: var(--space-4);
}
.tabs a {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 var(--space-4); margin-bottom: -1px;
  color: var(--muted); text-decoration: none; white-space: nowrap;
  border-bottom: 2px solid transparent;
}
.tabs a:hover { color: var(--text); }
.tabs a[aria-current="page"] { color: var(--text); font-weight: 700; border-bottom-color: var(--accent); }
.tabs a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* --- Tarjetas y detalle ---------------------------------------------------- */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: var(--space-4); }
.card { background: var(--panel); border: 1px solid var(--stroke-weak); border-radius: var(--radius-l); padding: var(--space-4); }
.card h2 { margin: 0 0 var(--space-2); font-size: 14px; color: var(--muted); }
.big { font-size: 32px; line-height: 1.2; font-weight: 700; margin: 0; }
.kv { list-style: none; padding: 0; margin: var(--space-2) 0 0; }
.kv li { display: flex; justify-content: space-between; gap: var(--space-3); padding: var(--space-1) 0; border-bottom: 1px solid var(--stroke-weak); }

.detail-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: var(--space-3); background: var(--panel); border: 1px solid var(--stroke-weak);
  border-radius: var(--radius-l); padding: var(--space-4); margin-bottom: var(--space-4);
}
.detail-grid > div { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); min-width: 0; }
.detail-grid .muted { font-size: 14px; }
.detail-grid code { overflow-wrap: anywhere; }

/* --- Tablas ----------------------------------------------------------------
   Cada tabla vive en un .table-scroll: en móvil se desplaza ella, no el body.
   14 px en las celdas: el panel es denso de datos y 16 px alarga las tablas sin
   ganar legibilidad (D-UI-1). */
.table-scroll { overflow-x: auto; margin: var(--space-3) 0; }
.table { width: 100%; border-collapse: collapse; font-size: 14px; line-height: 1.43; }
.table th, .table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--stroke-weak); }
.table th { color: var(--muted); font-weight: 700; white-space: nowrap; }
.table tbody tr:hover { background: var(--panel); }

.badge { display: inline-block; padding: 2px 8px; border-radius: var(--radius-pill); font-size: 13px; font-weight: 700; white-space: nowrap; }
.badge-queued { background: color-mix(in srgb, var(--warn) 20%, transparent); color: var(--warn-text); }
.badge-running { background: color-mix(in srgb, var(--accent) 20%, transparent); color: var(--accent-text); }
.badge-completed { background: color-mix(in srgb, var(--ok) 20%, transparent); color: var(--ok-text); }
.badge-failed { background: color-mix(in srgb, var(--danger) 20%, transparent); color: var(--danger-text); }
.badge-canceled { background: color-mix(in srgb, var(--muted) 20%, transparent); color: var(--muted); }
.badge-muted { background: var(--panel-2); color: var(--muted); }

.pagination { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }

/* --- Formularios ------------------------------------------------------------
   16 px en los campos: con menos, iOS Safari hace zoom al enfocar. Etiqueta
   encima del campo (flex en columna). */
.filters { display: flex; gap: var(--space-3); align-items: flex-end; flex-wrap: wrap; margin-bottom: var(--space-4); }
.filters label { display: flex; flex-direction: column; gap: var(--space-1); color: var(--muted); font-size: 14px; flex: 1 1 200px; max-width: 360px; }
input, select {
  min-height: 44px; font: inherit; font-size: 16px;
  background: var(--panel-2); border: 1px solid var(--stroke-strong); color: var(--text);
  padding: 0 var(--space-3); border-radius: var(--radius-m);
}
input:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* --- Botones ----------------------------------------------------------------
   44 px de alto en todos (también .btn-small, que solo es más estrecho) y los
   estados hover · active · focus · disabled. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: 44px; padding: 0 var(--space-4); border-radius: var(--radius-m);
  border: 1px solid var(--stroke-strong); background: var(--panel-2); color: var(--text);
  font: inherit; font-size: 15px; text-decoration: none; cursor: pointer;
}
.btn:hover { background: var(--hover-fill); }
.btn:active { background: var(--hover-fill); transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; transform: none; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 700; }
.btn-primary:hover { background: var(--accent); filter: brightness(1.1); }
.btn-danger { background: transparent; border-color: var(--danger); color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }
.btn-ghost { background: transparent; }
.btn-small { padding: 0 var(--space-3); font-size: 14px; }
.inline { display: inline; }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* --- Mensajes ---------------------------------------------------------------
   .error es un <div> (no <p>): lleva dentro el <details> con el detalle técnico,
   que no puede ir dentro de un párrafo. */
.error { color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); padding: var(--space-3) var(--space-4); border-radius: var(--radius-m); margin: 0 0 var(--space-4); }
.error p { margin: 0; }
.error details { margin-top: var(--space-2); color: var(--text); font-size: 14px; }
.error summary { cursor: pointer; }
.error-detail { display: block; margin-top: var(--space-1); color: var(--muted); font-size: 14px; }
.flash { padding: var(--space-3) var(--space-4); border-radius: var(--radius-m); margin-bottom: var(--space-4); }
.flash-key { background: color-mix(in srgb, var(--ok) 10%, transparent); border: 1px solid var(--ok); }
.flash-key .key-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }
.flash-key .key { flex: 1 1 20ch; font-size: 14px; word-break: break-all; }

/* --- Visor de logs ---------------------------------------------------------- */
.logs { background: var(--log-bg); border: 1px solid var(--stroke-weak); border-radius: var(--radius-m); padding: var(--space-2) 0; max-height: 460px; overflow: auto; font-family: ui-monospace, monospace; font-size: 12.5px; line-height: 1.55; }
.log-line { display: flex; padding: 0 var(--space-3); }
.log-line:hover { background: var(--log-hover); }
.log-ln { user-select: none; -webkit-user-select: none; color: var(--log-ln); min-width: 5ch; text-align: right; padding-right: var(--space-3); flex-shrink: 0; }
.log-text { white-space: pre-wrap; word-break: break-word; flex: 1; }
.log-error .log-text { color: var(--danger); }
.log-warning .log-text { color: var(--warn); }
.log-debug .log-text { color: var(--muted); }
.logs-meta { font-size: 13px; margin: var(--space-2) 2px 0; }
.reason { font-family: ui-monospace, monospace; font-size: 12.5px; color: var(--danger); }
/* Esqueleto de carga con la forma del visor: sin salto de layout al llegar las líneas. */
.skel-line { display: block; height: 12px; margin: var(--space-2) var(--space-3); background: var(--panel-2); border-radius: var(--radius-s); }
.skel-line:nth-child(2) { width: 80%; }
.skel-line:nth-child(3) { width: 55%; }

/* Cola del visor de logs (BL-014): marcador invisible al final del scroll que
   se auto-sondea y se sustituye a sí mismo con las líneas nuevas. */
.log-tail { display: block; height: 0; overflow: hidden; }

/* --- Indicador de refresco --------------------------------------------------
   htmx pone .htmx-request en el elemento que pide (o en su hx-indicator)
   aunque includeIndicatorStyles esté apagado. El retardo evita que parpadee
   cada 3 s: solo se ve si la petición tarda más de 300 ms. */
#nodes-live .live-stamp, #logs-meta { transition: opacity .2s; }
#nodes-live.htmx-request .live-stamp, #logs-meta.htmx-request {
  opacity: .5;
  transition-property: opacity; transition-duration: .2s; transition-delay: .3s;
}

/* --- Login ------------------------------------------------------------------ */
.login-body { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: var(--space-4); }
.login-card { background: var(--panel); border: 1px solid var(--stroke-weak); border-radius: var(--radius-l); padding: var(--space-6); width: 100%; max-width: 360px; display: flex; flex-direction: column; gap: var(--space-3); }
.login-card h1 { font-size: 20px; margin: 0 0 var(--space-1); }
.login-card label { display: flex; flex-direction: column; gap: var(--space-1); color: var(--muted); font-size: 14px; }

/* Distintivo de provider: letra + color. La LETRA es la que informa; el color
   solo acompaña (WCAG 1.4.1: no codificar información solo con color). */
.provider {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: var(--radius-s);
  font-size: 11px; font-weight: 700; line-height: 1;
  border: 1px solid var(--stroke-weak); color: var(--text);
}
.provider-hetzner { background: color-mix(in srgb, var(--accent) 25%, transparent); border-color: var(--accent); }
/* Cian y no el azul de marca de DO: a 14px de diametro seria indistinguible
   del azul de Hetzner, y el distintivo dejaria de distinguir. */
.provider-digitalocean { background: color-mix(in srgb, var(--do) 25%, transparent); border-color: var(--do); }
.provider-local { background: color-mix(in srgb, var(--warn) 25%, transparent); border-color: var(--warn); }
.provider-unknown { background: var(--panel-2); color: var(--muted); }

/* Barra de progreso. El ancho va en una clase discreta (pasos de 5) y no en un
   style="width:42%": la CSP del panel es `style-src 'self'`, sin unsafe-inline. */
.progress {
  display: block; width: 90px; height: 4px; margin-top: var(--space-1);
  background: var(--panel-2); border-radius: var(--radius-pill); overflow: hidden;
}
.progress-bar { display: block; height: 100%; background: var(--accent); border-radius: var(--radius-pill); }
.progress-bar.p-0 { width: 0%; }
.progress-bar.p-5 { width: 5%; }
.progress-bar.p-10 { width: 10%; }
.progress-bar.p-15 { width: 15%; }
.progress-bar.p-20 { width: 20%; }
.progress-bar.p-25 { width: 25%; }
.progress-bar.p-30 { width: 30%; }
.progress-bar.p-35 { width: 35%; }
.progress-bar.p-40 { width: 40%; }
.progress-bar.p-45 { width: 45%; }
.progress-bar.p-50 { width: 50%; }
.progress-bar.p-55 { width: 55%; }
.progress-bar.p-60 { width: 60%; }
.progress-bar.p-65 { width: 65%; }
.progress-bar.p-70 { width: 70%; }
.progress-bar.p-75 { width: 75%; }
.progress-bar.p-80 { width: 80%; }
.progress-bar.p-85 { width: 85%; }
.progress-bar.p-90 { width: 90%; }
.progress-bar.p-95 { width: 95%; }
.progress-bar.p-100 { width: 100%; }

/* --- Grafo de la máquina de estados (vista en vivo de /nodes) ---------------
   Todo el estilo vive aquí, en un archivo servido con 'self': la CSP del panel
   (`style-src 'self'`, sin unsafe-inline) bloquea `style=` y `<style>`, así que
   la plantilla solo emite atributos de presentación SVG y clases. Sin SMIL
   (<animate>): Firefox lo bloquea bajo CSP; las animaciones son CSS.
   En móvil el grafo no se encoge por debajo de 640 px (las etiquetas serían
   ilegibles): se desplaza dentro de .fsm-scroll, cuya posición conserva
   /static/scroll-keep.js entre refrescos. */
.fsm-scroll {
  overflow-x: auto;
  margin: var(--space-2) 0 var(--space-1);
  background: var(--panel); border: 1px solid var(--stroke-weak); border-radius: var(--radius-l);
}
.fsm { display: block; width: 100%; min-width: 640px; height: auto; }
.live-stamp { font-size: 13px; margin: 0 0 2px; }

.fsm-state rect { fill: var(--panel-2); stroke: var(--stroke-weak); stroke-width: 1; }
.fsm-state-label {
  fill: var(--muted); font-size: 12px; font-weight: 700;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.fsm-state--error rect { stroke: var(--danger); }
.fsm-state--terminal rect { stroke-dasharray: 4 3; }
/* Un estado con nodos dentro se destaca: es lo que el operador busca de un vistazo. */
.fsm-state--active rect { fill: color-mix(in srgb, var(--accent) 14%, transparent); stroke: var(--accent); }
.fsm-state--active .fsm-state-label { fill: var(--text); }
/* ...pero ERROR/ORPHANED con nodos dentro NO deben leerse como actividad sana:
   son nodos que siguen facturando y piden intervención. */
.fsm-state--active.fsm-state--error rect { fill: color-mix(in srgb, var(--danger) 18%, transparent); stroke: var(--danger); }
.fsm-state--active.fsm-state--error .fsm-state-label { fill: var(--danger-text); }

.fsm-edge { fill: none; stroke: var(--edge); stroke-width: 1.5; }
.fsm-edge--happy { stroke: var(--muted); }
.fsm-edge--cancel { stroke: var(--edge); stroke-dasharray: 4 4; }
.fsm-edge--error { stroke: var(--edge-error); }
.fsm-edge--recovery { stroke: var(--edge-recovery); stroke-dasharray: 6 3; }
.fsm-arrowhead { fill: var(--muted); }

.fsm-token { stroke: var(--bg); stroke-width: 1.5; }
.fsm-token.provider-hetzner { fill: var(--accent); }
.fsm-token.provider-digitalocean { fill: var(--do); }
.fsm-token.provider-local { fill: var(--warn); }
.fsm-token.provider-unknown { fill: var(--muted); }
.fsm-token-letter {
  fill: var(--bg); font-size: 8px; font-weight: 700;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  pointer-events: none;
}
/* Siglas de dos letras (DO): a 8px se salen del circulo de radio 7. */
.fsm-token-letter-2 { font-size: 6px; }
.fsm-overflow {
  fill: var(--muted); font-size: 11px; font-weight: 700;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.fsm a { cursor: pointer; }
.fsm a:focus-visible .fsm-token { stroke: var(--text); stroke-width: 2.5; }

.fsm-legend { font-size: 13px; margin: 0 0 var(--space-4); display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

/* Latido en los estados donde el pipeline está trabajando de verdad: ayuda a
   distinguir "avanza" de "atascado" sin leer el sello de hora. */
.fsm-state--active.fsm-state--happy rect { animation: fsm-pulse 2.4s ease-in-out infinite; }
@keyframes fsm-pulse {
  0%, 100% { stroke-opacity: 1; }
  50% { stroke-opacity: .35; }
}
@media (prefers-reduced-motion: reduce) {
  .fsm-state--active.fsm-state--happy rect { animation: none; }
  .btn:active { transform: none; }
}
