/* ═══════════════════════════════════════════════════════════════════════════
   etrinok Design System — Design Tokens
   ═══════════════════════════════════════════════════════════════════════════
   Single source of truth for color, spacing, typography, radii and shadows.
   Every token is a CSS custom property so both themes (dark/light) share one
   set of names — components never hardcode a color, they reference a token.

   Theme selection:
     - `:root` (and `[data-theme="dark"]`) → dark palette (default)
     - `[data-theme="light"]`               → light palette
     - No stored preference → falls back to `prefers-color-scheme`
   The `theme_controller.js` Stimulus controller sets `data-theme` on <html>
   and persists the choice to localStorage. See app/javascript/controllers/theme_controller.js

   Palette source: docs/REDESIGN_SPEC.md §2 (Anthropic's own swatches — clay,
   ivory, slate, olive, kraft, sky, heather, manilla, fig — read from
   anthropic.com's CSS). §3 sources the type scale and radii below.
   A few pre-redesign names (--accent-active, --*-border on
   positive/negative/warning/info, --neutral-subtle/-border,
   --scrollbar-*, --shadow-xl) fall outside the spec vocabulary and are kept
   as "legacy alias" tokens, derived from the new palette, because existing
   views/components.css still read them. The --*-text family on
   positive/negative/warning/info/neutral was the same kind of alias —
   retired in wave 7 once its last call site (5 Stimulus controllers) was
   migrated to the canonical --positive/--negative/--warning/--info/
   --text-secondary classes. The Binance-brand accent alias family (5 names:
   base/-hover/-subtle/-border/-text) was the same kind of alias too —
   retired in wave 8 once its only call site (a nav-link modifier the wave 2
   sidebar rewrite had already stopped emitting) was deleted from
   components.css.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── Typography ──────────────────────────────────────────────────────── */
  --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

  /* Type scale — spec §3. All sans (no serif for body copy in a trading
     tool); mono stays reserved for numbers that align in a column. */
  --text-xs:   0.65625rem; /* 10.5px — micro-label, uppercase, 0.06em tracking */
  --text-sm:   0.84375rem; /* 13.5px — collapses with --text-base, spec §3 */
  --text-base: 0.84375rem; /* 13.5px — body */
  --text-md:   0.9375rem;  /* 15px */
  --text-lg:   1.1875rem;  /* 19px — KPI */
  --text-xl:   1.4375rem;  /* 23px — page title */
  --text-2xl:  1.875rem;   /* 30px — hero */
  --text-3xl:  2.25rem;    /* 36px */

  --leading-tight:  1.2;
  --leading-snug:   1.375;
  --leading-normal: 1.5;

  /* ── Spacing scale (4px base) ────────────────────────────────────────── */
  --space-1:  0.25rem;
  --space-2:  0.5rem;
  --space-3:  0.75rem;
  --space-4:  1rem;
  --space-5:  1.25rem;
  --space-6:  1.5rem;
  --space-8:  2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;

  /* ── Radii — spec §3 real Anthropic scale (4/8/16/999). Five names kept
     for call-site compatibility even though lg/xl/2xl now collapse to the
     same 16px card radius; pills use --radius-full, fields use --radius-md. */
  --radius-sm:  4px;
  --radius-md:  8px;
  --radius-lg:  16px;
  --radius-xl:  16px;
  --radius-2xl: 16px;
  --radius-full: 9999px;

  /* ── Motion ──────────────────────────────────────────────────────────── */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --duration-fast: 120ms;
  --duration-base: 180ms;
  --duration-slow: 280ms;

  /* ── Z-index scale ───────────────────────────────────────────────────── */
  --z-nav: 40;
  --z-dropdown: 50;
  --z-modal: 100;
  --z-toast: 110;
}

/* ═══════════════════════════════════════════════════════════════════════
   DARK THEME (default)
   ═══════════════════════════════════════════════════════════════════════ */
:root,
[data-theme="dark"] {
  color-scheme: dark;

  /* Surfaces — layered elevation from canvas → raised (spec §2) */
  --surface-canvas:   #141413;
  --surface-base:     #201f1e;
  --surface-sunken:   #0e0e0d;
  --surface-raised:   #2b2a28;
  --surface-overlay:  #3d3d3a;
  --surface-backdrop: rgba(8, 8, 7, 0.66);

  /* Borders — ink at low opacity, never a solid gray; cards read by surface
     contrast in dark, not by a strong border (spec §3) */
  --border-subtle:  rgba(250, 249, 245, 0.06);
  --border-default: rgba(250, 249, 245, 0.10);
  --border-strong:  rgba(250, 249, 245, 0.20);
  --border-focus:   #d97757;

  /* Text */
  --text-primary:   #faf9f5; /* 15,7:1 */
  --text-secondary: #b0aea5; /* 7,3:1 */
  --text-tertiary:  #87867f; /* 4,5:1 */
  --text-disabled:  #5e5d59;
  --text-inverse:   #141413;

  /* Accent — clay. One value, shared by both platforms (spec §4: the
     product identity doesn't repaint per workspace); the workspace switcher
     and topbar pill carry platform color separately via --platform-* below. */
  --accent:        #d97757;
  --accent-hover:  #e89b7f;
  --accent-text:   #d97757; /* 5,3:1 — clay passes for text in dark */
  --accent-subtle: rgba(217, 119, 87, 0.18);
  --accent-border: rgba(217, 119, 87, 0.38);

  /* Status colors — positive/negative are NOT clay-adjacent on purpose
     (spec §2 decision 3: a loss must never read as an accent) */
  --positive:        #8ca46c; /* olive, lightened, 6,1:1 */
  --positive-subtle: rgba(140, 164, 108, 0.16);
  --negative:        #dd6b75; /* derived, 5,1:1 */
  --negative-subtle: rgba(221, 107, 117, 0.16);
  --warning:         #d4a27f; /* kraft, direct, 7,4:1 */
  --warning-subtle:  rgba(212, 162, 127, 0.16);
  --info:            #6a9bcc; /* sky, direct, 5,7:1 */
  --info-subtle:     rgba(106, 155, 204, 0.16);

  /* Platform identity — tinted badge + dot for the workspace switcher and
     topbar pill (spec §4). Polymarket = heather, Binance = manilla. */
  --platform-polymarket-bg:  rgba(203, 202, 219, 0.16);
  --platform-polymarket-ink: #cbcadb;
  --platform-polymarket-dot: #cbcadb;
  --platform-binance-bg:     rgba(235, 219, 188, 0.16);
  --platform-binance-ink:    #ebdbbc;
  --platform-binance-dot:    #ebdbbc;

  /* Chart base — grid/axis only; series colors live further down since they
     alias --accent/--text-* per spec §6's "no categorical palette" rule */
  --chart-grid: rgba(250, 249, 245, 0.07);
  --chart-axis: #87867f;

  /* Primary button — ink↔ivory inversion, not clay (white-on-clay fails
     contrast for text); mirrors anthropic.com (spec §2 decision 2) */
  --btn-primary-bg:  #faf9f5;
  --btn-primary-ink: #141413;
  --brand-mark-ink:  #faf9f5; /* tinta sobre relleno de acento (marca de Devise, burbuja del chat): clara en LOS DOS temas, porque el clay no cambia de claridad — spec §6-bis pt. 2 */

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 20px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 2px 6px rgba(0, 0, 0, 0.4), 0 20px 44px rgba(0, 0, 0, 0.5);

  /* ── Legacy aliases — outside the spec vocabulary; derived from the
     palette above so nothing goes colorless while wave 2 migrates call
     sites to the canonical names (see file header). ────────────────────── */
  --accent-active:   #c6613f; /* legacy alias → pressed/active state, one step past --accent-hover */
  --positive-border: rgba(140, 164, 108, 0.38); /* legacy alias → translucent border variant of --positive */
  --negative-border: rgba(221, 107, 117, 0.38); /* legacy alias → translucent border variant of --negative */
  --warning-border:  rgba(212, 162, 127, 0.38); /* legacy alias → translucent border variant of --warning */
  --info-border:     rgba(106, 155, 204, 0.38); /* legacy alias → translucent border variant of --info */
  --neutral-subtle: rgba(250, 249, 245, 0.08); /* legacy alias → ivory-tinted neutral fill */
  --neutral-border: rgba(250, 249, 245, 0.18); /* legacy alias → ivory-tinted neutral border */
  --shadow-xl: 0 4px 10px rgba(0, 0, 0, 0.45), 0 32px 72px rgba(0, 0, 0, 0.60); /* legacy alias → modal/popover shadow one step past --shadow-lg */
  --scrollbar-thumb: rgba(250, 249, 245, 0.16); /* legacy alias → ivory-tinted scrollbar thumb */
  --scrollbar-track: transparent; /* legacy alias → kept for API symmetry with --scrollbar-thumb */

  /* ── Focus ring — always visible, always clay (spec §5) ───────────────── */
  --shadow-focus-ring: 0 0 0 3px var(--accent-subtle), 0 0 0 1px var(--accent);

  /* ── Chart series — spec §6 bans categorical palettes. Line 1 is the only
     one in brand color; 2–4 ride the gray/border ramp so a second or third
     series reads as de-emphasized, never as a different category. ─────── */
  --chart-line-1: var(--accent);
  --chart-line-2: var(--text-tertiary);
  --chart-line-3: var(--text-disabled);
  --chart-line-4: var(--border-strong);
  --chart-up:   var(--positive);
  --chart-down: var(--negative);
}

/* ═══════════════════════════════════════════════════════════════════════
   LIGHT THEME
   ═══════════════════════════════════════════════════════════════════════ */
[data-theme="light"] {
  color-scheme: light;

  /* Surfaces — layered elevation from canvas → raised (spec §2) */
  --surface-canvas:   #f0eee6;
  --surface-base:     #faf9f5;
  --surface-sunken:   #e8e6dc;
  --surface-raised:   #ffffff;
  --surface-overlay:  #e8e6dc; /* ivory-dark — spec only names this surface for dark; here it's the hover/overlay tint (spec §2 swatch table) */
  --surface-backdrop: rgba(20, 20, 19, 0.42);

  /* Borders — ink at low opacity, never a solid gray (spec §3) */
  --border-subtle:  rgba(20, 20, 19, 0.06);
  --border-default: rgba(20, 20, 19, 0.10);
  --border-strong:  rgba(20, 20, 19, 0.20);
  --border-focus:   #d97757;

  /* Text */
  --text-primary:   #141413; /* 17,7:1 */
  --text-secondary: #5e5d59; /* 6,2:1 */
  --text-tertiary:  #6f6e69; /* 4,4:1 */
  --text-disabled:  #a39a89;
  --text-inverse:   #faf9f5;

  /* Accent — clay. One value, shared by both platforms (spec §4: the
     product identity doesn't repaint per workspace); the workspace switcher
     and topbar pill carry platform color separately via --platform-* below.
     Clay itself fails as text color here (3,0:1) — --accent-text is the
     darkened one that passes (spec §2 decision 1). */
  --accent:        #d97757; /* fill only, NOT for text */
  --accent-hover:  #c6613f;
  --accent-text:   #a94f30; /* 5,2:1 — this is the one that carries text */
  --accent-subtle: rgba(217, 119, 87, 0.14);
  --accent-border: rgba(217, 119, 87, 0.32);

  /* Status colors — positive/negative are NOT clay-adjacent on purpose
     (spec §2 decision 3: a loss must never read as an accent) */
  --positive:        #5e7548; /* olive, darkened, 4,9:1 */
  --positive-subtle: rgba(94, 117, 72, 0.12);
  --negative:        #b03a46; /* derived, 5,7:1 */
  --negative-subtle: rgba(176, 58, 70, 0.10);
  --warning:         #8d6035; /* kraft, darkened, 5,2:1 */
  --warning-subtle:  rgba(212, 162, 127, 0.24);
  --info:            #3d6f9e; /* sky, darkened, 5,0:1 */
  --info-subtle:     rgba(106, 155, 204, 0.16);

  /* Platform identity — tinted badge + dot for the workspace switcher and
     topbar pill (spec §4). Polymarket = heather, Binance = manilla. */
  --platform-polymarket-bg:  #cbcadb;
  --platform-polymarket-ink: #141413;
  --platform-polymarket-dot: #7b79ad;
  --platform-binance-bg:     #ebdbbc;
  --platform-binance-ink:    #141413;
  --platform-binance-dot:    #b08b4f;

  /* Chart base — grid/axis only; series colors live further down since they
     alias --accent/--text-* per spec §6's "no categorical palette" rule */
  --chart-grid: rgba(20, 20, 19, 0.08);
  --chart-axis: #6f6e69;

  /* Primary button — ink↔ivory inversion, not clay (clay-on-white would be
     3,1:1 and fails); mirrors anthropic.com (spec §2 decision 2) */
  --btn-primary-bg:  #141413;
  --btn-primary-ink: #faf9f5;
  --brand-mark-ink:  #faf9f5; /* tinta sobre relleno de acento (marca de Devise, burbuja del chat): clara en LOS DOS temas, porque el clay no cambia de claridad — spec §6-bis pt. 2 */

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(20, 20, 19, 0.05);
  --shadow-md: 0 1px 2px rgba(20, 20, 19, 0.04), 0 6px 16px rgba(20, 20, 19, 0.06);
  --shadow-lg: 0 2px 4px rgba(20, 20, 19, 0.05), 0 18px 40px rgba(20, 20, 19, 0.12);

  /* ── Legacy aliases — outside the spec vocabulary; derived from the
     palette above so nothing goes colorless while wave 2 migrates call
     sites to the canonical names (see file header). ────────────────────── */
  --accent-active:   #a94f30; /* legacy alias → pressed/active state, one step past --accent-hover */
  --positive-border: rgba(94, 117, 72, 0.32); /* legacy alias → translucent border variant of --positive */
  --negative-border: rgba(176, 58, 70, 0.30); /* legacy alias → translucent border variant of --negative */
  --warning-border:  rgba(212, 162, 127, 0.44); /* legacy alias → translucent border variant of --warning */
  --info-border:     rgba(106, 155, 204, 0.34); /* legacy alias → translucent border variant of --info */
  --neutral-subtle: rgba(20, 20, 19, 0.06); /* legacy alias → ink-tinted neutral fill */
  --neutral-border: rgba(20, 20, 19, 0.16); /* legacy alias → ink-tinted neutral border */
  --shadow-xl: 0 4px 8px rgba(20, 20, 19, 0.06), 0 28px 64px rgba(20, 20, 19, 0.18); /* legacy alias → modal/popover shadow one step past --shadow-lg */
  --scrollbar-thumb: rgba(20, 20, 19, 0.18); /* legacy alias → ink-tinted scrollbar thumb */
  --scrollbar-track: transparent; /* legacy alias → kept for API symmetry with --scrollbar-thumb */

  /* ── Focus ring — always visible, always clay (spec §5) ───────────────── */
  --shadow-focus-ring: 0 0 0 3px var(--accent-subtle), 0 0 0 1px var(--accent);

  /* ── Chart series — spec §6 bans categorical palettes. Line 1 is the only
     one in brand color; 2–4 ride the gray/border ramp so a second or third
     series reads as de-emphasized, never as a different category. ─────── */
  --chart-line-1: var(--accent);
  --chart-line-2: var(--text-tertiary);
  --chart-line-3: var(--text-disabled);
  --chart-line-4: var(--border-strong);
  --chart-up:   var(--positive);
  --chart-down: var(--negative);
}

/* ── System-preference fallback for first paint before JS runs ──────────
   theme_controller sets data-theme as early as possible (inline in <head>)
   to avoid flash-of-wrong-theme; this block is a safety net only. ────── */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    color-scheme: light;
  }
}
/* ═══════════════════════════════════════════════════════════════════════════
   etrinok Design System — Base / Reset layer
   Global element defaults built on top of tokens.css. Themeable, minimal.
   ═══════════════════════════════════════════════════════════════════════════ */

* {
  border-color: var(--border-default);
}

html {
  color-scheme: light dark;
}

body {
  background: var(--surface-canvas);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  /* Cifras sueltas en el cuerpo: proporcionales por defecto (spec §3). Las
     columnas que necesitan alinear dígitos piden .num-tabular o .tabular
     explícitamente. */
  font-variant-numeric: proportional-nums;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color var(--duration-base) var(--ease-out);
}

/* Disable transitions during the instant a theme is switched to avoid a
   laggy "wipe" effect across every element — theme_controller toggles
   this class off again on the next frame. */
.theme-switching,
.theme-switching * {
  transition: none !important;
}

::selection {
  background: var(--accent-subtle);
  color: var(--text-primary);
}

a {
  color: inherit;
}

/* Scrollbars */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--radius-full);
}
*::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }

/* ── Escala tipográfica (spec §3) ─────────────────────────────────────────
   Utilidades nuevas: todavía no las consume nadie, pero las vistas de la
   ola 2 y los partials de shared/ las van a usar en vez de tamaños sueltos. */
.type-hero {
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
  font-variant-numeric: proportional-nums;
}

.type-page-title {
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: var(--leading-tight);
}

.type-kpi {
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: var(--leading-tight);
  font-variant-numeric: proportional-nums;
}

.type-card-heading {
  font-size: var(--text-sm);
  font-weight: 650;
  line-height: var(--leading-snug);
  color: var(--text-primary);
}

.type-body {
  font-size: var(--text-base);
  font-weight: 400;
  line-height: var(--leading-normal);
}

/* Micro-etiqueta del spec §3: 10,5px / weight 700 / letter-spacing 0.06em /
   uppercase (19 vistas). NO confundir con `.text-micro` (components.css,
   ~línea 861), que solo fija font-size (0.65rem) y no es la clase del spec. */
.type-micro {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
}

/* ── Color de texto por token ──────────────────────────────────────────────
   Las tres jerarquías de texto del spec §2 como utilidades. Tailwind NO
   genera estas clases (no son colores de su theme), así que no hay colisión;
   y como este archivo va sin @layer, ganan por cascada a cualquier
   `text-gray-N` que quede en una vista a medio migrar.
   La ola 2 reemplaza `text-gray-400/500/600/700` por estas
   (ver docs/DESIGN_TOKEN_MIGRATION.md §2, fila 7). */
.text-primary   { color: var(--text-primary); }
.text-secondary { color: var(--text-secondary); }
.text-tertiary  { color: var(--text-tertiary); }
.text-disabled  { color: var(--text-disabled); }

/* Acento y estado — mismo criterio de cascada que las cuatro de arriba.
   Reemplazan los `style="color:var(--accent-text)"` etc. inline (conteo por
   clase en docs/DESIGN_TOKEN_MIGRATION.md §8); ninguna vista de esta ola las
   consume todavía, quedan definidas para que la ola 5 tenga a dónde migrar. */
.text-accent    { color: var(--accent-text); }
.text-positive  { color: var(--positive); }
.text-negative  { color: var(--negative); }
.text-warning   { color: var(--warning); }
.text-info      { color: var(--info); }
.text-inverse   { color: var(--text-inverse); }

/* Monospace utility for tabular/financial figures */
.font-mono,
.tabular {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Tabulares en sans (columnas de tabla que no necesitan monoespaciada) y
   proporcionales explícitas — el complemento de .tabular / .font-mono. */
.num-tabular {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

.num-proportional {
  font-variant-numeric: proportional-nums;
}

/* Focus ring — consistent everywhere, replaces Tailwind's default outline.
   Siempre visible, con clay (spec §5): nunca outline:none sin reemplazo.
   input/select/textarea/button/a heredan esta regla en vez de anularla. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--shadow-focus-ring);
}

/* Placeholder consistency */
::placeholder {
  color: var(--text-tertiary);
  opacity: 1;
}

/* Native scrollbar-adjacent form controls should inherit theme */
select {
  color-scheme: inherit;
}

/* prefers-reduced-motion (spec §5) — única regla del design system; no la
   dupliques en components.css. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* El atributo `hidden` le gana a cualquier `display` de autor.
 *
 * El navegador trae `[hidden] { display: none }` en su hoja de usuario, y
 * CUALQUIER regla de autor le gana — así que `hidden` no oculta un `.btn`
 * (display: inline-flex), un `.row-detail` (display: table-row) ni un
 * `.ws-menu`. El proyecto ya se tropezó tres veces con esto y lo resolvió tres
 * veces a mano, una por componente:
 *
 *     .ws-menu.hidden     { display: none; }   (nav.css)
 *     .row-detail[hidden] { display: none; }   (components.css)
 *     .alert--stacked.hidden { display: none; }
 *
 * y la cuarta se escapó: el botón "cargar más" seguía visible después de la
 * última tanda porque `.btn` le ganaba. Una regla, y deja de pasar.
 *
 * `!important` a propósito: la gracia es justamente que nada la pise.
 */
[hidden] { display: none !important; }
/* ═══════════════════════════════════════════════════════════════════════════
   etrinok Design System — Components
   Reusable, theme-aware building blocks. Class names are semantic (`.badge`,
   `.card`, `.btn`) — never a color utility. Swap `data-theme` on <html> and
   every one of these repaints correctly with no per-view changes.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Card ────────────────────────────────────────────────────────────────
   Drop-in replacement for the old ad-hoc `.card { background: var(--trading-card) }`.
   Same class name so existing views need zero markup changes. */
.card {
  background: var(--surface-base);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out);
}

.card--interactive:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
}

.card--accent {
  border-color: var(--accent-border);
  background: linear-gradient(180deg, var(--accent-subtle) 0%, var(--surface-base) 140%);
}

/* Hermana de .card--accent para el registro de "atención": la usa el bloque de
   órdenes límite pendientes, que tenía los dos colores inline. */
.card--warning {
  border-color: var(--warning-border);
  background: linear-gradient(180deg, var(--warning-subtle) 0%, var(--surface-base) 140%);
}

.card--sunken {
  background: var(--surface-sunken);
  border-color: var(--border-subtle);
  box-shadow: inset 0 1px 0 var(--border-subtle);
}

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
}

.card-title {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.card-subtitle {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-top: 2px;
}

/* Esta clase es DUEÑA de su margen inferior (`margin-bottom` abajo). Un
   `mb-*` de Tailwind escrito al lado en el mismo `class="…"` queda MUERTO por
   la trampa de capas de siempre (spec §6-bis): Tailwind v4 emite sus
   utilidades dentro de `@layer utilities` y esta hoja va sin capa, así que le
   gana siempre, con cualquier especificidad. No le agregues `mb-*` — no hace
   nada, y el guard de `spec/views/design_system_lint_spec.rb` lo frena.
   (`mt-*` sí funciona: esta clase no toca `margin-top`.) */
.card-eyebrow {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
  margin-bottom: var(--space-1);
}

/* ── KPI card (número hero + contexto — spec §3/§5) ──────────────────────
   Reemplaza los stats sueltos sin unidad ni período. El sublabel es donde
   va el «(últimos 7 días)» que hoy falta en los dashboards. */
.kpi-card {
  background: var(--surface-base);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}
.kpi-label {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
}
.kpi-value {
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-numeric: proportional-nums;
}
.kpi-sublabel {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-top: 2px;
}

/* ── Buttons ─────────────────────────────────────────────────────────────
   `.btn` + one modifier. Sizing via `.btn--sm` / default / `.btn--lg`.     */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  padding: 0.5rem 0.9rem;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
  text-decoration: none;
}
.btn:active { transform: scale(0.98); }
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.btn--sm { font-size: var(--text-xs); padding: 0.3rem 0.65rem; border-radius: var(--radius-full); }
.btn--lg { font-size: var(--text-md); padding: 0.7rem 1.3rem; border-radius: var(--radius-full); }

/* Rails envuelve cada `button_to` en un <form class="button_to">, y ese form es
   un bloque. En una fila flex el ítem que se alinea es el FORM, no el .btn de
   adentro: al lado de un <a class="btn"> los botones de formulario quedaban
   corridos, con su propia caja y su propia altura. Con inline-flex el form se
   ajusta al botón y los dos se alinean como si fueran lo mismo.

   Va global y no en una vista porque hay 29 `button_to` en la app y ninguno
   tenía regla: donde se mezclan con enlaces, todos estaban igual de corridos. */
.button_to { display: inline-flex; }

/* Botón de un solo glifo. Con el padding lateral del botón de texto la caja
   queda más ancha que alta y el ícono se ve corrido: glifos como ✕, ⎘ o ▶
   tienen la caja de tinta desplazada respecto de la caja em, así que el
   centrado no sale de acertarle al padding sino de una caja cuadrada donde el
   `align-items/justify-content: center` de .btn hace el trabajo.

   El alto se hereda del tamaño que toque (.btn--sm, default o .btn--lg): así
   queda igual que el del botón de texto de al lado, sin número mágico. El ancho
   es un piso explícito.

   Acá hubo un intento con `aspect-ratio: 1` en vez del piso, y NO funciona:
   aspect-ratio necesita que una de las dos medidas sea definida para sacar la
   otra, y en un botón así las dos son automáticas (el alto lo da el padding, el
   ancho el glifo). El resultado medido fue 11.6 × 22.1 px — el ancho del glifo
   pelado, o sea botones finitos.

   Esta regla reemplaza además una anterior (`padding: 0.5rem` + radio) que no
   tenía ni un uso en las vistas. */
.btn--icon {
  padding-left: 0;
  padding-right: 0;
  min-width: 2.5rem;
  flex: none;
  border-radius: var(--radius-full);
}
.btn--sm.btn--icon { min-width: 1.75rem; }
.btn--full { width: 100%; }

/* Primary — main action (Sync, Save, Start Simulation).
   Inversión tinta↔marfil, NO clay (spec §2, decisión 2 — blanco sobre clay
   no pasa contraste). Mismo tratamiento en :hover y :active. */
.btn--primary {
  background: var(--btn-primary-bg);
  border-color: var(--btn-primary-bg);
  color: var(--btn-primary-ink);
}
.btn--primary:hover,
.btn--primary:active {
  background: color-mix(in srgb, var(--btn-primary-bg) 88%, var(--surface-canvas));
  border-color: color-mix(in srgb, var(--btn-primary-bg) 88%, var(--surface-canvas));
}

/* Secondary — neutral, low emphasis (Edit, Cancel, View) */
.btn--secondary {
  background: var(--surface-overlay);
  border-color: var(--border-default);
  color: var(--text-secondary);
}
.btn--secondary:hover {
  border-color: var(--border-strong);
  color: var(--text-primary);
}

/* Ghost — no border/fill until hover (nav items, tertiary links) */
.btn--ghost {
  background: transparent;
  color: var(--text-secondary);
}
.btn--ghost:hover {
  background: var(--surface-overlay);
  color: var(--text-primary);
}

/* Positive / success — Start bot, confirm profitable action */
.btn--positive {
  background: var(--positive);
  border-color: var(--positive);
  color: var(--text-inverse);
}
.btn--positive:hover { filter: brightness(1.08); }

/* Danger — Stop, Delete */
.btn--danger {
  background: var(--negative-subtle);
  border-color: var(--negative-border);
  color: var(--negative);
}
.btn--danger:hover {
  background: var(--negative);
  border-color: var(--negative);
  color: var(--text-inverse);
}
.btn--danger-solid {
  background: var(--negative);
  border-color: var(--negative);
  color: var(--text-inverse);
}
.btn--danger-solid:hover { filter: brightness(1.1); }

/* Warning — Pause, Reset */
.btn--warning {
  background: var(--warning-subtle);
  border-color: var(--warning-border);
  color: var(--warning);
}
.btn--warning:hover {
  background: var(--warning);
  border-color: var(--warning);
  color: var(--text-inverse);
}

/* Outline accent variant — used for the Binance amber CTAs etc. */
.btn--accent-outline {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent-text);
}
.btn--accent-outline:hover { background: var(--accent-subtle); border-color: var(--accent); }

/* Binance-brand solid button — spec §6-bis decisión 5: los tokens --platform-*
   son para pastillas y para el selector de workspace, no para botones. El
   mismo --platform-binance-bg da un botón sólido en tema claro y un tinte al
   16% (fantasma) en oscuro, porque ahí es donde vive el token — no en un
   fondo de botón. .btn--binance ahora se comporta exactamente como
   .btn--primary (inversión tinta↔marfil vía --btn-primary-bg/-ink); la
   identidad de plataforma sigue viviendo en .platform-tag y en el selector de
   workspace. El selector se mantiene sin borrar: hay call sites fuera de esta
   ola que lo usan. */
.btn--binance {
  background: var(--btn-primary-bg);
  border-color: var(--btn-primary-bg);
  color: var(--btn-primary-ink);
}
.btn--binance:hover,
.btn--binance:active {
  background: color-mix(in srgb, var(--btn-primary-bg) 88%, var(--surface-canvas));
  border-color: color-mix(in srgb, var(--btn-primary-bg) 88%, var(--surface-canvas));
}

/* ── Badges / Status pills ──────────────────────────────────────────────
   `.badge` + semantic modifier. Used for bot status, mode (SIM/LIVE),
   backtest status, public/private, win/loss, etc. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.4;
  padding: 0.15rem 0.55rem;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  white-space: nowrap;
}

.badge--dot::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

.badge--pulse::before {
  animation: badge-pulse 1.6s ease-in-out infinite;
}

@keyframes badge-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.45; transform: scale(0.8); }
}

.badge--neutral  { background: var(--neutral-subtle);  border-color: var(--neutral-border);  color: var(--text-secondary); }
.badge--positive { background: var(--positive-subtle); border-color: var(--positive-border); color: var(--positive); }
.badge--negative { background: var(--negative-subtle); border-color: var(--negative-border); color: var(--negative); }
.badge--warning  { background: var(--warning-subtle);  border-color: var(--warning-border);  color: var(--warning); }
.badge--info     { background: var(--info-subtle);     border-color: var(--info-border);     color: var(--info); }
.badge--accent   { background: var(--accent-subtle);   border-color: var(--accent-border);   color: var(--accent-text); }

/* Solid (filled) variants — higher emphasis, e.g. unread counters */
.badge--solid-positive { background: var(--positive); border-color: var(--positive); color: var(--text-inverse); }
.badge--solid-negative { background: var(--negative); border-color: var(--negative); color: var(--text-inverse); }
.badge--solid-warning  { background: var(--warning);  border-color: var(--warning);  color: var(--text-inverse); }
.badge--solid-accent   { background: var(--accent);   border-color: var(--accent);   color: var(--text-inverse); }

/* Bot-session status → badge modifier map (mirrors BotSession#status_badge_class) */
.badge--status-running { background: var(--positive-subtle); border-color: var(--positive-border); color: var(--positive); }
.badge--status-paused  { background: var(--warning-subtle);  border-color: var(--warning-border);  color: var(--warning); }
.badge--status-stopped { background: var(--neutral-subtle);  border-color: var(--neutral-border);  color: var(--text-secondary); }
.badge--status-error   { background: var(--negative-subtle); border-color: var(--negative-border); color: var(--negative); }
.badge--status-idle    { background: var(--neutral-subtle);  border-color: var(--neutral-border);  color: var(--text-secondary); }

/* ── Status (punto + palabra, nunca solo color — spec §5) ───────────────────
   Para estado de bots/sesiones/backtests donde el color solo no alcanza
   (daltonismo, pantallas apagadas). El texto adentro es la etiqueta:
   <span class="status status--running">Corriendo</span> */
.status {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: var(--text-xs);
  font-weight: 700;
}
.status::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.status--running { color: var(--positive); }
.status--paused  { color: var(--warning); }
.status--stopped,
.status--idle    { color: var(--text-secondary); }
.status--error   { color: var(--negative); }
/* Reutiliza la animación que ya define .badge--pulse más arriba */
.status--live::before { animation: badge-pulse 1.6s ease-in-out infinite; }

/* ── Platform tag (fondo tintado + punto + nombre — spec §5) ────────────────
   Identifica de qué plataforma es una fila/tarjeta cuando conviven varias,
   ej. la vista combinada de Posiciones/Historial. */
.platform-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  border-radius: var(--radius-full);
  padding: 0.15rem 0.6rem;
  font-size: var(--text-xs);
  font-weight: 700;
}
.platform-tag::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.platform-tag--polymarket {
  background: var(--platform-polymarket-bg);
  color: var(--platform-polymarket-ink);
}
.platform-tag--polymarket::before { background: var(--platform-polymarket-dot); }

.platform-tag--binance {
  background: var(--platform-binance-bg);
  color: var(--platform-binance-ink);
}
.platform-tag--binance::before { background: var(--platform-binance-dot); }

/* ── PnL text helpers ────────────────────────────────────────────────────
   Replaces raw `text-green-400`/`text-red-400` sprinkled through models
   and views. Sign-aware color only — no background. */
.pnl-positive { color: var(--positive); }
.pnl-negative { color: var(--negative); }
.pnl-neutral  { color: var(--text-tertiary); }

/* Middle-of-the-road rating state (e.g. Sharpe 0.5–1.5, profit factor 1–1.5) —
   not bad enough for negative, not strong enough for positive. */
.text-sharpe-ok { color: var(--warning); }

/* ── Tables ──────────────────────────────────────────────────────────────
   Dense financial tables: tabular numerals, sticky header option, subtle
   row dividers, right-aligned numeric columns by convention (`.num`).     */
/* Contenedor de scroll horizontal — el body de la página nunca scrollea de
   costado (spec §5). La ola 2 envuelve cada <table class="table"> con esto. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
}
.table-scroll > .table { min-width: max-content; }
/* Tablas de texto corrido (documentación, descripciones largas): el
   min-width:max-content de arriba las estiraría hasta que la prosa entre en una
   sola línea. Con este modificador el texto envuelve dentro del ancho
   disponible y el contenedor solo scrollea si de verdad no entra. */
.table-scroll > .table--wrap { min-width: 0; }

/* ── Columnas que se esconden según el ancho ─────────────────────────────────
 *
 * Antes esto era una columna de acciones `sticky` a la derecha. Resolvía llegar
 * al botón, pero se veía mal: una celda sticky necesita fondo opaco propio, y
 * ese fondo no coincide con el de la fila, así que el botón quedaba en una
 * banda vertical que se leía como un panel flotante y no como parte de la fila.
 *
 * Esto es mejor: con catorce columnas de datos financieros no hay ancho que
 * alcance, así que en pantallas chicas se muestran menos. No se pierde nada —
 * todo lo que se esconde está en el panel que se abre al hacer click en la
 * fila, que es justamente para lo que se creó.
 *
 * Los cortes salen de medir la tabla de posiciones: 1662 px con las catorce.
 */
@media (max-width: 1600px) {
  .table .col-op-3 { display: none; }
}
@media (max-width: 1300px) {
  .table .col-op-2 { display: none; }
}
@media (max-width: 1000px) {
  .table .col-op-1 { display: none; }
}
/* En un teléfono ni cuatro columnas de datos financieros entran (medido: 711 px
   de tabla contra 325 disponibles), así que la tabla va a scrollear igual. Lo
   que NO puede pasar es que el botón de cerrar quede inalcanzable: acá se
   esconde de la fila y aparece dentro del panel, que a este ancho es la vista
   que de verdad se lee. */
@media (max-width: 768px) {
  .table .col-op-0 { display: none; }
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.table th {
  text-align: left;
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
  padding: var(--space-3);
  border-bottom: 1px solid var(--border-default);
  white-space: nowrap;
}

.table th.num,
.table td.num {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  /* Una cifra nunca parte en dos líneas. Importa sobre todo en las tablas con
     `.table--wrap`, donde las columnas se reparten el ancho del contenedor:
     sin esto un "+123.45%" podía quedar cortado al medio. */
  white-space: nowrap;
}

/* `.table th` fija text-align sin @layer, así que le gana a `.text-center` de Tailwind (que vive en @layer utilities) — hace falta este modificador explícito para centrar (spec §6-bis). */
.table th.center,
.table td.center {
  text-align: center;
}

/* El `color` por defecto de la celda va con :where() a propósito. `.table td`
   pesa (0,1,1) y las clases canónicas de color de base.css (.text-negative,
   .text-positive, .text-warning…) pesan (0,1,0): sin bajarle la especificidad,
   `.table td` le gana a todas y CUALQUIER celda coloreada queda aplanada a
   --text-secondary. Con :where() la regla pesa (0,0,1) y la clase de la celda
   manda. (Ojo: esto no rescata a las utilidades de color de Tailwind — viven en
   @layer utilities y cualquier regla sin capa les gana, sin importar la
   especificidad. Para colorear una celda hay que usar la clase canónica.) */
:where(.table) td { color: var(--text-secondary); }

.table td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border-subtle);
}

.table tbody tr {
  transition: background-color var(--duration-fast) var(--ease-out);
}
.table tbody tr:hover {
  background: var(--surface-overlay);
}
.table tbody tr:last-child td {
  border-bottom: none;
}

.table--sortable th a {
  color: inherit;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  text-decoration: none;
}
.table--sortable th a:hover { color: var(--text-primary); }
.table--sortable th.is-active a { color: var(--accent-text); }

/* ── Forms ───────────────────────────────────────────────────────────────
   `.field`, `.field-label`, `.input`/`.select`/`.textarea`.               */
.field { margin-bottom: var(--space-4); }

/* Misma trampa que `.card-eyebrow` más arriba: esta clase ya es dueña de su
   `margin-bottom` (abajo), así que un `mb-*` al lado queda muerto
   (spec §6-bis). No lo agregues — el guard de
   `spec/views/design_system_lint_spec.rb` lo vigila. `mt-*` sí funciona.
   Ojo: `.field` (arriba) y `.field-hint` (abajo) también fijan margen, pero
   cada una de un lado distinto — `.field` el de abajo, `.field-hint` el de
   arriba. La regla real es: la utilidad muere solo si la clase fija ESA
   MISMA propiedad. */
.field-label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-tertiary);
  margin-bottom: var(--space-2);
}

/* Renglón de etiqueta que puede llevar un badge al lado ("Instrumento ⚡PERP").
   Reserva la altura del badge SIEMPRE, tenga badge o no.

   Sin esto, dos campos lado a lado —uno con badge y otro sin— arrancan sus
   controles a alturas distintas: el badge es más alto que el texto de la
   etiqueta y empuja el control hacia abajo. Medido en backtests/new: el
   renglón con badge medía 19px, el de al lado 14px, y los dos desplegables
   quedaban 5px corridos uno respecto del otro.

   Es dueña de su propio `margin-bottom`, así que un `mb-*` al lado queda
   muerto — misma trampa que `.field` y `.card-eyebrow` (spec §6-bis). Y la
   etiqueta que va adentro no debe traer el suyo. */
.field-label-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 1.1875rem; /* alto de un .badge--xs: 10px de texto + 0.1rem×2 */
  margin-bottom: var(--space-2);
}
.field-label-row > .field-label { margin-bottom: 0; }

.field-hint {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-top: var(--space-1);
}

.field-error {
  font-size: var(--text-xs);
  color: var(--negative);
  margin-top: var(--space-1);
}

.input,
.select,
.textarea {
  width: 100%;
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
  color: var(--text-primary);
  border-radius: var(--radius-md);
  padding: 0.55rem 0.8rem;
  font-size: var(--text-sm);
  font-family: var(--font-sans);
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}
.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus-ring);
}
.input::placeholder,
.textarea::placeholder { color: var(--text-tertiary); }
.input:disabled,
.select:disabled,
.textarea:disabled { opacity: 0.55; cursor: not-allowed; }

.textarea { resize: vertical; min-height: 5rem; }

/* `class="input font-mono"` estaba MUERTO y nadie lo notaba: `.font-mono` vive en
   base.css y `.input/.select/.textarea` fijan `font-family: var(--font-sans)` en
   components.css. Misma especificidad (0,1,0) y las dos sin capa → gana la hoja
   que carga después, o sea components.css. Con ~20 call sites (los campos
   numéricos de las estrategias de Binance, el prompt de sistema del LLM, la URL
   de Ollama, el JSON de app_settings) todos se venían renderizando en sans.
   Esta regla (0,2,0) los devuelve a monoespaciada. */
/* Un campo obligatorio que todavía está vacío: avisa, no es un error todavía.
   Estaba inline en el buscador de instrumentos. */
.input--required-empty { border-color: var(--warning-border); }

/* Un <select> chico embebido en una barra de herramientas, no un campo de
   formulario: se encoge al contenido y baja un escalón de tamaño. */
/* Tercera opción del selector: no es una plataforma, es la vuelta a la
   pantalla que las compara. Se separa con una línea para que no se lea como
   un tercer workspace. */
/* Encabezado de tarjeta sin la línea divisoria de abajo: para paneles donde
   el contenido ya viene en cajas propias y el borde suma un renglón de ruido.
   Estaba resuelto con tres declaraciones inline en el panel de rendimiento. */
/* Atenuar sin esconder: una sesión detenida sigue estando, pero no compite con
   las que están operando. Es el otro extremo del gradiente de peso visual del
   tablero — riesgo fuerte arriba, historia apagada abajo. */
.is-dimmed { opacity: 0.55; }
.is-dimmed:hover { opacity: 1; }

.card-header--flush {
  margin-bottom: var(--space-3);
  padding-bottom: 0;
  border-bottom: none;
}

/* Etiqueta de campo que comparte una fila flex con otra cosa (un badge, un
   enlace): sin el margen propio, para que las dos cajas se alineen y el margen
   lo ponga la fila. Va como modificador y no como `mb-0` suelto porque
   `.field-label` no está en ninguna capa y una utilidad de Tailwind pierde
   contra ella — la trampa que vigila design_system_lint_spec.rb. */
.field-label--inline { margin-bottom: 0; }

/* Campo dentro de un grid: el hueco lo pone `gap`, así que el margen propio de
   `.field` sobra y desalinea las filas. Misma trampa de capas que la de arriba
   —`.field` va sin capa y le gana a `mb-0`—, así que también necesita
   modificador propio. */
.field--flush { margin-bottom: 0; }

.ws-option--compare {
  border-top: 1px solid var(--border-subtle);
  color: var(--text-tertiary);
  font-size: 0.75rem;
}

.select--compact {
  width: auto;
  font-size: var(--text-xs);
  padding: 0.3rem 0.5rem;
}

/* Campo que comparte una fila con pastillas y botones (la barra del resumen de
   rendimiento). Copia el padding y el tamaño de letra de .filter-pill para que
   los tres compartan altura: con el padding del .input normal, los campos de
   fecha quedaban más altos y toda la fila se veía desalineada.

   `width: auto` porque .input es width:100% y en una fila flex eso lo estira
   hasta empujar a los demás fuera de su lugar. */
.input--inline {
  width: auto;
  font-size: var(--text-xs);
  padding: 0.28rem 0.5rem;
}

.input.font-mono,
.select.font-mono,
.textarea.font-mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.checkbox {
  width: 1rem;
  height: 1rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface-sunken);
  accent-color: var(--accent);
}

/* Fila de filtros — una sola fila de chips arriba de todo lo que filtran,
   nunca filtros por tarjeta (spec §5).

   NO lleva margen propio. Lo llevaba (`margin-bottom: var(--space-4)`) y eso
   desalineaba todos los ejes rotulados: cuando la hilera va adentro de una fila
   `display:flex; align-items:center` junto a su rótulo ("Estado", "Tipo",
   "Período"), el centrado del eje transversal centra la caja CON su margen, así
   que las pastillas suben media unidad (~8px) y el rótulo queda visiblemente
   más abajo que lo que rotula. Pasaba en los dos listados de sesiones y en el
   historial, o sea en todos los lugares donde el patrón se repite.

   La separación con lo que se filtra la pone quien la usa: el contenedor de la
   pantalla (`gap`/`space-y`) o, cuando la hilera va suelta y no hay contenedor
   que la separe, el modificador de abajo. */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Hilera suelta, sin rótulo al lado y sin contenedor que la separe de la
   lista/tabla que filtra: ahí el margen sí es separación y no desalinea nada,
   porque no comparte fila con ningún texto. Lo pide `shared/_filter_bar` con
   `stacked: true`. */
.filter-bar--stacked {
  margin-bottom: var(--space-4);
}

/* Segmented filter pills (status/period filters across index views) */
.filter-pill {
  display: inline-flex;
  align-items: center;
  font-size: var(--text-xs);
  padding: 0.28rem 0.7rem;
  border-radius: var(--radius-full);
  border: 1px solid var(--border-default);
  color: var(--text-secondary);
  transition: all var(--duration-fast) var(--ease-out);
  text-decoration: none;
}
.filter-pill:hover { border-color: var(--border-strong); color: var(--text-primary); }
.filter-pill.is-active {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent-text);
}

/* La cuenta que lleva cada tag en el filtro de los listados de estrategias.
   Va dentro de la pastilla, más apagada que el nombre: dice cuántas
   estrategias vas a ver ANTES de hacer clic, sin competir con el nombre. */
.tag-count {
  margin-left: 0.4rem;
  color: var(--text-disabled);
  font-variant-numeric: tabular-nums;
}
.filter-pill.is-active .tag-count { color: var(--accent-text); opacity: 0.7; }

/* El "+N" que abre la cola de tags. Punteado para que se lea como "hay más
   acá" y no como un tag que se puede elegir. */
.filter-pill--more {
  border-style: dashed;
  color: var(--text-tertiary);
  cursor: pointer;
}
.filter-pill--more:hover { border-color: var(--accent-border); color: var(--accent-text); }
.filter-pill--more[aria-expanded="true"] {
  border-style: solid;
  border-color: var(--accent-border);
  color: var(--accent-text);
}

/* El panel con el resto de los tags. Se expande DEBAJO de la fila en lugar de
   flotar: sin posicionamiento absoluto no hay forma de que quede fuera de la
   pantalla, y empujar el contenido es honesto — el panel está abierto porque
   lo abriste. El tope de altura evita que cincuenta tags ocupen la pantalla
   entera. */
.tag-filter-panel {
  width: 100%;
  margin-top: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  background: var(--surface-sunken);
  max-height: 15rem;
  overflow-y: auto;
}

/* ── Nav (sidebar) ───────────────────────────────────────────────────────── */
.nav-section-label {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-disabled);
  padding: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
}

.nav-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  border: 1px solid transparent;
  transition: all var(--duration-fast) var(--ease-out);
  text-decoration: none;
}
.nav-link:hover {
  background: var(--surface-overlay);
  color: var(--text-primary);
}
.nav-link.is-active {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent-text);
}
.nav-link svg { width: 1rem; height: 1rem; flex-shrink: 0; }

.nav-badge-count {
  margin-left: auto;
  background: var(--accent);
  color: var(--text-inverse);
  font-size: 0.625rem;
  font-weight: 700;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-full);
}

/* ── Chart containers ────────────────────────────────────────────────────
   Themeable replacement for the hardcoded navy background in
   binance_charts.css. Chart.js/Chartkick canvases render on transparent
   backgrounds by default so this wrapper supplies the surface + loading
   states consistently in both themes. */
.chart-container {
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
  position: relative;
  min-height: 280px;
}

.chart-container--tall { min-height: 460px; }

.chart-loading-overlay {
  position: absolute;
  inset: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--surface-sunken) 93%, transparent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  pointer-events: none;
  border-radius: inherit;
}

.chart-loading-bars {
  display: flex;
  align-items: flex-end;
  gap: 5px;
  height: 52px;
}
.chart-loading-bar {
  width: 6px;
  border-radius: 2px 2px 0 0;
  opacity: 0.25;
  transform-origin: bottom;
  animation: chart-bar-load 1.5s ease-in-out infinite;
}
.chart-loading-bar--up   { background: var(--chart-up); }
.chart-loading-bar--down { background: var(--chart-down); }
@keyframes chart-bar-load {
  0%, 100% { opacity: 0.2; transform: scaleY(0.65); }
  50%       { opacity: 0.9; transform: scaleY(1); }
}

.chart-loading-label {
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.07em;
}
.chart-loading-sublabel {
  color: var(--text-tertiary);
  font-size: 0.625rem;
  letter-spacing: 0.04em;
}

/* ── Aviso del borde izquierdo: historia al desplazar ─────────────────────────
   Una pastilla chica arriba a la izquierda del área de dibujo. NO es un overlay:
   las velas viejas entran por ese borde y taparlas sería tapar lo que la persona
   está esperando. `pointer-events: none` para que no se coma un arrastre que
   empiece justo ahí. Ver docs/VELAS_AL_DESPLAZAR.md §5.3. */
.chart-historia-aviso {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 15;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface-raised) 88%, transparent);
  border: 1px solid var(--border-default);
  color: var(--text-secondary);
  font-size: 0.625rem;
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
  pointer-events: none;
}
.chart-historia-aviso[hidden] { display: none; }
.chart-historia-aviso > span[hidden] { display: none; }
.chart-historia-aviso > span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.chart-historia-aviso__fecha { color: var(--text-tertiary); }
/* El "cargando" de 16 px que pide §5.3: un punto que late, no un spinner con
   canvas propio. */
.chart-historia-aviso__punto {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  animation: chart-historia-latido 1.1s ease-in-out infinite;
}
@keyframes chart-historia-latido {
  0%, 100% { opacity: 0.25; transform: scale(0.8); }
  50%      { opacity: 1;    transform: scale(1); }
}

/* ── Empty / placeholder states ──────────────────────────────────────────── */
.empty-state {
  text-align: center;
  padding: var(--space-12) var(--space-6);
  color: var(--text-tertiary);
}
.empty-state-icon {
  font-size: 2rem;
  color: var(--text-disabled);
  margin-bottom: var(--space-4);
}
.empty-state-title {
  color: var(--text-secondary);
  font-weight: 600;
  margin-bottom: var(--space-2);
}
.empty-state-hint {
  font-size: var(--text-sm);
  color: var(--text-tertiary);
  margin-bottom: var(--space-5);
}

/* "Primera vez": explica qué es y ofrece empezar — el borde punteado se lee
   como invitación, como una tarjeta esperando a rellenarse (spec §5). */
.empty-state--first-run {
  border: 1px dashed var(--border-default);
  border-radius: var(--radius-lg);
}

/* "No hay resultados con este filtro": ofrece limpiar el filtro, sin borde —
   es una ausencia temporal, no una invitación a crear algo nuevo. */
.empty-state--no-results {
  border: none;
}

.empty-state-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: center;
  flex-wrap: wrap;
}

/* ── Alert / banner ──────────────────────────────────────────────────────── */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  border-radius: var(--radius-lg);
  border: 1px solid;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
}
.alert--positive { background: var(--positive-subtle); border-color: var(--positive-border); color: var(--positive); }
.alert--negative { background: var(--negative-subtle); border-color: var(--negative-border); color: var(--negative); }
.alert--warning  { background: var(--warning-subtle);  border-color: var(--warning-border);  color: var(--warning); }
.alert--info     { background: var(--info-subtle);     border-color: var(--info-border);     color: var(--info); }
.alert--accent   { background: var(--accent-subtle);   border-color: var(--accent-border);   color: var(--accent-text); }

/* .alert es display:flex (icono + texto en fila). Cuando adentro van varias
   líneas apiladas — la lista de errores de un formulario, un aviso de dos
   párrafos — hace falta que apile. Esta clase reemplaza el style="display:block"
   inline que se venía copiando en 14 vistas. */
.alert--stacked { display: block; }
/* Trampa de capas (spec §6-bis, la de `.ws-menu`): `.alert` y `.alert--stacked`
   fijan `display`, y el design system va SIN capa, así que le gana a la
   utilidad `.hidden` de Tailwind — que v4 emite dentro de `@layer utilities`.
   Sin esta regla, esconder un aviso con `hidden` dejaría de funcionar en
   silencio. Hoy ninguna vista lo hace; queda declarada para que el día que
   alguien lo intente, ande. */
.alert.hidden,
.alert--stacked.hidden { display: none; }

/* ── Progress bar (deployed capital, drawdown, etc.) ─────────────────────── */
.progress-track {
  background: var(--surface-sunken);
  border-radius: var(--radius-full);
  height: 6px;
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--accent);
  transition: width var(--duration-slow) var(--ease-out);
}
.progress-fill--positive { background: var(--positive); }
.progress-fill--warning  { background: var(--warning); }
.progress-fill--negative { background: var(--negative); }
/* Cuando el valor todavía no alcanza el umbral que le daría color propio. */
.progress-fill--muted    { background: var(--text-disabled); }

/* ── Modal ───────────────────────────────────────────────────────────────── */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--surface-backdrop);
  backdrop-filter: blur(4px);
}
.modal-panel {
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl);
}

/* ── Skeleton loading ────────────────────────────────────────────────────── */
.skeleton {
  background: linear-gradient(90deg, var(--surface-overlay) 25%, var(--surface-raised) 50%, var(--surface-overlay) 75%);
  background-size: 200% 100%;
  animation: skeleton-sweep 1.4s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
@keyframes skeleton-sweep {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Refresco sin salto de layout — skeleton solo en la primera carga; al
   refrescar, el dato viejo baja de opacidad en vez de desaparecer (spec §5). */
.is-stale {
  opacity: 0.6;
  transition: opacity var(--duration-base) var(--ease-out);
}

/* Ayuda inline para jerga — solo estilo, el tooltip lo agrega JS/Stimulus
   aparte (title attr o similar). Ej: <span class="hint" title="...">Sharpe</span> */
.hint {
  border-bottom: 1px dotted var(--border-strong);
  cursor: help;
}

/* ── Divider ─────────────────────────────────────────────────────────────── */
.divider { border-top: 1px solid var(--border-subtle); }

/* ── Utilidades de superficie / borde / elevación ────────────────────────
   Vocabulario nuevo para lo que hoy se resuelve con `style="...var(--token)..."`
   inline (ver docs/DESIGN_TOKEN_MIGRATION.md §8 para el conteo grep-verificado
   por clase). Cada clase referencia un solo token — cero hex — y ninguna fija
   `display`, así que ninguna necesita su propia regla `.X.hidden` (spec §6-bis,
   la trampa de `.ws-menu`). Verificado sin colisión con Tailwind: el entry
   (`app/assets/tailwind/application.css`) es `@import "tailwindcss"` sin
   `@theme`, y ninguno de estos 21 nombres aparece en el CSS ya compilado.
   Ninguna vista de esta ola las consume todavía — quedan definidas para que
   la ola 5 tenga a dónde migrar. */

/* Superficie — solo fondo, sin borde ni sombra propios */
.surface-canvas  { background: var(--surface-canvas); }
.surface-base    { background: var(--surface-base); }
.surface-sunken  { background: var(--surface-sunken); }
.surface-raised  { background: var(--surface-raised); }
.surface-overlay { background: var(--surface-overlay); }

/* Relleno tintado de estado — el fondo suave de un badge/alert suelto, sin el
   resto del componente (borde, padding, radio) */
.fill-accent   { background: var(--accent-subtle); }
.fill-positive { background: var(--positive-subtle); }
.fill-negative { background: var(--negative-subtle); }
.fill-warning  { background: var(--warning-subtle); }
.fill-info     { background: var(--info-subtle); }

/* Relleno de estado SÓLIDO, no el tinte suave de arriba: un punto de estado o
   una barra que tiene que leerse como el color pleno. Se agregaron solo los dos
   que se usan; agregar la familia completa "por las dudas" fue lo que dejó
   vocabulario muerto en la ola 1. */
.fill-solid-accent   { background: var(--accent); }
.fill-solid-positive { background: var(--positive); }
.fill-solid-negative { background: var(--negative); }
.fill-solid-muted    { background: var(--text-disabled); }
.fill-solid-warning { background: var(--warning); }
/* Para datos SIN valencia, como un conteo. El acento (clay) y el negativo están
   pegados en el círculo cromático — mirá --accent y --negative en tokens.css,
   son casi el mismo tono— así que una barra de "cuántas operaciones hubo"
   pintada de acento se lee como una pérdida. El celeste no significa ni
   ganancia ni pérdida en trading. */
.fill-solid-info    { background: var(--info); }

/* `accent-color` es la propiedad nativa que pinta el check de un checkbox y el
   punto de un radio. No había clase — un comentario de bot_sessions/edit lo
   decía explícitamente— así que vivía inline en 6 sitios, y un `style=` es
   justo lo que esconde cosas de los greps: ya tapó dos bugs en la ola 12. */
.accent-brand    { accent-color: var(--accent); }
.accent-info     { accent-color: var(--info); }
.accent-warning  { accent-color: var(--warning); }
.accent-negative { accent-color: var(--negative); }

/* Borde en las 4 direcciones y en los dos lados sueltos más comunes
   (encabezado/pie de tarjeta sin usar .card-header completo) */
.bordered         { border: 1px solid var(--border-default); }
.bordered-subtle  { border: 1px solid var(--border-subtle); }
.bordered-strong  { border: 1px solid var(--border-strong); }
.bordered-accent  { border: 1px solid var(--accent-border); }
.bordered-warning  { border: 1px solid var(--warning-border); }
.bordered-positive { border: 1px solid var(--positive-border); }
.bordered-negative { border: 1px solid var(--negative-border); }
.border-r-subtle  { border-right: 1px solid var(--border-subtle); }
.border-t-accent  { border-top: 1px solid var(--accent-border); }
.border-t-warning { border-top: 1px solid var(--warning-border); }
.border-t-default { border-top: 1px solid var(--border-default); }
.border-b-default { border-bottom: 1px solid var(--border-default); }
.border-t-subtle  { border-top: 1px solid var(--border-subtle); }
.border-b-subtle  { border-bottom: 1px solid var(--border-subtle); }

/* Elevación — mismos tres niveles que .card/.modal-panel, sueltos para
   componentes que no son ni tarjeta ni modal */
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }

/* ── Vocabulario extraído de estilos inline (ola 6) ───────────────────────
   Cada clase de acá reemplaza un patrón `style="…"` que se repetía en las
   vistas. Los números entre paréntesis son las ocurrencias que reemplaza. */

/* Badge extra-chico: reemplaza 43 `style="font-size:10px"` sueltos. */
.badge--xs {
  font-size: 10px;
  padding: 0.1rem 0.45rem;
}

/* Caja de error (bloque "la sesión murió con este mensaje"): 3+ ocurrencias
   en bot_sessions/show. */
.error-box {
  background: var(--negative-subtle);
  border: 1px solid var(--negative-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.error-box-row { border-bottom: 1px solid var(--negative-border); }
.error-box-row:last-child { border-bottom: 0; }

/* Separadores de fila: 6+ ocurrencias por vista de sesión.
   `--top-subtle` y `--left` los agregó el auditor: sin ellos las vistas caían
   en `border-t`/`border-l-2` de Tailwind, que en v4 tiene `currentColor` por
   defecto — o sea el borde salía del color del TEXTO del bloque, no del token
   de borde. */
.divider-bottom        { border-bottom: 1px solid var(--border-default); }
.divider-bottom-subtle { border-bottom: 1px solid var(--border-subtle); }
.divider-top           { border-top: 1px solid var(--border-default); }
.divider-left          { border-left: 2px solid var(--border-default); }
/* `.divider` (más arriba, línea ~755) YA era el borde superior sutil; se le
   agrega acá el nombre de la familia en vez de duplicar la regla. Preferí
   `.divider-top-subtle` en las vistas nuevas; `.divider` queda por los call
   sites viejos. */
.divider-top-subtle    { border-top: 1px solid var(--border-subtle); }

/* Micro-texto de 0.65rem (etiquetas de sección dentro de tarjetas densas, 9
   vistas). Fija solo font-size — NO es la micro-etiqueta del spec §3 y NO
   confundir con `.type-micro` (base.css, ~línea 96), que sí es esa clase
   (10,5px + weight 700 + tracking 0.06em + uppercase). */
.text-micro { font-size: 0.65rem; line-height: 1.5; }

/* Punto de estado SUELTO, fuera de un `.badge` (lo agregó el auditor de la
   ola 6). No se puede resolver con `.badge--dot`/`.badge--pulse`: su `::before`
   solo se blockifica dentro de `.badge` (que es inline-flex), y `.badge--pulse`
   anima un `::before` que en un `<span>` pelado no tiene `content` — o sea que
   los "puntos pulsantes" de las vistas de sesión no pulsaban nada y además
   necesitaban un `background` inline. Toma el color de `currentColor`, así que
   se tiñe con `.text-warning` / `.text-positive` como cualquier otro texto. */
.pulse-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
  animation: badge-pulse 1.6s ease-in-out infinite;
}

/* Caja de log / consola hundida. */
.log-box {
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
}
.log-box--flat { border: 0; }

/* ── Toast (notificaciones flotantes, ola 7) ──────────────────────────────
   Reemplaza el toast.style.background/border/color inline de
   notifications_controller.js. Mismo patrón que .alert de más arriba: la
   base fija border-style/width (sin color, así que hereda currentColor
   hasta que la variante lo pisa), cada variante resuelve el triplete desde
   el token canónico — nunca un alias `-text`. */
.toast {
  border: 1px solid;
}
.toast--success { background: var(--positive-subtle); border-color: var(--positive-border); color: var(--positive); }
.toast--error   { background: var(--negative-subtle); border-color: var(--negative-border); color: var(--negative); }
.toast--info    { background: var(--info-subtle);     border-color: var(--info-border);     color: var(--info); }

/* ── Combobox de estrategia (binance/bot_sessions/new, ola 7) ──────────────
   Opción seleccionada del dropdown de búsqueda. Reemplaza el
   opt.style.background/color inline que escribía el script de la vista: ahora
   servidor y JS togglean la MISMA clase, así que el estado tras un click es
   idéntico al que pinta el servidor. Especificidad (0,2,0) a propósito: le
   gana a la .text-secondary de reposo del mismo elemento. */

/* Hover de fondo (ola 12): mismo bug de capas que .link-* (ola 9) — Tailwind
   v4 emite hover:bg-[var(…)] dentro de @layer utilities y esta clase, sin
   capa, siempre le ganaba, así que el fondo nunca cambiaba al pasar el mouse.
   Especificidad (0,2,0), igual que .is-selected de abajo: por eso va ANTES en
   el archivo — a empate de especificidad gana la regla que está más abajo, y
   así la opción ya elegida no pierde su fondo al pasar el mouse por otra fila
   del combobox. */
.strategy-option { transition: background var(--duration-base) var(--ease-out); }
.strategy-option:hover { background: var(--surface-overlay); }

.strategy-option.is-selected {
  background: var(--accent-subtle);
  color: var(--accent-text);
}

/* ── Chat markdown (llm_configurations/chat, ola 8) ───────────────────────
   Markdown renderizado del chat de IA (data-controller="markdown" inyecta
   este HTML desde el Stimulus controller sobre .chat-markdown). Vivía como
   <style> inline en la vista, fijo a tema oscuro (19 hex) — en tema claro se
   veía roto. Movido acá tal cual: mismos selectores, mismos tamaños, mismos
   márgenes, solo tokenizados. Mapeo hex→token documentado en la orden de la
   ola 8; el lila que tenía `em` no entra como violeta (spec §6-bis punto 3),
   pasa a --text-primary igual que strong. */
.chat-markdown h1, .chat-markdown h2, .chat-markdown h3,
.chat-markdown h4, .chat-markdown h5, .chat-markdown h6 {
  color: var(--text-primary); font-weight: 600;
  margin-top: 1em; margin-bottom: 0.35em; line-height: 1.3;
}
.chat-markdown h1 { font-size: 1.15em; border-bottom: 1px solid var(--border-default); padding-bottom: 0.2em; }
.chat-markdown h2 { font-size: 1.05em; }
.chat-markdown h3 { font-size: 0.97em; color: var(--text-secondary); }
.chat-markdown p  { margin-bottom: 0.6em; line-height: 1.65; }
.chat-markdown ul, .chat-markdown ol { margin: 0.4em 0 0.6em 1.4em; line-height: 1.65; }
.chat-markdown li { margin-bottom: 0.15em; }
.chat-markdown ul { list-style-type: disc; }
.chat-markdown ol { list-style-type: decimal; }
.chat-markdown code {
  background: var(--surface-sunken); border: 1px solid var(--border-default); border-radius: 4px;
  padding: 0.1em 0.4em; font-family: 'Courier New', monospace;
  font-size: 0.84em; color: var(--accent-text);
}
.chat-markdown pre {
  background: var(--surface-canvas); border: 1px solid var(--border-default); border-radius: 8px;
  padding: 0.85em 1em; overflow-x: auto; margin: 0.55em 0;
}
.chat-markdown pre code {
  background: none; border: none; padding: 0; color: var(--text-secondary); font-size: 0.81em;
}
.chat-markdown blockquote {
  border-left: 3px solid var(--border-strong); padding-left: 0.85em;
  color: var(--text-tertiary); margin: 0.55em 0; font-style: italic;
}
.chat-markdown strong { color: var(--text-primary); font-weight: 600; }
.chat-markdown em    { color: var(--text-primary); }
.chat-markdown a     { color: var(--accent-text); text-decoration: underline; }
.chat-markdown hr    { border-color: var(--border-default); margin: 0.75em 0; }
.chat-markdown table { border-collapse: collapse; width: 100%; margin: 0.55em 0; font-size: 0.84em; }
.chat-markdown th, .chat-markdown td {
  border: 1px solid var(--border-default); padding: 0.3em 0.6em; text-align: left;
}
.chat-markdown th { background: var(--surface-sunken); color: var(--text-secondary); font-weight: 600; }
.chat-markdown tr:nth-child(even) { background: var(--surface-canvas); }
.chat-markdown > *:last-child { margin-bottom: 0; }

/* ── Links con :hover real (ola 9) ─────────────────────────────────────────
   Reemplaza el idiom viejo: una utilidad `hover:text-` de Tailwind con un
   valor arbitrario entre corchetes apuntando a una custom property (algo
   como `var(--text-primary)`) — aparecía en 17 lugares de 13 vistas y NUNCA
   cambiaba de color: Tailwind v4 emite sus utilidades (incluidas las de
   :hover) dentro de @layer utilities, y este archivo —como base.css— va sin
   @layer. En CSS lo
   sin-capa le gana siempre a lo con-capa, tenga la especificidad que tenga,
   así que el color de reposo (`.text-tertiary`, `.text-accent`…) nunca le
   soltaba el paso al hover de Tailwind escrito encima. Es el mismo problema
   de capas que ya mordió tres veces a `.table` (ver el comentario de
   `:where(.table) td` más arriba) — el bug nunca fue de color, es de capas.
   La solución es un :hover propio, sin capa, que gana por construcción.

   `.link-accent` NO baja a --accent-hover en :hover: en tema claro
   --accent-hover queda debajo del piso de 4,5:1 sobre el lienzo (spec §2), y
   la decisión 1 de esa sección es explícita en que el texto acentuado usa
   --accent-text, que sí lo pasa (5,2:1). El hover ahí es solo el subrayado;
   el color no se mueve. */
.link-muted {
  color: var(--text-tertiary);
  transition: color var(--duration-base) var(--ease-out);
}
.link-muted:hover { color: var(--text-primary); }

.link-quiet {
  color: var(--text-secondary);
  transition: color var(--duration-base) var(--ease-out);
}
.link-quiet:hover { color: var(--text-primary); }

.link-accent {
  color: var(--accent-text);
  transition: color var(--duration-base) var(--ease-out);
}
.link-accent:hover { text-decoration: underline; }

/* Sin color propio: hereda el del contexto en reposo (ej. dentro de un <td>
   que ya trae su color) y solo define qué pasa en :hover. */
.link-hover-accent {
  transition: color var(--duration-base) var(--ease-out);
}
.link-hover-accent:hover { color: var(--accent-text); }

/* Variante para hover del ANCESTRO (`.group` de Tailwind), no del propio
   elemento — ej. una etiqueta que cambia de color cuando se pasa el mouse
   por todo el `<label>`, no solo por el texto. */
.link-muted-group {
  color: var(--text-tertiary);
  transition: color var(--duration-base) var(--ease-out);
}
.group:hover .link-muted-group { color: var(--text-primary); }

/* ── Panel de detalle de fila (ola 10) ─────────────────────────────────────
   Genérico: cualquier tabla puede abrir un <tr class="row-detail" hidden>
   justo debajo de la fila de datos. Nació en Posiciones para sacar
   Apalancamiento y Liquidación de la tabla —vacías la mayoría de las veces:
   leverage no existe en Polymarket ni en spot, liquidación solo aplica a
   futuros apalancados— sin dejar de mostrarlas. Ver row_detail_controller.js. */
.row-toggle {
  display: inline-flex;
  align-items: center;
  background: transparent;
  border: 0;
  padding: 0;
  color: var(--text-tertiary);
  cursor: pointer;
}
.row-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.row-toggle span {
  display: inline-block;
  transition: transform var(--duration-fast);
}
.row-toggle[aria-expanded="true"] span { transform: rotate(90deg); }

/* Padding propio: `.table td` deja var(--space-3) en los cuatro lados, que en
   una fila de datos está bien pero deja al panel pegado a la fila de arriba y
   a la de abajo — abierto se lee como si fuera otra fila más, no como un
   bloque aparte. */
.row-detail > td { background: var(--surface-sunken); padding: var(--space-4) var(--space-4) var(--space-5); }

/* Tabla ADENTRO de un panel de detalle: la de las corridas de una estrategia.
   El padding normal de `.table td` es el correcto para una tabla que es la
   pantalla; acá compite por espacio con la curva y los números de al lado, y
   seis filas con ese aire empujan el panel a media pantalla de alto. */
.table--compact th,
.table--compact td { padding: var(--space-1) var(--space-2); }
.table--compact { background: transparent; }

/* Explícito a propósito, aunque hoy ninguna otra regla le fija `display` a
   este <tr> (el `[hidden]` del user-agent stylesheet ya le gana por su
   cuenta a `tr { display: table-row }`, también del UA — mismo origen, más
   especificidad). El motivo es no repetir la trampa de `.ws-menu` (spec
   §6-bis): CUALQUIER regla de autor que en el futuro le toque `display` a
   `.row-detail` —tenga o no @layer— le gana al stylesheet del navegador sin
   importar especificidad, y `[hidden]` dejaría de andar en silencio. Fijarlo
   acá hace que ese día la regla ya exista. */
.row-detail[hidden] { display: none; }

/* Cada par etiqueta/valor es UN item de la grilla, no dos. Con <dt> y <dd>
   sueltos como hijos directos la grilla los reparte por separado y a anchos
   reales los pares se parten: la etiqueta queda al final de una fila y su
   valor arranca la siguiente, así que el panel se lee como una lista de
   palabras sin dueño. Verificado con el panel abierto en /positions antes de
   corregirlo. HTML5 permite envolver cada grupo dt+dd en un <div> dentro de
   un <dl> justamente para esto. */
.row-detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-4) var(--space-6);
}
.row-detail-item dt { margin-bottom: 0.15rem; color: var(--text-tertiary); }
.row-detail-item dd { margin: 0; }
/* El texto no llega gris por un selector que le gane a este — `:where(.table)
   td` (ola 5) ya baja su propia especificidad a (0,0,1) a propósito para no
   aplastar una celda coloreada — sino por HERENCIA: el <td colspan="11"> de
   este panel sí matchea esa regla y queda en --text-secondary, y <dl>/<dt>/
   <dd> heredan ese color si nadie lo pisa. Sin esta línea el panel se lee
   apagado en los dos temas (es la misma familia de bug que ya mordió tres
   veces a `.table`, ver el comentario de `:where(.table) td` más arriba). */
.row-detail-grid dd { color: var(--text-primary); }

/* ── Hover de borde y fondo (ola 12) ────────────────────────────────────────
   Misma trampa que mató a `.link-*` en la ola 9, pero en borde y fondo en vez
   de texto: `hover:border-[var(--info)]`, `hover:border-[var(--warning)]` y
   `hover:bg-[var(--surface-overlay)]` escritos encima de una clase del design
   system (`.card`…) NUNCA cambiaron nada. Tailwind v4 emite sus utilidades
   —incluidas las de `:hover`— dentro de `@layer utilities`; este archivo,
   como base.css, va sin capa, y en CSS lo sin-capa le gana siempre a lo
   con-capa, con cualquier especificidad. Dos de los nueve hovers muertos
   estaban además tapados por un `style="border-color:…"` inline en el mismo
   elemento: un estilo inline le gana a toda regla CSS, con capa o sin capa,
   así que esos dos no tuvieron ninguna chance ni antes del bug de capas. La
   solución es la misma que la de `.link-*`: una clase del design system con
   su propio `:hover`, sin `!important`, y el `style` inline afuera.

   `.mode-option[data-mode=…].is-selected` tiene especificidad (0,3,0), igual
   que su `:hover` con el mismo atributo — por eso las reglas `.is-selected`
   van DESPUÉS acá abajo: a especificidad empatada gana la que está más abajo
   en el archivo, y así el estado seleccionado le gana al hover a propósito.
   El color de modo no se pierde al pasar el mouse por encima de la tarjeta
   ya elegida. */

/* Tarjetas de modo del formulario de sesión de Polymarket */
.mode-card { border-width: 2px; }
.mode-card--info          { border-color: var(--info-border); }
.mode-card--info:hover    { border-color: var(--info); }
.mode-card--warning       { border-color: var(--warning-border); }
.mode-card--warning:hover { border-color: var(--warning); }

/* Filas de modo del formulario de sesión de Binance (las usa OTRO implementador
   esta misma ola, en app/views/binance/bot_sessions/new.html.erb) */
.mode-option {
  border-color: var(--border-default);
  transition: border-color var(--duration-base) var(--ease-out);
}
.mode-option:hover                          { border-color: var(--border-strong); }
.mode-option[data-mode="demo"]:hover        { border-color: var(--warning-border); }
.mode-option[data-mode="live"]:hover        { border-color: var(--negative-border); }
.mode-option[data-mode="simulation"].is-selected { border-color: var(--info); }
.mode-option[data-mode="demo"].is-selected       { border-color: var(--warning); }
.mode-option[data-mode="live"].is-selected       { border-color: var(--negative); }

/* Burbujas del chat con el asistente (chat_widget_controller.js las arma desde JS).
   El widget tenía su propio juego de colores fijos a tema oscuro y por eso se quedó
   con el diseño viejo cuando el resto de la app cambió: al vivir en un template de
   JavaScript, ninguna auditoría de vistas lo veía. */
.chat-bubble {
  border-radius: var(--radius-xl);
  padding: 0.5rem 0.75rem;
  line-height: 1.6;
  max-width: 88%;
}
.chat-bubble--user      { background: var(--accent); color: var(--brand-mark-ink); }
.chat-bubble--assistant { background: var(--surface-sunken); color: var(--text-primary); }
.chat-bubble--error {
  background: var(--negative-subtle);
  border: 1px solid var(--negative-border);
  color: var(--negative);
}
/* Estado "pensando" y traza de herramientas: mismo cuerpo, tinta atenuada. */
.chat-bubble--thinking { background: var(--surface-sunken); color: var(--text-tertiary); }
.chat-dot {
  width: 0.375rem; height: 0.375rem; border-radius: var(--radius-full);
  background: currentColor; display: inline-block;
}

/* Radio presentado como tarjeta elegible (elegir tipo de reporte: error o sugerencia).
   Se distingue de .mode-option en que acá NO hay JS: el estado sale del :checked del
   propio <input>, que es hermano anterior del <div> que se pinta. Por eso tampoco
   sirven las utilidades peer-checked/* de Tailwind — viven en @layer utilities y
   cualquier regla sin capa les gana, que es la trampa que ya mordió cuatro veces. */
.choice-card {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  transition: border-color var(--duration-base) var(--ease-out),
              background var(--duration-base) var(--ease-out);
}
.choice-card:hover { border-color: var(--border-strong); }

input:checked + .choice-card--negative {
  border-color: var(--negative);
  background: var(--negative-subtle);
  color: var(--negative);
}
input:checked + .choice-card--info {
  border-color: var(--info);
  background: var(--info-subtle);
  color: var(--info);
}
/* El foco del teclado tiene que verse: el <input> está visualmente oculto, así que
   el anillo lo lleva la tarjeta. Sin esto, navegar con Tab por el formulario es a ciegas. */
input:focus-visible + .choice-card { outline: 2px solid var(--accent); outline-offset: 2px; }

/* <summary> de un <details> con cabecera tintada (mcp_docs/index) */
.details-summary {
  background: var(--surface-overlay);
  transition: background var(--duration-base) var(--ease-out);
}
.details-summary:hover { background: var(--surface-raised); }

/* ── Tarjeta plegable de consulta ──────────────────────────────────────────
   Para el bloque LARGO que se consulta y no se opera: el prompt completo de un
   slot son cientos de líneas y, desplegado de entrada, empujaba fuera de la
   pantalla lo único que la gente viene a hacer en /prompts (crear y elegir
   variantes).

   `<details>` nativo, como el menú de acciones de estrategias: se abre sin una
   línea de JavaScript —así que no depende del bundle ni pelea con la CSP
   estricta— y el navegador ya le da teclado, foco y búsqueda dentro de la
   página. El triangulito por defecto del navegador no se puede estilar en todos
   ellos, así que se apaga (`list-style` + el pseudo-elemento de WebKit) y el
   chevrón lo pone `::after`, que sí gira.

   El estado se dice DOS veces a propósito: el chevrón (color, para quien mira)
   y el "Ver"/"Ocultar" que la vista alterna con `group-open:` (palabra, para
   quien no interpreta un triángulo). */
.disclosure-card > summary {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.disclosure-card > summary::-webkit-details-marker { display: none; }
.disclosure-card > summary::after {
  content: "▾";
  display: inline-block;
  color: var(--text-tertiary);
  transition: transform var(--duration-base) var(--ease-out);
}
.disclosure-card[open] > summary::after { transform: rotate(180deg); }
/* El foco del teclado va en el <summary>, que es el elemento que se activa: sin
   esto, tabular hasta el plegable es a ciegas. */
.disclosure-card > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius-lg);
}
@media (prefers-reduced-motion: reduce) {
  .disclosure-card > summary::after { transition: none; }
}

/* Techo para un cuerpo largo: ~26rem son unas 40 líneas de mono a text-xs, de
   sobra para entender de qué va el texto. Sin tope, un prompt de 500 líneas
   vuelve inútil la barra de scroll de la página entera. */
.longform-scroll {
  max-height: 26rem;
  overflow-y: auto;
}

/* ── Tooltip propio ────────────────────────────────────────────────────────
   Reemplaza al `title` del navegador donde el dato se consulta al vuelo: en las
   barras de los gráficos del tablero y en el contador de sesiones. El nativo
   tarda casi un segundo en aparecer, no se puede estilar y no admite más de una
   línea con formato.

   `position: fixed` y colgado del <body> (lo hace tooltip_controller.js): si se
   posicionara relativo al disparador, cualquier ancestro con `overflow` lo
   recortaría, y estos viven adentro de tarjetas y de contenedores con
   overflow-x: auto.

   `pointer-events: none` para que el globo no se meta entre el cursor y la
   barra: si lo hiciera, aparecer taparía el disparador, dispararía el mouseout
   y el globo parpadearía sin parar. */
.tooltip-bubble {
  position: fixed;
  z-index: var(--z-toast);
  pointer-events: none;
  max-width: 20rem;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--border-default);
  background: var(--surface-overlay);
  color: var(--text-secondary);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-xs);
  line-height: 1.45;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
}
.tooltip-bubble.is-visible { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .tooltip-bubble { transition: none; }
}

/* Tabla chica dentro de un tooltip (el resumen de sesiones). Grilla y no una
   fila flex con space-between: con varias sesiones, el space-between alinea cada
   fila por su cuenta y los números quedan en zigzag. Con grilla, las dos
   columnas de plata caen en la misma vertical y se pueden comparar de un
   vistazo — que es para lo que uno abre el resumen. */
.tooltip-grid {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--space-1) var(--space-4);
  align-items: baseline;
  white-space: nowrap;
}

/* Encabezado de las columnas de números. Sin esto eran dos cifras pegadas sin
   decir cuál era el capital y cuál el resultado. */
.tooltip-col {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
  text-align: right;
}

/* Estado + nombre en la misma celda. El estado va con su palabra y no sólo con
   el punto de color (spec §5). */
.tooltip-session {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
}

/* Lista de etiqueta-valor dentro de un tooltip (el detalle de una sesión).
   Grilla de dos columnas y no un <dl> suelto: así los valores arrancan todos en
   la misma vertical y la lista se lee como una ficha en vez de como prosa. */
.tooltip-defs {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-4);
  align-items: baseline;
  margin: 0;
}
.tooltip-defs dt { color: var(--text-tertiary); white-space: nowrap; }
.tooltip-defs dd { margin: 0; }

/* El color por defecto del valor va con :where() por el mismo motivo que
   `:where(.table) td` unas líneas más arriba: `.tooltip-defs dd` pesa (0,1,1) y
   las clases canónicas de color de base.css (.text-negative, .pnl-positive…)
   pesan (0,1,0). Sin bajarle la especificidad, esta regla le gana a todas y
   aplana cualquier valor coloreado a --text-secondary. Pasó de verdad: el modo
   "En Vivo" llevaba .text-negative para marcar que es plata real y salía del
   mismo gris que el resto — medido en el navegador, no se veía a simple vista. */
:where(.tooltip-defs) dd { color: var(--text-secondary); }

/* Las cifras alineadas a la derecha: es lo que permite comparar dos capitales
   de largo distinto sin leerlos dígito por dígito. */
.tooltip-num { text-align: right; }

.tooltip-more {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border-subtle);
  color: var(--text-tertiary);
}
.tooltip-title {
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

/* El disparador avisa que hay algo para ver sin depender del globo. */
.tooltip-trigger { cursor: help; }

/* ── Tarjeta que se despliega ──────────────────────────────────────────────
   La tarjeta entera responde al click para abrir su panel (ver
   disclosure_controller.js). El cursor lo dice: sin esto, que se pueda hacer
   click en la tarjeta es información que sólo tiene el que la descubre de
   casualidad. Los controles de adentro —botones, links— traen su propio cursor
   y lo pisan, que es lo correcto: ahí el click hace otra cosa. */
.card--expandable { cursor: pointer; }

/* El chevron gira al abrir. Es la señal de que el panel de abajo salió de acá y
   no apareció de la nada. */
.disclosure-chevron svg {
  transition: transform var(--duration-base) var(--ease-out);
}
.disclosure-chevron[aria-expanded="true"] svg { transform: rotate(180deg); }

@media (prefers-reduced-motion: reduce) {
  .disclosure-chevron svg { transition: none; }
}

/* Efecto de hover para el HTML que genera strategy_canvas_controller.
 *
 * Esos botones traían el efecto en `onmouseover=`/`onmouseout=` inline, y la CSP
 * (`script-src 'self' 'strict-dynamic'`) bloquea los manejadores inline en TODOS
 * los entornos: un atributo `on*` no puede llevar nonce. O sea que ningún botón
 * del canvas respondía al mouse, en silencio.
 *
 * El valor de cada propiedad varía por botón —y algunos lo calculan en tiempo de
 * ejecución— así que va por variables CSS puestas en el `style=` del elemento.
 * Un `style=` inline SÍ está permitido: la CSP restringe scripts, no estilos.
 * El fallback de cada `var()` es el valor actual, así que un botón que sólo
 * define una de las tres no toca las otras dos.
 */
.ch-hover:hover {
  color: var(--ch-color, inherit);
  border-color: var(--ch-border, currentColor);
  background: var(--ch-bg, transparent);
}

/* ── Menú de acciones ────────────────────────────────────────────────────────
 *
 * `<details>` nativo y no un dropdown con JavaScript: se abre solo, funciona
 * con teclado sin escribir un handler, y no necesita nonce — que con
 * `script-src 'self' 'strict-dynamic'` no es un detalle menor.
 *
 * Nació en la pantalla de una estrategia, que tenía DIEZ botones repartidos en
 * tres grupos, dos de ellos duplicados. */
.menu-acciones { position: relative; }

/* El marcador por defecto del <summary> es un triangulito del navegador que no
   se puede estilar y rompe la caja del .btn. */
.menu-acciones > summary { list-style: none; cursor: pointer; }
.menu-acciones > summary::-webkit-details-marker { display: none; }
.menu-acciones > summary::after { content: " ▾"; opacity: 0.6; }

/* Sin esto el panel se ve SIEMPRE. El navegador oculta los hijos de un
   `<details>` cerrado con su propia regla, y el `display: flex` de acá abajo le
   gana igual que le ganaba a `[hidden]` — la misma trampa, en su versión
   hermana. Medido: 309px de alto con el menú cerrado. */
.menu-acciones:not([open]) > .menu-acciones-panel { display: none; }

.menu-acciones-panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: 30;
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  padding: var(--space-1);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.menu-item {
  display: block;
  width: 100%;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
}
.menu-item:hover { background: var(--surface-sunken); color: var(--text-primary); }
.menu-item--peligro { color: var(--negative); }
.menu-item--peligro:hover { background: color-mix(in srgb, var(--negative) 10%, transparent); }

.menu-separador {
  height: 1px;
  margin: var(--space-1) 0;
  background: var(--border-subtle);
}

/* `form_class: "contents"` en los button_to: el <form> que Rails envuelve
   alrededor del botón sería un bloque más en la columna flex y rompería el
   espaciado. Con `display: contents` el form desaparece del layout y el botón
   se alinea con los links. */
.menu-acciones-panel form.contents { display: contents; }

/* ── El árbol de la estrategia, legible ──────────────────────────────────────
 *
 * Reemplaza el volcado de JSON en un <pre>. La sangría sale de --nivel, que
 * pone la vista: así el anidamiento se ve sin generar una clase por nivel. */
.arbol-seccion { margin-bottom: var(--space-4); }
.arbol-seccion:last-child { margin-bottom: 0; }

.arbol-seccion-titulo {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.arbol-grupo { margin-left: calc(var(--nivel, 0) * var(--space-3)); }

.arbol-operador {
  display: inline-block;
  font-size: var(--text-micro);
  font-weight: 600;
  letter-spacing: 0.05em;
  padding: 1px var(--space-1);
  border-radius: var(--radius-sm);
  color: var(--text-tertiary);
  background: var(--surface-sunken);
}
.arbol-operador--or { color: var(--accent); }

/* La línea vertical es lo que hace legible el anidamiento: sin ella, tres
   niveles de sangría se leen como una lista plana con márgenes raros. */
.arbol-hijos {
  margin-left: var(--space-2);
  padding-left: var(--space-3);
  border-left: 1px solid var(--border-subtle);
}

.arbol-nodo {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 2px 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.arbol-nodo--accion { color: var(--text-primary); font-weight: 500; }

.arbol-vinieta { color: var(--text-disabled); }
.arbol-flecha  { color: var(--accent); }

.arbol-rama { margin-top: var(--space-1); }
.arbol-rama-etiqueta {
  font-size: var(--text-micro);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* ── Histograma de dispersión de backtests ───────────────────────────────────
 *
 * Reemplaza una fila de puntos sobre un eje. Aquello fallaba por dos motivos
 * independientes: el SVG iba con `preserveAspectRatio="none"` para ocupar el
 * ancho de la tarjeta, y eso escala la geometría —un círculo en un viewBox de
 * 100 unidades dentro de un contenedor de 1400 px sale 14 veces más ancho que
 * alto—; y aunque hubieran sido redondos, setenta puntos sobre una línea se
 * tapan entre sí y el montón no tiene forma legible.
 *
 * Por eso esto es HTML y no SVG: el alto de cada barra lo fija el CSS y el
 * ancho se reparte solo, así que no hay escala que deforme nada.
 */
.spread-chart {
  --spread-plot-h: 88px;
  margin-top: var(--space-1);
}

.spread-plot {
  position: relative;
  height: var(--spread-plot-h);
  border-bottom: 1px solid var(--border-default);
}

/* Las barras se apoyan en la línea de base y crecen hacia arriba. */
.spread-bars {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  /* 2px de superficie entre barras: separa sin dibujarles un borde. */
  gap: 2px;
}

.spread-bin {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: stretch;
  cursor: default;
}
.spread-bin:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.spread-bin-bar {
  display: block;
  width: 100%;
  min-height: 2px;
  /* Extremo de dato redondeado arriba y en escuadra abajo: la base es el eje,
     no un borde del gráfico. */
  border-radius: 3px 3px 0 0;
  transition: opacity var(--duration-fast) var(--ease-out);
}
.spread-bin-bar.is-win  { background: var(--positive); }
.spread-bin-bar.is-loss { background: var(--negative); }

/* El hover aclara toda la columna, no sólo los píxeles de la barra: el blanco
   de una barra de una sola corrida es un objetivo de dos píxeles. */
.spread-bin:hover .spread-bin-bar,
.spread-bin:focus-visible .spread-bin-bar { opacity: 0.75; }

/* ── Las dos referencias ─────────────────────────────────────────────────────
 *
 * Eran dos hairlines grises casi idénticas y sin rótulo: no había forma de saber
 * cuál era el cero y cuál la mediana. Ahora se distinguen por TRES cosas a la
 * vez —peso, color y un rótulo propio— porque una sola no alcanza: en un gráfico
 * de 88 px de alto, dos grises separados por una diferencia de opacidad se leen
 * igual.
 *
 * El cero es la referencia del dominio (ganó/perdió) y va sobria, porque el
 * cambio de color de las barras ya lo marca. La mediana es una estadística
 * calculada sobre estos datos: va más marcada y en el color de acento, que es
 * el que en toda la app significa "esto lo calculó el sistema".
 */
.spread-rule {
  position: absolute;
  top: 0;
  bottom: 0;
  pointer-events: none;
  /* Por ENCIMA de las barras. `.spread-bars` es absoluto y va después en el
     DOM, así que sin esto las tapaba justo en la zona densa — que es donde uno
     quiere saber de qué lado de la mediana cae el montón. */
  z-index: 1;
}
.spread-rule--zero {
  width: 1px;
  background: var(--border-default);
}
.spread-rule--median {
  width: 2px;
  margin-left: -1px;   /* centrada en su posición, no corrida a la derecha */
  background: var(--accent);
  opacity: 0.85;
}

/* La banda de rótulos vive ARRIBA del plot: adentro, una barra alta taparía el
   texto justo en el caso interesante (mucha densidad alrededor de la mediana). */
.spread-marks {
  position: relative;
  height: 15px;
  margin-bottom: 2px;
}
.spread-mark {
  position: absolute;
  bottom: 0;
  font-size: var(--text-micro);
  line-height: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* De qué lado del eje cuelga el texto. Sin esto, una referencia contra el borde
   derecho se sale del gráfico. */
.spread-mark.is-right { padding-left: 4px; }
.spread-mark.is-left  { transform: translateX(-100%); padding-right: 4px; }

/* Los rótulos van en tinta de texto, no en el color de la marca: un texto
   pintado del color de la serie se lee peor y no hace falta — el color lo carga
   la línea que tiene justo debajo. La mediana se distingue del cero por el peso
   tipográfico, que es lo mismo que las distingue en importancia. */
.spread-mark--zero   { color: var(--text-disabled); }
.spread-mark--median { color: var(--text-secondary); font-weight: 600; }

.spread-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 3px;
  font-size: var(--text-micro);
  color: var(--text-disabled);
  font-variant-numeric: tabular-nums;
}

.spread-legend-swatch {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  flex-shrink: 0;
}
.spread-legend-swatch--win  { background: var(--positive); }
.spread-legend-swatch--loss { background: var(--negative); }

/* ── Selector de variables del editor de prompts ──────────────────────────
   Cada botón de la columna derecha de /prompts → Nueva variante. No es un
   `.badge` ni una `.filter-pill` a propósito: esos son etiquetas de una línea y
   acá hacen falta TRES datos por variable —el nombre que se inserta, qué es, y
   qué forma tiene el valor real— porque el problema de la pantalla no es
   "elegir de una lista", es "entender qué le vas a pedir al modelo". Con
   diecinueve variables (el asesor local) una pared de pastillas iguales no se
   lee: se escanea con la vista y se abandona.

   El estado `is-used` lo pone el controlador de Stimulus mirando el cuerpo, y
   viaja también en `aria-pressed` para quien no ve el color.

   La primera versión no se leía como un control: `border: 1px solid transparent`
   y ningún fondo, o sea nada hasta que le pasabas el mouse por encima. Un botón
   que sólo existe en :hover es invisible para quien no sabe que hay algo ahí
   —que es exactamente quien entra a esta pantalla por primera vez— y no existe
   en un touch. Ahora trae relieve propio (`--surface-raised` sobre la `.card`,
   que es `--surface-base`) y borde visible desde el primer vistazo. */
.var-pick {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  transition: background var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
  cursor: pointer;
}
.var-pick:hover {
  background: var(--surface-overlay);
  border-color: var(--border-strong);
}
.var-pick:active { border-color: var(--accent-border); }
/* La misma trampa de capas de `.alert.hidden`: esta clase fija `display`, así
   que le gana al `[hidden]` de la hoja del navegador y el filtro del buscador
   no escondería nada. */
.var-pick[hidden] { display: none; }
/* El foco por teclado NO se apoya en el borde: con `is-used` puesto el borde ya
   es de acento y los dos estados se confundirían. El anillo (dos sombras: halo
   suave + línea nítida) se ve igual con cualquier borde debajo. */
.var-pick:focus-visible {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus-ring);
}

/* Las tres líneas eran --text-xs, --text-xs y 0.65rem separadas por 0.1rem y
   0.15rem: tres renglones del mismo tamaño y casi del mismo color, o sea una
   masa gris de la que no se despega nada. La jerarquía de ahora no es sólo de
   tamaño (la escala del spec §3 va de 10,5px a 13,5px, no da para tres
   escalones) sino de FAMILIA, PESO, COLOR y AIRE, que juntos alcanzan:

     nombre  — mono 600 sobre tinta primaria: es el título y lo que se inserta;
     qué es  — sans 400 sobre tinta secundaria, con interlineado de prosa;
     ejemplo — mono chico sobre tinta terciaria: un dato de apoyo, no una frase. */
.var-pick__name {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}
.var-pick__desc {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  margin-top: var(--space-1);
}
/* El valor de ejemplo, en una línea y recortado: es una muestra de la FORMA del
   dato, no el dato. Un bloque de noticias de 3000 caracteres no puede empujar
   la columna. La flecha va en `::before` —y no como texto del HTML— porque es
   puntuación que dice "esto es lo que sale", no contenido: copiar el ejemplo no
   se la lleva puesta, y no hay que traducirla. */
.var-pick__example {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  color: var(--text-tertiary);
  margin-top: var(--space-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.var-pick__example::before {
  content: "→ ";
  color: var(--text-disabled);
}

/* Ya está en el prompt. El tilde va en `::after` y no como carácter del HTML
   para que seleccionar el nombre no se lo lleve puesto. Ahora que el botón
   tiene fondo propio, el relleno de acento solo no alcanzaba para separarlo del
   estado normal: lleva también el borde tintado. */
.var-pick.is-used {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
}
.var-pick.is-used:hover { background: var(--accent-subtle); border-color: var(--accent); }
.var-pick.is-used .var-pick__name { color: var(--accent-text); }
.var-pick.is-used .var-pick__name::after {
  content: " ✓";
  color: var(--accent-text);
}

/* La lista dentro de un grupo. Los botones venían pegados uno contra otro, así
   que diecinueve variables eran un bloque continuo sin costuras: con relieve
   propio y sin aire entre medio se leerían como una sola caja rayada. */
.var-pick-list {
  display: grid;
  /* `minmax(0, 1fr)` y no el `auto` implícito: una pista `auto` se dimensiona
     por el max-content de lo que tiene adentro, y adentro está
     `.var-pick__example` con `white-space: nowrap`. Con un ejemplo largo (el
     bloque de noticias, la sección de posición) la columna crecía hasta el ancho
     del texto entero —medido: 448px dentro de un panel de 277— y el `ellipsis`
     no recortaba nada porque nunca le faltaba lugar. El `0` de mínimo es el que
     rompe el piso automático de max-content de los items de grid. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* El alto del panel. Estaba en `max-h-96` (384px) al lado de un editor de 505px:
   sobraban 120px de aire abajo y faltaba lista arriba. Vive acá y no como
   utilidad de Tailwind porque un valor arbitrario obligaría a recompilar el
   bundle para verlo. */
.var-pick-scroll {
  max-height: 31rem;
  overflow-y: auto;
}

/* ── Editor de prompts: el espejo pintado ─────────────────────────────────
   El cuerpo se edita en un `<textarea>` de texto plano, donde `%{symbol}` se ve
   igual que la prosa de al lado — y el modo de fallar de esta pantalla es UNA
   LETRA (`%{simbol}` se guarda igual de bien, el resolver no lo sustituye, y al
   modelo le llega la llave cruda).

   La técnica es la superposición: un `<pre>` espejo detrás con el mismo texto y
   los `%{...}` envueltos en `<mark>`, y el textarea encima con la tinta
   transparente y el caret visible. NO `contenteditable`: eso rompe el undo
   nativo, el pegado de texto plano y el comportamiento de las teclas.

   Todo lo que decide DÓNDE CAE CADA CARÁCTER va en la regla compartida de abajo
   y en ningún otro lado: familia, tamaño, interlineado, tracking, tabulación,
   padding, ancho de borde, radio y el modo de cortar renglones. Un solo píxel de
   diferencia y el texto pintado deja de coincidir con el que estás escribiendo.

   `scrollbar-gutter: stable` en LOS DOS es parte de esa lista aunque no lo
   parezca: el textarea saca una barra de scroll en cuanto el prompt pasa de una
   pantalla, esa barra le come ancho a la caja, y el espejo —que no tiene barra—
   cortaría los renglones un poco más a la derecha. Reservando el canal siempre y
   en los dos, el ancho de corte es el mismo con y sin scroll.

   Sin transiciones a propósito: pintar el texto mientras se escribe es un evento
   por tecla y cualquier animación acá sería un parpadeo permanente. (La regla
   global de `prefers-reduced-motion` vive en base.css; acá directamente no hay
   nada que reducir.) */
.prompt-editor { position: relative; }

.prompt-editor__mirror,
.textarea.prompt-editor__input {
  /* `display: block` no es cosmética: un `<textarea>` es `inline-block` por
     defecto y arrastra el espacio para las colas de las letras del renglón que
     lo contiene. Medido acá: el envoltorio quedaba 4,25px más alto que el campo,
     y el espejo —que es `inset: 0`— asomaba con su fondo por abajo del borde. */
  display: block;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  letter-spacing: normal;
  tab-size: 2;
  /* Los dos que salieron de comparar las 686 propiedades computadas de las dos
     capas en el navegador, y que ninguna lectura del CSS habría encontrado:
       · `text-rendering` — el `<pre>` heredaba `optimizeLegibility` (base.css) y
         el textarea se quedaba en `auto`. Con ligaduras y kerning distintos, el
         ancho de avance de cada glifo cambia y el texto se corre de a poquito;
       · `unicode-bidi` — la hoja del navegador le pone `isolate` a `<pre>` y deja
         al textarea en `normal`. Sólo se nota con texto RTL adentro del prompt,
         que es raro pero no imposible.
     Van declarados en los dos, con el mismo valor, para que ninguna hoja de
     usuario ni herencia futura los separe otra vez. */
  text-rendering: auto;
  unicode-bidi: normal;
  padding: 0.55rem 0.8rem;
  border-width: 1px;
  border-style: solid;
  border-radius: var(--radius-md);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: normal;
  scrollbar-gutter: stable;
  margin: 0;
}

/* Decorativo puro: `aria-hidden` en la vista, sin foco (un `<pre>` no es
   focusable) y sin clicks. El texto de acá abajo es una COPIA del que ya está en
   el textarea; si se pudiera seleccionar, un Ctrl+C sobre el editor traería todo
   dos veces. */
.prompt-editor__mirror {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: none;
  overflow: hidden;
  color: var(--text-primary);
  background: var(--surface-sunken);
  border-color: transparent;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

/* Sólo cuando el controlador de Stimulus está vivo. Si el JS no cargó, el espejo
   nunca se pinta: dejar la tinta transparente igual sería un editor invisible,
   bastante peor que un editor sin colores. */
.prompt-editor--live .prompt-editor__mirror { display: block; }
.prompt-editor--live .textarea.prompt-editor__input {
  position: relative;
  z-index: 1;
  background: transparent;
  color: transparent;
  caret-color: var(--accent);
}
/* La selección la pinta el textarea, que está ARRIBA del espejo: con un color
   opaco taparía el texto pintado y seleccionar dejaría el renglón en blanco.
   `--accent-subtle` es translúcido, así que deja ver lo de abajo. */
.prompt-editor--live .textarea.prompt-editor__input::selection {
  background: var(--accent-subtle);
}

/* Los tres estados de un `%{...}`. Ni padding ni borde ni margen: cualquiera de
   los tres correría el texto pintado respecto del que estás escribiendo. El
   fondo y el subrayado no ocupan lugar, por eso son los que llevan la carga.
   `<mark>` trae fondo amarillo y tinta negra del navegador — se pisan los dos. */
.var-mark {
  padding: 0;
  margin: 0;
  border-radius: var(--radius-sm);
  font: inherit;
  background: transparent;
  color: inherit;
}
/* Válida: la conoce el slot y el resolver la va a sustituir. */
.var-mark--ok {
  background: var(--accent-subtle);
  color: var(--accent-text);
}
/* Mal escrita: el slot no la conoce. Va con el mismo rojo del aviso de abajo y
   además subrayada, porque el color solo no distingue nada para un daltónico. */
.var-mark--unknown {
  background: var(--negative-subtle);
  color: var(--negative);
  text-decoration: underline wavy;
  text-decoration-skip-ink: none;
}
/* El slot no interpola NADA: acá cualquier llave está mal, tenga el nombre que
   tenga. No es rojo de "te equivocaste de nombre" sino ámbar de "esto no va". */
.var-mark--inert {
  background: var(--warning-subtle);
  color: var(--warning);
  text-decoration: underline wavy;
  text-decoration-skip-ink: none;
}

/* ── Ayuda plegable de una pantalla ───────────────────────────────────────
   Abierta la primera vez (crear) y cerrada después (editar) — lo decide la
   vista con el atributo `open`. Es `<details>` nativo y no un panel con
   JavaScript: se abre solo, funciona sin JS y no hay handler que la CSP pueda
   bloquear. */
.help-note__body {
  padding: 0 var(--space-4) var(--space-4);
}
.help-note__item {
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  margin-top: var(--space-2);
}
.help-note__item code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-primary);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.35em;
}

/* ── Combo: elegir de una lista, o escribir lo que no está ──────────────────
   Nace del reporte "el dropdown no funciona: hago click y no expande". Era un
   <input> + <datalist>, que sugiere mientras tipeás pero NO se abre al hacer
   click, mientras el navegador le dibujaba una flecha que prometía otra cosa.
   Ver combo_modelo_controller.js. */
.combo { position: relative; }

.combo__campo { position: relative; display: flex; align-items: center; }
/* El input ocupa todo y la flecha se le monta encima: así el ancho lo sigue
   fijando `.input` y no hay que sincronizar dos cajas. El padding derecho deja
   el hueco para que el texto no pase por debajo de la flecha. */
.combo__campo .input { padding-right: 2.25rem; }

.combo__flecha {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 2.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-tertiary);
  cursor: pointer;
  background: none;
  border: none;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.combo__flecha:hover { color: var(--text-primary); }
/* tabindex="-1": el campo YA es el control accesible (role="combobox" con
   flechas y Escape). Un segundo parador de tabulación para lo mismo obliga a
   pasar dos veces por el mismo control. */

.combo__lista {
  position: absolute;
  z-index: 20;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  max-height: 17rem;
  overflow-y: auto;
  padding: var(--space-1);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
}
/* La misma trampa de capas que ya mordió con `.alert.hidden` y `.var-pick`:
   estas reglas fijan `display`, así que sin esto le ganan al [hidden] de la
   hoja del navegador y ni la lista ni las opciones filtradas se esconderían. */
.combo__lista[hidden], .combo__opcion[hidden], .combo__vacio[hidden] { display: none; }

.combo__opcion {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border: none;
  background: none;
  color: var(--text-primary);
  font-size: var(--text-xs);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.combo__opcion:hover,
.combo__opcion.is-active { background: var(--surface-overlay); }
.combo__opcion.is-active { color: var(--accent-text); }

.combo__vacio {
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

/* La nota que acompaña a la opción (el nombre visible al lado del símbolo).
   Se apaga para que el código, que es lo que se busca, siga siendo lo primero
   que se lee. */
.combo__opcion-nota {
  color: var(--text-tertiary);
  font-family: var(--font-sans);
  margin-left: var(--space-2);
}

/* Variante para una barra de herramientas, donde el combo comparte fila con
   pastillas y desplegables compactos (la cabecera del previsualizador).
   El ancho es fijo y no `auto`: la lista está posicionada contra los bordes del
   .combo, así que con ancho de contenido el desplegable se encogería al del
   símbolo más corto. */
.combo--compacto { width: 11rem; }
.combo--compacto .combo__campo .input { padding-right: 1.75rem; }
.combo--compacto .combo__flecha { width: 1.75rem; }
.combo--compacto .combo__lista { min-width: 14rem; }

/* ── Selector de par de la ficha del instrumento ─────────────────────────────
   El combo que cambia de instrumento sin volver al listado. Es el mismo control
   que los otros dos —misma caja, misma lista, mismo teclado—, con dos
   diferencias que vienen de lo que muestra: cada opción trae símbolo, mercado,
   nombre, precio y cambio del día, así que la fila es una grilla de dos
   columnas en dos renglones, y la lista tiene que ser MÁS ANCHA que el campo.

   El campo mide 12rem porque vive en la fila de navegación, al lado del
   "Volver". El ancho es fijo y no `auto` por lo mismo que `.combo--compacto`:
   la lista se posiciona contra los bordes del `.combo`. */
.combo--par { width: 12rem; }
.combo--par .combo__campo .input { padding-right: 1.75rem; }
.combo--par .combo__flecha { width: 1.75rem; }
/* `min()` y no `24rem` pelado: la lista arranca en el borde izquierdo del campo
   y a 375 px de viewport 24rem se le van de la pantalla por la derecha. */
.combo--par .combo__lista {
  min-width: min(24rem, calc(100vw - 2rem));
  max-height: 21rem;
}

/* La fila: [ símbolo + mercado ] … [ precio ]
            [ nombre           ] … [ cambio ]
   `baseline` alinea el símbolo con el precio aunque midan distinto. */
.combo__opcion--par {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--space-3);
  row-gap: 0.1rem;
  align-items: baseline;
  text-decoration: none;
}
.selector-par__par {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.selector-par__simbolo { font-size: var(--text-xs); }
.selector-par__precio {
  text-align: right;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}
/* El nombre y el cambio son el renglón de apoyo: más chicos y apagados, para
   que el símbolo siga siendo lo primero que se lee. */
.selector-par__nombre {
  font-size: 10px;
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.selector-par__cambio {
  text-align: right;
  font-size: 10px;
  font-family: var(--font-mono);
}
/* El par que ya estás mirando. No se saca de la lista —sacarlo correría todo lo
   demás de lugar cada vez que cambiás de par— pero se marca. */
.selector-par__actual {
  font-size: 10px;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.combo__opcion--par.is-current { background: var(--surface-overlay); }

/* ── Cerrar una posición: entera o una parte ─────────────────────────────────
   El panel que aparece DENTRO del modal de confirmación al apretar "Cerrar"
   (shared/_close_choice.html.erb). Antes esto era un formulario suelto colgado
   del panel de detalle de la fila (.partial-close); se mudó al modal porque el
   botón de cerrar asumía "todo" y la porción quedaba en una pantalla que hay
   que desplegar.

   Adentro del modal no lleva caja ni fondo propios: el .modal-panel ya es la
   caja, y una caja dentro de otra se lee como si fuera otra pantalla. Una línea
   alcanza para decir "acá arranca la decisión, arriba estaba el aviso".

   El alto máximo es para el teléfono acostado: con el modal centrado y 380px de
   alto de viewport, sin esto el panel crece hasta que los botones de Confirmar y
   Cancelar quedan fuera de la pantalla y no hay forma de cerrarlo. */
.close-choice {
  border-top: 1px solid var(--border-subtle);
  padding-top: var(--space-4);
  max-height: 55vh;
  overflow-y: auto;
}

.close-choice__modes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-3) 0;
}

/* Cada opción es un <label> con su radio adentro: el clic en cualquier parte de
   la pastilla marca la opción, y el teclado la recorre sin ayuda. El radio no se
   esconde con display:none —eso lo saca del orden de tabulación— sino que se
   dibuja chiquito al lado del texto. En un control que manda una orden a
   mercado, ver cuál está marcado importa más que la pastilla perfecta. */
.close-choice__option {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  /* 2.25rem de alto: el mínimo cómodo para el pulgar en un teléfono, que es
     donde la columna de acciones de la tabla ni siquiera se ve. */
  min-height: 2.25rem;
  padding: 0.3rem 0.85rem;
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out);
}
.close-choice__option:hover { border-color: var(--border-strong); color: var(--text-primary); }
.close-choice__option:has(input:checked) {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent-text);
}
.close-choice__option input { accent-color: var(--accent); margin: 0; }

.close-choice__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Los atajos. Copian la forma de .filter-pill —misma altura, mismo radio— pero
   con clase propia: .filter-pill nació para el <a> de un filtro y no fija
   `background`, así que un <button> con esa clase se dibuja con el fondo gris
   del user-agent. */
.close-choice__quick {
  display: inline-flex;
  align-items: center;
  background: transparent;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  min-height: 2.25rem;
  padding: 0.28rem 0.7rem;
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out);
}
.close-choice__quick:hover { border-color: var(--border-strong); color: var(--text-primary); }
.close-choice__quick.is-active {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent-text);
}

/* Ancho fijo: `.input` es width:100% y en una fila flex se estira hasta empujar
   los atajos fuera de su lugar. Dos dígitos y las flechitas. */
.close-choice__pct { width: 4.75rem; text-align: right; }

/* El renglón que hace que apretar no sorprenda: qué porcentaje, cuánta cantidad
   sobre el total, cuánto queda, y el aviso cuando lo que se va a ejecutar no es
   lo que se pidió. Va en --text-secondary y no en terciario: se tiene que leer.
   El alto mínimo evita que el modal salte de alto al recalcular. */
.close-choice__note {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  margin-top: var(--space-3);
  min-height: 2.25rem;
}
.close-choice__note--warning { color: var(--warning); }
.close-choice__note--error   { color: var(--negative); }

/* Por qué esta posición no admite cierre parcial (Polymarket). Un hueco sin
   explicación deja al usuario buscando qué le falta. */
.close-choice__unavailable {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-top: var(--space-2);
}

/* "Sale a precio de mercado" va SIEMPRE visible y no sólo en el renglón de
   previsualización: es la diferencia entre "salgo ya" y "salgo si llega a X", y
   con el libro moviéndose no son lo mismo. */
.close-choice__market {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-top: var(--space-2);
}


/* ── Noticias: la lectura del modelo ──────────────────────────────────────
   Un titular clasificado trae cuatro datos que la persona necesita separar de
   un vistazo: hacia dónde empuja el precio, cuánto, qué clase de hecho es y en
   qué plazo. La primera versión los dibujó todos a 10px en el mismo gris
   apagado que la fuente y la hora, y el resultado fue que no se veía ninguno:
   estaban en el DOM y no en la pantalla.

   Dos decisiones que vienen de ahí:

   1. La DIRECCIÓN no se codifica sólo con color. Lleva flecha (↑ ↓ —) además
      del color, porque un punto rojo de 6px no es legible y porque el color
      solo excluye a quien no lo distingue.
   2. La pastilla se dibuja SIEMPRE, también cuando el modelo dijo "sin
      impacto". Que el modelo haya mirado y concluido que no mueve nada es
      información, no ausencia de información — y omitirla dejaba 33 de 46
      filas sin ninguna marca. La variante neutra va apagada para que las
      fuertes resalten. */
.news-impact {
  display: inline-flex;
  align-items: center;
  gap: 0.28em;
  flex-shrink: 0;
  border-radius: var(--radius-full);
  padding: 0.1rem 0.5rem;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}
.news-impact--up   { background: var(--positive-subtle); color: var(--positive); }
.news-impact--down { background: var(--negative-subtle); color: var(--negative); }
.news-impact--flat { background: var(--neutral-subtle);  color: var(--text-secondary); }

/* El tipo de evento va como contorno y no relleno: es una categoría, no una
   alarma, y compitiendo en peso con la pastilla de impacto le robaría la
   atención a la que sí es urgente. */
.news-event {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  padding: 0.05rem 0.45rem;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* El porqué del modelo, separado con una guía a la izquierda. Sin ella quedaba
   con el mismo tamaño y color que la línea de fuente y hora, y se leía como
   más metadata en vez de como el argumento. */
.news-why {
  border-left: 2px solid var(--border-default);
  padding-left: var(--space-2);
  color: var(--text-secondary);
}

/* ── El gráfico del instrumento, maximizado ──────────────────────────────────
 *
 * Se maximiza la TARJETA ENTERA y no sólo el lienzo, porque el controlador de
 * Stimulus vive en la tarjeta: llevándose la tarjeta se lleva la barra de
 * herramientas, el selector de timeframe, los indicadores y el interruptor de
 * noticias. Maximizar sólo el lienzo daría un gráfico grande donde no se puede
 * dibujar, que es la mitad del pedido sin cumplir.
 *
 * `position: fixed` sobre el viewport en vez de la API nativa de pantalla
 * completa. La nativa saca al usuario del contexto de la app —desaparece la
 * navegación, el navegador dibuja su propio aviso— y no deja poner nada por
 * encima del elemento promovido, lo que rompería el globo de las noticias y el
 * panel de estilo de los dibujos, que son `position: absolute` sobre la
 * tarjeta. Con `fixed` la app sigue siendo la app.
 *
 * El alto del lienzo lo maneja el JS, no el CSS: lightweight-charts dibuja en
 * un canvas y sólo cambia de tamaño por `applyOptions`. Acá se le da el LUGAR
 * (`flex: 1` y `min-height: 0`) y el controlador lee `clientHeight` y se lo
 * pasa al gráfico. Sin el `min-height: 0` un hijo flex no baja de su contenido
 * y el lienzo empujaría la tarjeta fuera de la pantalla.
 */
.chart-card--maximizado {
  position: fixed;
  inset: 0;
  z-index: 60;
  margin: 0;
  border-radius: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface-base);
}

.chart-card--maximizado > .chart-canvas-container {
  flex: 1;
  min-height: 0;
  height: auto;
}

/* El scroll del fondo se bloquea mientras la tarjeta ocupa la pantalla: si no,
 * la rueda sobre el gráfico mueve la página que hay detrás. */
body.chart-maximizado-abierto {
  overflow: hidden;
}


/* ── La marca ──────────────────────────────────────────────────────────────────
   El SVG lo dibuja `app/views/shared/_marca.html.erb`. Acá va sólo cómo se
   comporta adentro de un layout: el color del NOMBRE, que es lo único de la
   marca que cambia con el tema —el isotipo no, porque el clay y el marfil valen
   igual en los dos— y por eso el nombre va en `currentColor`.
   ────────────────────────────────────────────────────────────────────────── */
.marca {
  display: block;
  flex-shrink: 0;
  color: var(--text-primary);
}

.marca--sola { width: auto; }

/* En la barra de la app: al lado del selector de workspace, con un separador
   que la despega sin dibujar una línea más. */
.topbar-marca {
  display: flex;
  align-items: center;
  padding-right: var(--space-3);
  margin-right: var(--space-1);
  border-right: 1px solid var(--border-subtle);
  text-decoration: none;
}

.topbar-marca:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
  border-radius: var(--radius-sm);
}

/* En una sección pública: arriba del título, con aire abajo. */
.noticias-hero__marca {
  display: inline-flex;
  margin-bottom: var(--space-4);
  text-decoration: none;
}

.noticias-hero__marca:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
  border-radius: var(--radius-sm);
}
/* ─────────────────────────────────────────────────────────────────────────
   El canvas del chart SÍ sigue el tema. instrument_chart_controller.js lo pinta
   con chartToken("--surface-sunken") y se re-suscribe a los cambios de tema, así
   que en claro el chart es marfil y en oscuro es carbón.

   Este archivo decía lo contrario ("intentionally stay dark-surfaced in BOTH
   themes") y tenía el contenedor clavado en #0f172a, azul marino de la paleta
   vieja. Resultado visible: en tema claro el canvas pintaba marfil y la franja
   que sobraba abajo del chart quedaba azul oscura, sin relación con nada. El
   comentario había quedado vencido cuando el JS pasó a seguir el tema.

   Regla: todo lo de acá usa los MISMOS tokens que usa el JS, para que el
   contenedor y el canvas no puedan desincronizarse nunca más.
   ───────────────────────────────────────────────────────────────────────── */

/* Chart loading overlay animation */
@keyframes chart-bar-load {
  0%, 100% { opacity: 0.15; transform: scaleY(0.65); }
  50%       { opacity: 0.85; transform: scaleY(1); }
}

/* Contenedor del canvas: mismo token que pinta el chart por dentro. */
.chart-canvas-container {
  background: var(--surface-sunken);
  min-height: 580px;
  position: relative;
  border-radius: var(--radius-lg);
}

.chart-canvas-container--sm {
  min-height: 460px;
}

/* Backtest / equity chart — this one DOES follow the theme (simple Chartkick
   line, not a candlestick canvas), background supplied by .chart-container */
.equity-chart-container {
  position: relative;
}

.equity-curve-chart {
  height: 280px;
}

/* Trade ticker mini-canvas. El alto de acá es el PISO y el de arranque: dentro
   de la fila de microestructura el canvas crece para llenar la tarjeta (ver
   más abajo), y market_ticker_controller.js lee su offsetHeight para darle al
   bitmap los mismos píxeles que ocupa en pantalla. */
.trade-ticker-canvas {
  height: 80px;
}

/* ─────────────────────────────────────────────────────────────────────────
   Microestructura: el libro de órdenes y el flujo de operaciones, lado a lado.

   El problema que resuelve: las dos tarjetas se leen juntas y quedaban de
   distinto alto. El intento anterior fue `items-start` en el grid, para que la
   más corta no se estirara — pero eso no las empareja, sólo evita el hueco.

   Acá se emparejan de verdad: el grid vuelve a estirar (su default) y CADA
   tarjeta tiene una pieza elástica que absorbe el alto que sobre, así que
   estirar no puede dejar media tarjeta en blanco:
     · el libro estira sus dos columnas de niveles, que además scrollean por
       dentro si algún día no entran (así el alto es una promesa que se cumple
       aunque el contenido crezca);
     · el flujo estira su canvas, que es contenido de verdad: el mismo dibujo
       de 60 segundos con más resolución vertical.

   `--alto-microestructura` es el PISO común. Está calibrado al alto natural
   del libro con sus 10 niveles (medido en el navegador: cabecera 45 px + 10
   filas de 21 px + el thead), que es la tarjeta que manda. Si algún día el
   libro creciera más, las dos crecen juntas: el mínimo no es un techo.
   ───────────────────────────────────────────────────────────────────────── */
.microestructura {
  --alto-microestructura: 19rem;
}

.microestructura-panel {
  min-height: var(--alto-microestructura);
  display: flex;
  flex-direction: column;
}

/* Lo que crece. `min-height: 0` porque un hijo flexible con contenido adentro
   no baja de su tamaño intrínseco sin esto, y entonces el scroll de adentro
   nunca aparece: se desborda la tarjeta. */
.microestructura-cuerpo {
  flex: 1 1 auto;
  min-height: 0;
}

/* El libro: cada lado es una columna que scrollea por dentro. */
.microestructura-libro > * {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.microestructura-libro .table-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* El flujo: la única pieza elástica es el canvas — el resumen de arriba y la
   leyenda de abajo miden lo que miden. */
.microestructura-flujo {
  display: flex;
  flex-direction: column;
}
.microestructura-flujo .trade-ticker-canvas {
  flex: 1 1 auto;
  min-height: 80px;
}

/* Strategy search dropdown */
.strategy-dropdown-list {
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  max-height: 260px;
  overflow-y: auto;
  border-radius: var(--radius-md);
}

/* Velo de carga: se apoya sobre el canvas, así que usa su misma superficie. */
.chart-loading-overlay {
  position: absolute;
  inset: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--surface-sunken) 93%, transparent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  pointer-events: none;
}

/* Animated candlestick bars container */
.chart-loading-bars {
  display: flex;
  align-items: flex-end;
  gap: 5px;
  height: 52px;
}

.chart-loading-bar {
  width: 6px;
  border-radius: 2px 2px 0 0;
  opacity: 0.2;
  transform-origin: bottom;
  animation: chart-bar-load 1.5s ease-in-out infinite;
}

.chart-loading-bar--green { background: var(--positive); }
.chart-loading-bar--red   { background: var(--negative); }

/* Bar heights */
.chart-loading-bar:nth-child(1) { height: 100%; }
.chart-loading-bar:nth-child(2) { height: 50%; }
.chart-loading-bar:nth-child(3) { height: 82%; }
.chart-loading-bar:nth-child(4) { height: 38%; }
.chart-loading-bar:nth-child(5) { height: 95%; }
.chart-loading-bar:nth-child(6) { height: 60%; }
.chart-loading-bar:nth-child(7) { height: 100%; }
.chart-loading-bar:nth-child(8) { height: 45%; }
.chart-loading-bar:nth-child(9) { height: 75%; }

/* Bar animation delays */
.chart-loading-bar:nth-child(1) { animation-delay: 0ms; }
.chart-loading-bar:nth-child(2) { animation-delay: 100ms; }
.chart-loading-bar:nth-child(3) { animation-delay: 200ms; }
.chart-loading-bar:nth-child(4) { animation-delay: 300ms; }
.chart-loading-bar:nth-child(5) { animation-delay: 400ms; }
.chart-loading-bar:nth-child(6) { animation-delay: 500ms; }
.chart-loading-bar:nth-child(7) { animation-delay: 600ms; }
.chart-loading-bar:nth-child(8) { animation-delay: 700ms; }
.chart-loading-bar:nth-child(9) { animation-delay: 800ms; }

/* Labels */
.chart-loading-labels {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
}

.chart-loading-label {
  color: var(--text-secondary);
  font-size: 13px;
  font-family: ui-monospace, monospace;
  font-weight: 600;
  letter-spacing: 0.07em;
}

.chart-loading-sublabel {
  color: var(--text-tertiary);
  font-size: 11px;
  letter-spacing: 0.04em;
}
/* ─────────────────────────────────────────────────────────────────────────
   Barra de herramientas del gráfico de un instrumento: indicadores a demanda
   y herramientas de dibujo.

   Nada de color propio: todo sale de los tokens. El único color que este
   archivo NO fija es el punto de la pastilla de la leyenda, que se lo pone el
   JS con el color REAL de la serie — es lo que ata la pastilla a la línea del
   gráfico. Las pastillas mismas son `.preview-chip` (strategy_canvas.css), la
   misma que usa el previsualizador del constructor: dos pastillas distintas
   para lo mismo se desfasarían.
   ───────────────────────────────────────────────────────────────────────── */

.chart-tools-selector {
  position: relative;
}

/* El selector es un popover y no un panel que empuja: elegir un indicador no
   puede mover de lugar el gráfico que estás mirando. */
.chart-tools-panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 6px);
  left: 0;
  width: 19rem;
  max-width: calc(100vw - 3rem);
  padding: 8px;
  border-radius: var(--radius-md);
}
.chart-tools-panel[hidden] { display: none; }

.chart-tools-lista {
  max-height: 20rem;
  overflow-y: auto;
  margin-top: 8px;
}

.chart-tools-grupo {
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  padding: 8px 6px 4px;
}

.chart-tools-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 5px 6px;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  color: var(--text-secondary);
  cursor: pointer;
  text-align: left;
  background: none;
  border: 0;
}
.chart-tools-item:hover { background: var(--surface-sunken); color: var(--text-primary); }
.chart-tools-item:focus-visible { outline: none; box-shadow: var(--shadow-focus-ring); }

/* Los que abren panel propio son los que se agotan contra el techo de seis, así
   que se marcan al ELEGIR y no después de que el gráfico diga que no. */
.chart-tools-item-panel {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  font-family: var(--font-sans);
}

/* El botón de editar/quitar dentro de una pastilla. */
.chart-tools-chip-btn {
  cursor: pointer;
  line-height: 1;
  padding: 0 1px;
  color: var(--text-tertiary);
  background: none;
  border: 0;
  font-size: var(--text-xs);
}
.chart-tools-chip-btn:hover { color: var(--text-primary); }
.chart-tools-chip-btn:focus-visible { outline: none; box-shadow: var(--shadow-focus-ring); }

.chart-tools-leyenda:empty { display: none; }
.chart-tools-editor[hidden] { display: none; }

.chart-tools-param {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  color: var(--text-tertiary);
}

.chart-tools-param-input { width: 5.5rem; }

/* El campo de una nota. Ancho de renglón y no de casillero: el tope son 120
   caracteres y un campo de 6 rem obligaría a escribir la nota mirando por una
   ranura. Se encoge en pantallas angostas en vez de empujar los botones de
   Aplicar y Cancelar a la fila de abajo. */
.chart-tools-texto-input {
  width: 24rem;
  max-width: 100%;
  flex: 1 1 12rem;
  font-family: var(--font-mono);
}

/* La herramienta elegida. `.is-active` es el mismo modificador que ya llevan
   las pastillas de intervalo, así que un botón prendido se ve igual en toda la
   pantalla. */
.chart-tools-herramienta.is-active {
  border-color: var(--accent-border);
  background: var(--accent-subtle);
  color: var(--accent-text);
}

/* ── El estilo de los dibujos: color, tipo de línea y grosor ───────────────
   Ningún color propio acá tampoco: las MUESTRAS se pintan por `style` desde el
   JS con el token YA RESUELTO del tema (el mismo valor con el que se pinta el
   dibujo en el canvas), que es lo único que ata la muestra a la raya. */

.chart-tools-estilo:empty { display: none; }

/* Una muestra de color. Cuadrada y no un punto: hace falta superficie para
   distinguir seis colores de un vistazo. */
.chart-tools-muestra {
  width: 18px;
  height: 18px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-default);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.chart-tools-muestra:focus-visible { outline: none; box-shadow: var(--shadow-focus-ring); }

/* La elegida lleva un anillo, no un borde de otro color: el borde es del color
   de la muestra y cambiarlo la haría mentir. */
.chart-tools-muestra.is-active {
  box-shadow: 0 0 0 2px var(--surface-base), 0 0 0 3px var(--text-primary);
}

/* Los botones de tipo de línea y de grosor: pastillas, no filas de una lista. */
.chart-tools-opcion {
  cursor: pointer;
  padding: 2px 8px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border-subtle);
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  line-height: 1.5;
}
.chart-tools-opcion:hover { border-color: var(--border-strong); color: var(--text-primary); }
.chart-tools-opcion:focus-visible { outline: none; box-shadow: var(--shadow-focus-ring); }
.chart-tools-opcion.is-active {
  border-color: var(--accent-border);
  background: var(--accent-subtle);
  color: var(--accent-text);
}

/* El tipo de línea, DIBUJADO además de nombrado: el borde de abajo de la
   pastilla es el trazo que va a salir. Sin esto, "rayada" y "punteada" son dos
   palabras que hay que probar para saber cuál es cuál. */
.chart-tools-trazo {
  border-bottom-width: 2px;
  border-bottom-color: currentColor;
}
.chart-tools-trazo--solida   { border-bottom-style: solid; }
.chart-tools-trazo--rayada   { border-bottom-style: dashed; }
.chart-tools-trazo--punteada { border-bottom-style: dotted; }

/* La pastilla del dibujo cuyo estilo se está tocando. Mismo `.is-active` que el
   botón de herramienta, para que "esto es lo que estás editando" se vea igual
   en toda la barra. */
.chart-tools-leyenda .preview-chip.is-active {
  border-color: var(--accent-border);
  background: var(--accent-subtle);
  color: var(--accent-text);
}

/* ── Noticias sobre la línea de tiempo ─────────────────────────────────────
   El interruptor y el panel de detalle que se abre al hacer click en una marca.

   Ningún color propio, como todo este archivo: las marcas sobre el canvas las
   pinta el JS con los tokens ya resueltos (`--positive` / `--negative` /
   `--text-secondary`, releídos en cada repintado porque el canvas no entiende
   var()), y el panel usa las MISMAS clases que la fila de noticias de arriba
   del gráfico (.news-impact, .news-event, .news-why, en components.css). Dos
   hojas distintas para el mismo dato terminarían mostrando dos cosas. */

/* Prendido = el mismo `.is-active` de las pastillas de intervalo y de las
   herramientas de dibujo, para que "esto está encendido" se vea igual en toda
   la barra. El estado viaja además en aria-pressed. */
.chart-news-toggle.is-active {
  border-color: var(--accent-border);
  background: var(--accent-subtle);
  color: var(--accent-text);
}

/* Cuántas hay dibujadas. Un interruptor que no dice qué prendió obliga a buscar
   las marcas con la vista para saber si hizo algo. */
.chart-news-cuenta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  opacity: 0.75;
}
.chart-news-cuenta[hidden] { display: none; }

/* El detalle: un popover SOBRE el canvas, no un panel que empuje el gráfico.
   Se abre al lado de la vela clickeada, así que tiene que taparla lo menos
   posible: ancho de lectura y alto acotado con scroll propio. */
.chart-news-panel {
  position: absolute;
  z-index: 40;
  width: 22rem;
  max-width: calc(100vw - 3rem);
  max-height: 18rem;
  overflow-y: auto;
  border-radius: var(--radius-md);
  padding: var(--space-2);
}
.chart-news-panel[hidden] { display: none; }

.chart-news-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.chart-news-cerrar {
  border: 0;
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  line-height: 1;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
}
.chart-news-cerrar:hover { color: var(--text-primary); }
.chart-news-cerrar:focus-visible { outline: none; box-shadow: var(--shadow-focus-ring); }

/* Varias notas en la misma vela es el caso NORMAL, no la excepción: van
   separadas por una línea para que se lean como entradas distintas y no como un
   párrafo largo. */
.chart-news-item + .chart-news-item {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border-subtle);
}
/* ═══════════════════════════════════════════════════════════════════════════
   etrinok Design System — Shell de navegación (topbar + sub-nav + menús)
   spec docs/REDESIGN_SPEC.md §4. Todas las clases de acá son nuevas: las
   consumen los partials app/views/layouts/_topbar.html.erb, _subnav.html.erb,
   _workspace_switcher.html.erb y _account_menu.html.erb — todavía no los
   renderiza nadie (la ola 2 los engancha desde trading.html.erb).

   Cero hex acá: todo color sale de tokens.css. Los dos temas heredan solos
   porque los tokens ya son theme-aware (:root/[data-theme="dark"] vs
   [data-theme="light"]) — este archivo no necesita bloques por tema.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Topbar ──────────────────────────────────────────────────────────────── */
.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  width: 100%;
  background: var(--surface-base);
  border-bottom: 1px solid var(--border-default);
}

.topbar-inner {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  max-width: 100%;
  padding: var(--space-2) var(--space-4);
}

.topbar-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}

/* Fila de los cinco destinos — scrollea horizontal en su propio contenedor;
   el body de la página nunca scrollea de costado (spec §5). `min-width: 0`
   es lo que deja que este hijo flex se achique por debajo del ancho de su
   contenido en vez de empujar el ancho de .topbar-inner (y con él, el body). */
.topbar-nav {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.topbar-nav::-webkit-scrollbar { display: none; }

.topbar-link {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  transition: color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}
.topbar-link:hover { color: var(--text-primary); }
/* Activo = color acento-texto + subrayado de 2px en el acento crudo (spec §2:
   clay no lleva texto en tema claro, por eso el texto usa --accent-text y el
   subrayado usa --accent). */
.topbar-link.is-active {
  color: var(--accent-text);
  border-bottom-color: var(--accent);
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}

.admin-pill {
  display: inline-flex;
  align-items: center;
  padding: 0.3rem 0.75rem;
  border-radius: var(--radius-full);
  border: 1px solid var(--accent-border);
  background: var(--accent-subtle);
  color: var(--accent-text);
  font-size: var(--text-xs);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--duration-fast) var(--ease-out);
}
.admin-pill:hover { border-color: var(--accent); }
/* Activo = dentro de /admin/*. Admin no tiene acento propio (spec §6-bis
   punto 1): se distingue con la familia --warning, no con --accent. */
.admin-pill.is-active {
  background: var(--warning-subtle);
  border-color: var(--warning);
  color: var(--warning);
}

/* ── Sub-nav (fila de sub-pestañas de la sección activa) ────────────────────
   No hay contenedor cuando la sección no tiene sub-pestañas visibles —
   eso lo resuelve _subnav.html.erb no renderizando el <nav> en absoluto. */
.subnav {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  overflow-x: auto;
  scrollbar-width: none;
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
}
.subnav::-webkit-scrollbar { display: none; }

.subnav-link {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-decoration: none;
  border: 1px solid transparent;
  transition: all var(--duration-fast) var(--ease-out);
}
.subnav-link:hover {
  color: var(--text-primary);
  background: var(--surface-sunken);
}
.subnav-link.is-active {
  color: var(--accent-text);
  background: var(--accent-subtle);
  border-color: var(--accent-border);
}

/* Sub-pestaña externa (hoy: Sidekiq en Admin·Sistema) — un panel de otra
   herramienta, con otro aspecto, que abre en otra pestaña. Se lee como "esto
   te saca de la app", no como una pestaña más: borde punteado en vez de
   sólido y texto apagado en vez del tratamiento normal de .subnav-link.
   Apéndice, no reemplaza ninguna regla de arriba — nunca lleva .is-active
   (ver el método admin_nav_section en navigation_helper.rb). */
.subnav-link--external {
  color: var(--text-tertiary);
  border: 1px dashed var(--border-default);
}
.subnav-link--external:hover {
  color: var(--text-secondary);
}

/* ── Workspace switcher ─────────────────────────────────────────────────────
   Botón + menú de dos opciones (Polymarket / Binance). El acento clay es el
   mismo en las dos plataformas — lo único que cambia acá es el nombre y el
   punto de color de cada opción (spec §4: "la interfaz no se repinta
   entera"). Abrir/cerrar lo maneja workspace_switcher_controller.js; sin JS
   las dos opciones siguen siendo <a> navegables. */
.ws-switcher {
  position: relative;
}

.ws-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-default);
  background: var(--surface-sunken);
  color: var(--text-primary);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out);
}
.ws-trigger:hover { border-color: var(--border-strong); }
.ws-trigger[aria-expanded="true"] { border-color: var(--accent-border); }

.ws-trigger-caret {
  color: var(--text-tertiary);
  font-size: 0.7em;
}

.ws-menu {
  position: absolute;
  left: 0;
  top: calc(100% + var(--space-2));
  z-index: var(--z-dropdown);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 15rem;
  max-width: calc(100vw - 2rem);
  padding: var(--space-2);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

/* El controller abre/cierra agregando y quitando `hidden`. Hace falta
   declararlo acá y no alcanza con la utilidad de Tailwind: Tailwind emite
   `.hidden{display:none}` dentro de `@layer utilities`, y cualquier regla sin
   capa (como `.ws-menu` de arriba) le gana a una regla en capa, sin importar
   especificidad ni orden de carga. Sin esta regla el menú arrancaría abierto. */
.ws-menu.hidden { display: none; }

.ws-menu-label {
  padding: var(--space-1) var(--space-2);
}

.ws-option {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out);
}
.ws-option:hover { background: var(--surface-overlay); }
/* Marca la plataforma actual dentro del menú — la pastilla fija de la
   topbar ya resuelve "dónde estoy", esto es un refuerzo dentro del menú. */
.ws-option[aria-current="true"] { background: var(--surface-sunken); }

.ws-option-desc {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

/* ── Account menu ────────────────────────────────────────────────────────────
   <details>/<summary> nativo a propósito: abre/cierra y expone el estado a
   accesibilidad sin JS (el browser mapea <summary> a un botón con estado
   expandido/colapsado). Mismo look de menú que .ws-menu. */
.account-switcher {
  position: relative;
}

.account-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  list-style: none;
}
.account-trigger::-webkit-details-marker { display: none; }
.account-trigger::marker { display: none; }

.account-avatar {
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-subtle);
  color: var(--accent-text);
  font-size: var(--text-xs);
  font-weight: 700;
}

.account-menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: var(--z-dropdown);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 14rem;
  max-width: calc(100vw - 2rem);
  padding: var(--space-2);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.account-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-decoration: none;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}
.account-item:hover {
  background: var(--surface-overlay);
  color: var(--text-primary);
}

/* La fila de idioma no navega en sí misma (los EN/ES de adentro sí) — no
   necesita el hover de fila que tienen los demás ítems. */
.account-item--locale {
  justify-content: space-between;
  cursor: default;
}
.account-item--locale:hover { background: none; }

.account-locale-options {
  display: flex;
  gap: var(--space-1);
}

.account-version {
  cursor: default;
  color: var(--text-disabled);
  font-size: var(--text-xs);
}
.account-version:hover { background: none; }

/* ── Topbar icon buttons (tareas activas / campana de notificaciones) ─────
   Botón desnudo con ícono + badge de esquina, sin fondo hasta hover.
   `position: relative` es lo que ancla `.topbar-count` a la esquina del
   propio botón (no del wrapper `data-controller` que lo rodea, que también
   es `relative` pero para posicionar el panel desplegable). */
.topbar-icon-btn {
  position: relative;
  padding: 0.375rem;
  color: var(--text-tertiary);
  transition: color var(--duration-fast) var(--ease-out);
}
.topbar-icon-btn:hover { color: var(--text-primary); }
/* Estado "hay algo que mirar" (tareas activas corriendo) — spec §6-bis:
   Admin y estados de aviso usan la familia --warning, no un hue propio. */
.topbar-icon-btn--attention { color: var(--warning); }

/* Badge numérico en la esquina de un .topbar-icon-btn (contador de tareas
   activas / notificaciones no leídas). active_tasks_controller.js y
   notifications_controller.js lo muestran/ocultan con
   `classList.toggle("hidden", ...)`: como esta clase fija `display: flex`,
   necesita su propia regla `.hidden` (spec §6-bis, "trampa que hay que
   respetar" — Tailwind emite `.hidden` dentro de `@layer utilities` y
   pierde contra cualquier regla sin capa como esta, sin importar
   especificidad). Mismo mecanismo que `.ws-menu.hidden` más arriba. */
.topbar-count {
  position: absolute;
  top: -2px;
  right: -2px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  border-radius: var(--radius-full);
  font-size: 9px;
  font-weight: 700;
  color: var(--text-inverse);
}
.topbar-count.hidden { display: none; }
.topbar-count--warning { background: var(--warning); }
.topbar-count--danger  { background: var(--negative); }

/* ── Topbar dropdown panel shell (tareas activas / notificaciones) ────────
   Compone con `.modal-panel` (components.css: fondo, borde, radio de 16px)
   — acá solo se pisa la sombra: estos paneles cuelgan de un botón, no son
   un modal centrado, así que llevan --shadow-lg en vez del --shadow-xl de
   `.modal-panel`. Gana por cascada porque este archivo carga después de
   components.css (ver application.css: tokens/base/components explícitos,
   después require_tree en orden alfabético). Ninguna de las dos reglas de
   acá fija `display`, así que el toggle de apertura/cierre con `.hidden`
   de Tailwind no necesita el escape de arriba. */
.topbar-panel {
  box-shadow: var(--shadow-lg);
}

.topbar-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-default);
}

/* Ícono ⚙️ dentro del header del panel de tareas activas (el de
   notificaciones no lleva ícono propio, solo título + cerrar). */
.topbar-panel-icon {
  color: var(--warning);
}

/* ── Dropdown panels de la topbar (tareas activas / notificaciones) ────────
   Los dos widgets vivían sueltos en el <header> viejo con utilidades
   Tailwind de gris/amarillo/índigo (text-gray-500, bg-yellow-400,
   text-indigo-400...) armadas por template string en
   active_tasks_controller.js / notifications_controller.js. Ahora que los
   dos paneles son parte del shell (dentro de .topbar-actions, ver
   _topbar.html.erb), sus filas usan estas clases — mismo mecanismo que
   .ws-option/.account-item de arriba, así que ninguna fila queda sin tema. */
.panel-row {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  transition: background-color var(--duration-fast) var(--ease-out);
}
.panel-row:last-child { border-bottom: none; }
.panel-row:hover { background: var(--surface-overlay); }
/* Notificación ya leída: se atenúa en vez de cambiar de color — mismo
   tratamiento que .is-stale para "dato viejo" (spec §5). */
.panel-row.is-read { opacity: 0.6; }

.panel-link {
  color: var(--accent-text);
  text-decoration: none;
}
.panel-link:hover { color: var(--accent-hover); }

/* Separador "Anteriores" entre notificaciones no leídas y leídas. */
.panel-subheader {
  padding: var(--space-1) var(--space-4);
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border-subtle);
}

/* ── Responsive: nunca scroll horizontal del body (spec §5) ─────────────── */
@media (max-width: 640px) {
  .topbar-inner { padding: var(--space-2) var(--space-3); gap: var(--space-2); }
  .subnav { padding: var(--space-2) var(--space-3); }
}
/* ═══════════════════════════════════════════════════════════════════════════
   Noticias de mercado — la sección PÚBLICA (/noticias)
   ═══════════════════════════════════════════════════════════════════════════
   Todo sale de tokens. Ni un hex, ni una utilidad de color de Tailwind: es
   justo lo que las otras tres pantallas públicas (shared_trade, shared_chart,
   landing) no hacen —entre las tres suman UNA sola referencia a un token— y es
   la deuda que esta sección viene a no repetir.

   Es pública, así que se separa del resto en dos cosas y sólo en dos:

     · más aire. Adentro de la app la densidad es una virtud (hay que ver diez
       cosas a la vez); acá se lee de corrido y en un teléfono.
     · una jerarquía tipográfica más alta arriba. La pantalla tiene que
       explicarse sola a alguien que llegó de un link y no sabe qué es esto.

   El vocabulario de componentes es el mismo: `.news-impact`, `.news-event` y
   `.news-why` son las de components.css, las mismas que usa el panel de
   noticias de adentro del gráfico. Dos pantallas que hablan de lo mismo tienen
   que decirlo igual.
   ═══════════════════════════════════════════════════════════════════════════ */

.noticias-shell {
  max-width: 74rem;
  margin: 0 auto;
  padding: var(--space-10) var(--space-5) var(--space-16);
}

/* ── Cabecera ─────────────────────────────────────────────────────────────── */

.noticias-hero {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-6);
  flex-wrap: wrap;
  margin-bottom: var(--space-8);
}

.noticias-hero__texto { max-width: 44rem; min-width: 0; }

.noticias-hero__titulo {
  margin-top: var(--space-2);
  color: var(--text-primary);
  text-wrap: balance;
}

.noticias-hero__bajada {
  margin-top: var(--space-3);
  color: var(--text-secondary);
  font-size: var(--text-md);
  line-height: var(--leading-normal);
}

/* El conmutador de idioma. Mismo gesto que el resto de los grupos de pastillas
   del sistema: un riel hundido y la opción activa levantada. */
.noticias-idioma,
.noticias-grupo__opciones {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface-sunken);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  flex-shrink: 0;
}

.noticias-idioma__opcion,
.noticias-chip {
  padding: 0.3rem 0.8rem;
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-tertiary);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--duration-fast) var(--ease-out),
              background var(--duration-fast) var(--ease-out);
}

.noticias-idioma__opcion:hover,
.noticias-chip:hover { color: var(--text-primary); }

.noticias-idioma__opcion.is-active,
.noticias-chip.is-active {
  background: var(--surface-raised);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}

.noticias-idioma__opcion:focus-visible,
.noticias-chip:focus-visible,
.noticias-mas__boton:focus-visible,
.noticias-par:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
}

/* ── Filtros ──────────────────────────────────────────────────────────────── */

.noticias-filtros {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  margin-bottom: var(--space-6);
}

/* Los tres grupos cortos —cuándo, impacto, orden— en una fila que envuelve.
   Cada uno es un riel hundido con su opción levantada, el mismo gesto que el
   conmutador de idioma: cuatro controles con cuatro formas distintas se leen
   como cuatro cosas sin relación. */
.noticias-filtros__fila {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
}

.noticias-grupo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.noticias-grupo__etiqueta {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  white-space: nowrap;
}

.noticias-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

/* El punto del chip de impacto crece con el nivel, igual que la marca del
   radar: el control se parece a lo que filtra. No hay un color de "impacto" en
   los tokens —la pastilla del feed colorea la DIRECCIÓN, que es otra columna—
   así que el punto va en currentColor y lo que cambia es el tamaño. Pintarlo
   inventaría una escala de color que nadie declaró. */
.noticias-chip__punto {
  display: inline-block;
  border-radius: var(--radius-full);
  background: currentColor;
  flex-shrink: 0;
  opacity: 0.75;
}

.noticias-chip--bajo  .noticias-chip__punto { width: 4px; height: 4px; }
.noticias-chip--medio .noticias-chip__punto { width: 6px; height: 6px; }
.noticias-chip--alto  .noticias-chip__punto { width: 9px; height: 9px; opacity: 1; }

.noticias-chip.is-active .noticias-chip__punto { opacity: 1; }

/* La flecha del chip de dirección: la MISMA que lleva la pastilla de cada
   tarjeta, y con el mismo color, para que el control y lo que filtra se lean
   como la misma cosa. Acá el color SÍ significa —dirección es la columna que el
   sistema pinta— al revés que en el chip de impacto, donde el punto va en
   currentColor porque no existe una escala de color para la magnitud. */
.noticias-chip__flecha {
  font-weight: 700;
  line-height: 1;
}

.noticias-chip--alcista .noticias-chip__flecha { color: var(--positive); }
.noticias-chip--bajista .noticias-chip__flecha { color: var(--negative); }
.noticias-chip--neutra  .noticias-chip__flecha { color: var(--text-tertiary); }

/* El grupo de dirección vive en la cabecera del feed, que ya es un flex con el
   título a la izquierda y la cuenta a la derecha. Va en su propio renglón
   cuando no entra, en vez de apretar el "mostrando N de M". */
.noticias-feed__lado { flex: 0 0 auto; }

/* En un teléfono la lista de pares scrollea EN SU CAJA. Sin esto arrastra la
   página entera de costado, que es el desborde que la capa responsive del
   sistema viene arreglando pantalla por pantalla. */
.noticias-filtros__pares {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

/* SIN scroll horizontal, a propósito.

   Lo tenía, y era el problema: una tira que scrollea queda cortada por la mitad
   y —por el `padding-bottom` que le hacía lugar a la barra de scroll— desalineada
   con el botón que tiene al lado. Además esconde chips detrás de un gesto que
   nadie hace en escritorio.

   La respuesta no fue mejorar el scroll: fue que no haga falta. Entran los
   cuatro pares más movidos (`PARES_VISIBLES`) y todo lo demás se escribe en el
   buscador. Si en una pantalla angosta no entraran ni esos cuatro, la fila
   envuelve hacia abajo, que se lee sin gestos. */
.noticias-pares__tira {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  min-width: 0;
}

/* Los pares que no entran en la barra. <details> nativo: se despliega sin
   JavaScript, que es la regla de esta pantalla. */
.noticias-mas { flex-shrink: 0; position: relative; }

/* Sólido y con lupa, no punteado: es una ACCIÓN y está parado al lado de una
   fila de pastillas que son VALORES. Punteado y sin ícono se leía como un chip
   más de la lista, que es exactamente lo que pasó. */
.noticias-mas__boton {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.8rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
  cursor: pointer;
  list-style: none;
  transition: border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              background var(--duration-fast) var(--ease-out);
}

.noticias-mas__lupa {
  width: 0.9em;
  height: 0.9em;
  flex-shrink: 0;
  color: var(--text-tertiary);
}

.noticias-mas__boton:hover .noticias-mas__lupa,
.noticias-mas[open] .noticias-mas__lupa { color: var(--accent-text); }

/* Safari dibuja su propio triangulito si no se le saca el marcador por acá
   además de por `list-style`. */
.noticias-mas__boton::-webkit-details-marker { display: none; }

.noticias-mas__boton:hover {
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.noticias-mas[open] .noticias-mas__boton {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent-text);
}

/* Flotante y no en el flujo: abrirlo no puede empujar el pulso media pantalla
   hacia abajo. */
.noticias-mas__lista {
  position: absolute;
  z-index: 40;
  top: calc(100% + var(--space-2));
  /* De la izquierda, que es donde está parado el botón. Anclado a la derecha
     el panel se abría hacia afuera de la pantalla. */
  left: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: max-content;
  max-width: min(38rem, 90vw);
  padding: var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

/* El campo de búsqueda. Nace `hidden` en el HTML y lo enciende su controlador:
   es lo único de esta pantalla que no puede funcionar sin JavaScript, y un
   campo donde escribir sin que pase nada es peor que no tenerlo. */
.noticias-mas__campo {
  width: 100%;
  min-width: 14rem;
  padding: 0.4rem 0.7rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-family: var(--font-mono);
  color: var(--text-primary);
}

.noticias-mas__campo::placeholder { color: var(--text-tertiary); font-family: var(--font-sans); }

.noticias-mas__campo:focus-visible {
  outline: none;
  border-color: var(--accent-border);
  box-shadow: var(--shadow-focus-ring);
}

.noticias-mas__cuenta {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

/* El tope de alto vive acá y no en el contenedor: si estuviera arriba, el campo
   se iría scrolleando junto con la lista y buscar en sesenta pares obligaría a
   volver a subir para corregir una letra. */
.noticias-mas__pares {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  max-height: 15rem;
  overflow-y: auto;
  scrollbar-width: thin;
}

.noticias-mas__vacio {
  margin: 0;
  max-width: 20rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.noticias-mas__vacio[hidden],
.noticias-mas__campo[hidden] { display: none; }

.noticias-par {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  transition: border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.noticias-par:hover { border-color: var(--border-strong); color: var(--text-primary); }

.noticias-par.is-active {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent-text);
}

.noticias-par__nombre { font-weight: 600; }

.noticias-par__cuenta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

.noticias-par.is-active .noticias-par__cuenta { color: var(--accent-text); }

/* ── El feed ──────────────────────────────────────────────────────────────── */

.noticias-feed__cabecera {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}

.noticias-feed__cuenta {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  font-family: var(--font-mono);
}

.noticias-lista {
  display: flex;
  flex-direction: column;
}

/* Sin tarjetas sueltas: una lista con separadores. Cien titulares en cien cajas
   con sombra es ruido; lo que tiene que destacar es el titular. */
.noticia {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-5) 0;
  border-bottom: 1px solid var(--border-subtle);
  transition: background var(--duration-slow) var(--ease-out);
}

.noticia:first-child { padding-top: var(--space-2); }

/* La barrita de color de la izquierda: la dirección, de nuevo, para poder
   barrer la lista con la vista sin leer cada pastilla. */
.noticia__marca {
  flex-shrink: 0;
  width: 3px;
  border-radius: var(--radius-full);
  background: var(--text-disabled);
}

.noticia__marca--up   { background: var(--positive); }
.noticia__marca--down { background: var(--negative); }
.noticia__marca--flat { background: var(--border-strong); }

.noticia__cuerpo { min-width: 0; flex: 1; }

.noticia__cabecera {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}

.noticia__par {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-tertiary);
  text-decoration: none;
}

.noticia__par:hover { color: var(--accent-text); }

.noticia__titulo {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 500;
  line-height: var(--leading-snug);
  text-wrap: pretty;
}

.noticia__enlace {
  color: var(--text-primary);
  text-decoration: none;
  background-image: linear-gradient(var(--border-default), var(--border-default));
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: 0 100%;
  transition: background-size var(--duration-base) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

a.noticia__enlace:hover {
  color: var(--accent-text);
  background-image: linear-gradient(var(--accent), var(--accent));
}

a.noticia__enlace:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
  border-radius: var(--radius-sm);
}

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

.noticia__origen {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

/* Sin fecha no es un hueco: es un dato que se dice. */
.noticia__sin_fecha {
  font-style: italic;
  color: var(--text-disabled);
}

.noticia__porque {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  max-width: 58rem;
}

.noticia__confianza {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

.noticia__confianza_etiqueta {
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.noticia__confianza_riel {
  display: block;
  width: 4rem;
  height: 3px;
  border-radius: var(--radius-full);
  background: var(--neutral-subtle);
  overflow: hidden;
}

.noticia__confianza_relleno {
  display: block;
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--text-tertiary);
}

.noticia__confianza_valor { font-family: var(--font-mono); }

/* ── Vacío y pie ──────────────────────────────────────────────────────────── */

.noticias-vacio {
  text-align: center;
  padding: var(--space-12) var(--space-4);
  background: var(--surface-sunken);
  border-radius: var(--radius-lg);
}

.noticias-vacio__titulo {
  margin: 0;
  font-size: var(--text-md);
  color: var(--text-primary);
}

.noticias-vacio__pista {
  margin: var(--space-2) 0 var(--space-4);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.noticias-pie {
  margin-top: var(--space-10);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border-subtle);
}

.noticias-pie__linea {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  line-height: var(--leading-normal);
  max-width: 60rem;
}

.noticias-pie__aviso { color: var(--text-disabled); }

/* ── Teléfono ─────────────────────────────────────────────────────────────
   Mismo corte que design_system/responsive.css. No es un extra: esta sección
   es pública y se abre desde el teléfono más que desde cualquier otro lado. */
@media (max-width: 768px) {
  .noticias-shell { padding: var(--space-6) var(--space-4) var(--space-12); }

  .noticias-hero { gap: var(--space-4); }

  /* El conmutador de idioma pasa arriba del título en la lectura, pero se
     dibuja al final: `order` lo sube sin cambiar el orden del DOM. */
  .noticias-idioma { order: -1; }

  /* Lo del PULSO en teléfono (una columna, etiquetas de anillo apagadas, globo
     más angosto) se mudó con el componente a `pulso_de_noticias.css`: vale
     igual para la ficha de un par, que también se abre desde el teléfono. */

  .noticia { gap: var(--space-3); padding: var(--space-4) 0; }

  .noticia__titulo { font-size: var(--text-base); }
}

@media (prefers-reduced-motion: reduce) {
  .noticia,
  .noticia__enlace,
  .noticias-par,
  .noticias-idioma__opcion,
  .noticias-mas__boton,
  .noticias-chip { transition: none; }
}
/* ═══════════════════════════════════════════════════════════════════════════
   El pulso de noticias — el COMPONENTE, en las dos pantallas que lo dibujan
   ═══════════════════════════════════════════════════════════════════════════
   El radar del sentimiento (y su línea de tiempo, cuando el contexto la pide)
   se monta en dos lugares: la sección pública `/noticias` y la ficha de un par
   de la sección de mercados. Antes vivía adentro de `noticias_publicas.css`,
   cuando había un solo cliente; con dos, un archivo llamado "públicas" que
   estiliza una pantalla autenticada es la clase de mentira que hace que la
   próxima persona duplique el bloque en vez de reusarlo.

   Todo sale de tokens. Ni un hex, ni una utilidad de color de Tailwind: el
   dibujo lo pinta el controlador de Stimulus con `style.fill = "var(--…)"`, así
   que sigue el tema claro/oscuro solo, y lo de acá tiene que hacer lo mismo.

   Lo que cambia entre las dos pantallas entra por el modificador de contexto
   (`--publico` / `--instrumento`) y es DENSIDAD, nada más: la geometría, los
   canales visuales y el vocabulario son idénticos porque son el mismo dibujo.

   `require_tree .` de application.css lo levanta para los dos layouts sin que
   ninguno lo nombre.
   ═══════════════════════════════════════════════════════════════════════════ */

.noticias-pulso {
  position: relative;
  background: var(--surface-base);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  margin-bottom: var(--space-8);
}

.noticias-pulso__cabecera { margin-bottom: var(--space-5); }

.noticias-pulso__titulo_fila {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* ── El ícono de "cómo se lee" ────────────────────────────────────────────────
   El texto de adentro es el MISMO que la sección pública imprime como bajada
   (`pulso.ayuda.texto`). Acá va plegado porque en la ficha del par la tarjeta
   es secundaria: el gráfico de velas de arriba es el objeto principal de la
   pantalla y tres renglones de explicación permanente le comen el lugar. */
.noticias-ayuda {
  position: relative;
  display: inline-flex;
}

.noticias-ayuda__boton {
  width: 1.15rem;
  height: 1.15rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: none;
  color: var(--text-tertiary);
  font-size: var(--text-xs);
  line-height: 1;
  cursor: pointer;
}

.noticias-ayuda__boton:hover {
  color: var(--text-primary);
  border-color: var(--border-strong);
}

.noticias-ayuda__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: var(--z-dropdown);
  width: 22rem;
  max-width: 78vw;
  padding: var(--space-3);
  background: var(--surface-overlay);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
}

.noticias-ayuda__panel[hidden] { display: none; }

.noticias-ayuda__titulo {
  display: block;
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: var(--space-2);
}

.noticias-ayuda__texto {
  margin: 0;
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

.noticias-pulso__bajada {
  margin-top: var(--space-2);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  max-width: 52rem;
}

/* ── El radar ocupa todo, hasta que hay algo al lado ──────────────────────────
   Sin ninguna nota marcada la columna de la derecha no tiene nada que mostrar,
   así que el dibujo se queda con el ancho entero — que es la pieza que uno vino
   a mirar. Al marcar la primera, el radar se corre y se achica y el panel entra.

   FLEX y no grid, a propósito: `grid-template-columns` no interpola entre una
   columna y dos —son cantidades distintas de pistas— así que el cambio sería un
   salto. Un `flex-basis` de 0 a 22rem sí transiciona, y en todos los
   navegadores. */
.noticias-pulso__grid {
  display: flex;
  gap: var(--space-6);
  align-items: flex-start;
}

.noticias-radar {
  flex: 1 1 auto;
  min-width: 0;
}

/* El dibujo va sobre un fondo MÁS OSCURO que la tarjeta.
   No es decoración: los puntos son de colores translúcidos sobre un semicírculo
   sin relleno, y contra la superficie de la tarjeta los más viejos —que ahora
   son los más transparentes— se despintaban del todo. Un fondo hundido les
   devuelve el contraste sin tocarles el color.

   `--surface-sunken` y no un hex: es el mismo token que usa la bandeja de
   marcadas, así que las dos cajas hundidas de la tarjeta se ven como una
   familia y siguen el tema claro/oscuro solas. */
.noticias-radar__lienzo {
  min-height: 4rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: var(--space-2) var(--space-2) 0;
}

/* Los tres rótulos del eje angular. Van en HTML y no adentro del SVG para que
   se puedan traducir sin tocar el dibujo y para que existan aunque el
   JavaScript no corra. */
.noticias-radar__eje_base {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.noticias-radar__eje_base span:nth-child(2) { color: var(--text-disabled); }

.noticias-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Cerrado: sin ancho y sin opacidad, pero PRESENTE — para que la transición
     tenga de dónde salir. `overflow: hidden` es lo que impide que el contenido
     se derrame mientras el ancho todavía es cero. */
  flex: 0 0 0;
  min-width: 0;
  /* Y sin ALTO tampoco. Con el ancho en cero pero el contenido intacto, la caja
     seguía midiendo sus 26rem y la fila entera se estiraba a ese alto: 33 px de
     aire muerto debajo del radar —medido— antes de la línea de tiempo. El alto
     no se anima, se apaga y listo: en ese momento la opacidad es cero, así que
     no hay nada que ver saltando. */
  height: 0;
  overflow: hidden;
  opacity: 0;
  transition: flex-basis var(--duration-slow) var(--ease-out),
              opacity var(--duration-slow) var(--ease-out);
}

.noticias-pulso.tiene-panel .noticias-panel {
  /* 18rem y no más: el radar es la pieza que uno vino a mirar, y con el panel
     en 22rem quedaba MÁS ANCHO que el dibujo. */
  flex-basis: 18rem;
  height: auto;
  opacity: 1;
}

/* El gesto entero se apaga si se pidió menos movimiento: el panel aparece y
   listo. */
@media (prefers-reduced-motion: reduce) {
  .noticias-panel { transition: none; }
}

/* En FILA y a lo ancho, arriba del dibujo. Apiladas en la columna de la
   derecha, "78" arriba de "22" no se leía como una proporción — y esa columna
   hacía falta entera para las noticias marcadas, que sí se leen una por una.

   `auto-fit` con mínimo: en un teléfono bajan a dos por renglón solas. */
.noticias-cifras {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-3);
  margin: 0 0 var(--space-5);
}

.noticias-cifra {
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}

.noticias-cifra__etiqueta {
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.noticias-cifra__valor {
  margin: 0;
  margin-top: 0.15rem;
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text-primary);
  line-height: var(--leading-tight);
}

.noticias-cifra__de {
  font-size: var(--text-sm);
  color: var(--text-tertiary);
  font-weight: 400;
}

/* La leyenda. Cuatro renglones, uno por canal visual: sin ella el dibujo es
   bonito y mudo, y las dos cosas que hay que entender —que el tamaño es el
   impacto y el ángulo es el tono— no se adivinan. */
.noticias-leyenda {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  border-top: 1px solid var(--border-subtle);
  padding-top: var(--space-4);
}

.noticias-leyenda__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  line-height: var(--leading-snug);
}

.noticias-leyenda__muestra {
  flex-shrink: 0;
  width: 2rem;
  height: 0.85rem;
  position: relative;
}

/* Tres puntos crecientes: el tamaño es el impacto. */
.noticias-leyenda__muestra--tamano::before,
.noticias-leyenda__muestra--tamano::after,
.noticias-leyenda__muestra--hueco::before,
.noticias-leyenda__muestra--angulo::before,
.noticias-leyenda__muestra--angulo::after,
.noticias-leyenda__muestra--anillo::before,
.noticias-leyenda__muestra--anillo::after {
  content: "";
  position: absolute;
  top: 50%;
  border-radius: var(--radius-full);
  transform: translateY(-50%);
}

/* La muestra de la edad: dos puntos del mismo color, uno translucido y otro
   opaco. Es la unica de la leyenda que muestra DOS cosas, porque lo que explica
   es una escala y no una forma. */
.noticias-leyenda__muestra--edad {
  background: none;
  border: none;
  position: relative;
}

.noticias-leyenda__muestra--edad::before,
.noticias-leyenda__muestra--edad::after {
  content: "";
  position: absolute;
  top: 50%;
  width: 6px;
  height: 6px;
  margin-top: -3px;
  border-radius: var(--radius-full);
  background: var(--text-secondary);
}

.noticias-leyenda__muestra--edad::before { left: 0;   opacity: 0.3; }
.noticias-leyenda__muestra--edad::after  { left: 8px; opacity: 1; }

.noticias-leyenda__muestra--tamano::before {
  left: 0; width: 4px; height: 4px; background: var(--text-tertiary);
}
.noticias-leyenda__muestra--tamano::after {
  left: 0.7rem; width: 10px; height: 10px; background: var(--text-secondary);
}

/* El anillo hueco: leído y sin movimiento esperado. */
.noticias-leyenda__muestra--hueco::before {
  left: 0.35rem; width: 8px; height: 8px;
  border: 1.5px solid var(--text-tertiary);
}

/* Los dos extremos del eje angular. */
.noticias-leyenda__muestra--angulo::before {
  left: 0; width: 8px; height: 8px; background: var(--negative);
}
.noticias-leyenda__muestra--angulo::after {
  right: 0; width: 8px; height: 8px; background: var(--positive);
}

/* Dos arcos concéntricos: los anillos son el plazo. */
.noticias-leyenda__muestra--anillo::before {
  left: 0.55rem; width: 7px; height: 7px;
  border: 1px solid var(--border-strong);
}
.noticias-leyenda__muestra--anillo::after {
  left: 0.1rem; width: 16px; height: 16px;
  border: 1px solid var(--border-default);
}

/* ── Línea de tiempo ──────────────────────────────────────────────────────── */

.noticias-tiempo {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border-subtle);
}

.noticias-tiempo__cabecera {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}

.noticias-tiempo__titulo {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.noticias-tiempo__rango,
.noticias-tiempo__nota {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

.noticias-tiempo__nota { margin: var(--space-2) 0 0; }

.noticias-tiempo__lienzo { min-height: 3rem; }

/* Las marcas de los dos dibujos. El cursor y el realce dicen que se pueden
   tocar; sin eso, nadie descubre que llevan a la nota de abajo. */
.noticias-marca { cursor: pointer; }
.noticias-marca:hover circle,
.noticias-marca:hover line { opacity: 1; }

/* ── El barrido ───────────────────────────────────────────────────────────────
   Un arco que se abre desde el centro hasta el borde, y a su paso enciende las
   notas de impacto alto en las que el modelo además tenía confianza.

   POR QUÉ RECORRE EL RADIO Y NO GIRA. Un radar de verdad barre en ángulo, pero
   en este dibujo el ángulo es el SENTIMIENTO y el radio es el PLAZO. Un barrido
   angular recorrería de bajista a alcista, que no es una secuencia de nada; uno
   radial recorre "ya · en horas · en días · en semanas", que sí lo es. El
   efecto queda diciendo algo cierto en vez de sólo moverse.

   POR QUÉ EN CSS Y NO EN JAVASCRIPT. Un `requestAnimationFrame` para esto
   tendría que seguir corriendo mientras la pestaña está abierta y quemando
   batería en un teléfono. Acá el navegador lo compone solo, lo pausa cuando la
   pestaña no se ve, y `prefers-reduced-motion` lo apaga con una regla — no hay
   nada que escuchar ni nada que limpiar al desconectar.

   EL SINCRONISMO. Cada marca conoce su fracción de radio y recibe su
   `--retraso` = fracción × lo que tarda el arco en llegar hasta ahí. Por eso
   se encienden cuando el arco las cruza y no todas juntas. */

.noticias-radar__barrido {
  /* Las unidades son las del viewBox y el centro es el del semicírculo
     (RADAR.cx / RADAR.cy del controlador). Si allá se mueve el centro, acá
     también: hay un spec que compara los dos números. */
  transform-origin: 240px 244px;
  animation: noticias-barrido var(--barrido-periodo, 7s) linear infinite;
  will-change: transform, opacity;
}

@keyframes noticias-barrido {
  0%   { transform: scale(0.03); opacity: 0; }
  4%   { opacity: 0.55; }
  38%  { opacity: 0.30; }
  50%  { transform: scale(1); opacity: 0; }
  /* De 50% a 100% no pasa nada: es la pausa entre pasada y pasada. Sin ella el
     dibujo late sin parar y una pantalla que uno mira para leer no puede estar
     siempre en movimiento. */
  100% { transform: scale(1); opacity: 0; }
}

/* El halo que deja el arco al cruzar una marca fuerte. Es un círculo aparte y
   no un filtro: un `filter` sobre ciento veinte nodos SVG es lo que convierte
   un efecto en un ventilador. */
.noticias-marca__halo {
  animation: noticias-halo var(--barrido-periodo, 7s) ease-out infinite;
  animation-delay: var(--retraso, 0s);
  pointer-events: none;
}

@keyframes noticias-halo {
  0%   { transform: scale(0.5); opacity: 0; }
  2%   { opacity: 0.5; }
  12%  { transform: scale(2.8); opacity: 0; }
  100% { transform: scale(2.8); opacity: 0; }
}

/* Y la marca misma sube a opacidad plena un instante. Vuelve a `--opacidad`,
   que es la confianza declarada: la animación no puede dejarla en otro valor
   que el que el dibujo afirma el resto del tiempo. */
.noticias-marca--destaca .noticias-marca__punto {
  animation: noticias-destello var(--barrido-periodo, 7s) ease-out infinite;
  animation-delay: var(--retraso, 0s);
}

@keyframes noticias-destello {
  0%   { opacity: var(--opacidad, 1); }
  2%   { opacity: 1; }
  12%  { opacity: var(--opacidad, 1); }
  100% { opacity: var(--opacidad, 1); }
}

/* Sin movimiento: el arco no se dibuja y nada parpadea. La marca fuerte igual
   se distingue —es la más grande y la más opaca del dibujo— así que apagar
   esto no le saca información a nadie. */
@media (prefers-reduced-motion: reduce) {
  .noticias-radar__barrido { display: none; }

  .noticias-marca__halo {
    display: none;
    animation: none;
  }

  .noticias-marca--destaca .noticias-marca__punto { animation: none; }
}

/* ── Las que marcaste ──────────────────────────────────────────────────────────
   Tocar un punto antes bajaba la pantalla hasta la tarjeta de esa nota; ahora
   se juntan acá, arriba del dibujo, y pasar el mouse por una enciende su punto
   en los dos gráficos. Es lo que convierte una lista en una lectura del radar.

   Va ARRIBA del grid y no al costado: en el ancho de escritorio el radar y las
   cifras ya ocupan las dos columnas, y meterla como tercera dejaba el radar en
   un tercio de pantalla — que es la pieza que uno vino a mirar. */
/* ALTO FIJO en el CONTENEDOR, no en la lista.
   Con el alto fijado sólo en la lista, el aviso de "llegaste al tope" aparecía
   y empujaba la línea de tiempo 23 px hacia abajo — medido. Fijándolo acá y
   dejando que la lista flexione, cualquier cosa que aparezca adentro le saca
   lugar a la lista en vez de agrandar la caja, y el resto de la tarjeta no se
   entera de nada. */
.noticias-marcadas {
  display: flex;
  flex-direction: column;
  height: 26rem;
  padding: var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
}

/* El cartel de cuando no marcaste ninguna. No es decorativo: sin él la columna
   arranca en blanco y nadie descubre que los puntos del dibujo se pueden
   tocar. */
.noticias-marcadas__vacio {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  line-height: var(--leading-relaxed);
}

.noticias-marcadas__cabecera {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.noticias-marcadas__titulo {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.noticias-marcadas__limpiar {
  background: none;
  border: none;
  padding: 0;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  cursor: pointer;
  text-decoration: underline;
}

.noticias-marcadas__limpiar:hover { color: var(--text-primary); }

/* En columna y con tope de alto: marcar seis notas no puede empujar el radar
   fuera de la pantalla. */
/* ALTO FIJO, no `max-height`.
   Con un alto que crecía, cada nota que marcabas empujaba la línea de tiempo de
   abajo un poco más lejos: la pantalla se movía sola mientras uno estaba
   leyendo. Con el alto fijo el resto de la tarjeta no se entera de cuántas hay,
   y la lista scrollea adentro suyo.

   El número está atado al tope de diez: es lo que entra sin que haya que
   scrollear para ver la que acabás de marcar. Si allá cambia `MAX_MARCADAS`,
   esto también. */
.noticias-marcadas__lista {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* Toma lo que quede del contenedor, que es de alto fijo. `min-height: 0` es
     lo que le permite a un hijo flex bajar de su contenido y scrollear en vez
     de estirarse. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
}

/* El aviso de que no entran más. No se descarta ninguna de las que marcaste
   —eso sería perder trabajo del usuario sin avisar— así que hay que decir por
   qué el próximo click no hace nada. */
.noticias-marcadas__tope {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs);
  color: var(--warning);
}

.noticias-marcada {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-base);
  border-radius: var(--radius-md);
  /* La misma barrita de color que la tarjeta del feed y que el globo: es la
     dirección, y se lee igual en los tres lugares. */
  border-left: 3px solid var(--border-strong);
}

.noticias-marcada--up   { border-left-color: var(--positive); }
.noticias-marcada--down { border-left-color: var(--negative); }

.noticias-marcada__titulo {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  line-height: var(--leading-snug);
}

.noticias-marcada__porque { margin: 0; }

/* La categoría, cuándo apareció y en qué plazo se juega. Sin esto había que
   volver al dibujo y buscar el punto para saber si una nota era de hace veinte
   minutos o de anteayer — justo lo que la bandeja viene a evitar. */
.noticias-marcada__pie {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.noticias-marcada__cuando {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

.noticias-marcada__acciones {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.noticias-marcada__leer,
.noticias-marcada__quitar {
  background: none;
  border: none;
  padding: 0;
  font-size: var(--text-xs);
  cursor: pointer;
  text-decoration: underline;
}

.noticias-marcada__leer   { color: var(--accent-text); }
.noticias-marcada__quitar { color: var(--text-tertiary); }
.noticias-marcada__quitar:hover { color: var(--negative); }

/* ── Las dos formas de estar señalada ──────────────────────────────────────────
   Son DOS estados y tienen que distinguirse entre sí, no sólo del resto:

     · `is-marcada`    — está en la bandeja. Permanente, discreto: un aro fino.
                         Es un mapa de "cuáles vengo mirando" y hay hasta diez a
                         la vez, así que gritar sería ruido.
     · `is-alumbrada`  — el mouse está encima de ESA fila de la bandeja. Tiene
                         que encontrarse entre ciento veinte puntos de un
                         vistazo, así que cambia de COLOR además de engordar.

   El aro es un círculo propio y no un `stroke` sobre el punto: el punto ya usa
   su relleno para la dirección y su opacidad para la confianza, y pisarlos
   borraría dos datos para mostrar uno. */
.noticias-marca__aro {
  fill: none;
  stroke: none;
  /* Redondo en la línea de tiempo, donde el aro es un trazo vertical y no un
     círculo: sin esto termina en dos puntas cuadradas que se ven como otra
     marca. */
  stroke-linecap: round;
  pointer-events: none;
  /* SIN transición, y no por gusto: con `transition: stroke-width` el valor
     quedaba clavado en el default de 1px —medido: el color y la opacidad se
     aplicaban y el grosor no— así que el aro señalado se veía igual que el
     marcado. Y además, para "encontrá este punto entre ciento veinte",
     instantáneo es lo que se quiere: una animación de 120 ms es justo el rato
     en que uno está buscando. */
}

.noticias-marca.is-marcada .noticias-marca__aro {
  /* CELESTE y no el color de acento.
     El acento es un terracota que, a seis píxeles y sobre un punto chico, se
     confundía con el salmón de las bajistas. `--info` no se parece a ninguno de
     los dos colores que el dibujo YA usa para decir algo (olivo alcista, salmón
     bajista), así que un aro nunca se lee como una dirección.

     Y PUNTEADO: es un segundo canal, además del color. Quien no distingue los
     tonos igual ve que ese punto está marcado, y el trazo cortado se lee como
     "seleccionado" y no como parte del dibujo. */
  stroke: var(--info);
  stroke-dasharray: 3 2.5;
  /* CON unidad: en CSS un `stroke-width` pelado es inválido y el navegador cae
     al default de 1px — el color se aplicaba y el grosor no. Adentro del SVG,
     como atributo, sí va sin unidad; acá no. */
  stroke-width: 1.6px;
  opacity: 0.9;
}

.noticias-marca.is-marcada circle,
.noticias-marca.is-marcada line { opacity: 1; }

.noticias-marca.is-alumbrada .noticias-marca__aro {
  /* El mismo celeste, SÓLIDO y al doble de grueso. Mismo tono porque son el
     mismo concepto en dos intensidades —marcada y señalada—; dos tonos
     distintos se leerían como dos cosas sin relación. Lo que las separa es el
     trazo: cortado contra entero se ve de lejos. */
  stroke: var(--info);
  stroke-dasharray: none;
  stroke-width: 3.5px;
  opacity: 1;
}

/* Y el punto de adentro también, para que el conjunto se lea como UNA marca
   encendida y no como un aro flotando. */
.noticias-marca.is-alumbrada circle,
.noticias-marca.is-alumbrada line { opacity: 1; }

/* La fila de la bandeja también se marca mientras la señalás: el puente se
   entiende mejor si los dos extremos se prenden juntos. */
.noticias-marcada.is-señalando {
  /* El mismo celeste que el aro del dibujo: los dos extremos del puente se
     prenden del mismo color, o no se lee que uno señala al otro. */
  outline: 2px solid var(--info);
  outline-offset: 1px;
}


/* ── El globo de la marca ─────────────────────────────────────────────────── */

.noticias-globo {
  position: absolute;
  z-index: var(--z-dropdown);
  max-width: 18rem;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-overlay);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.noticias-globo[hidden] { display: none; }

/* ── El globo, con el aspecto de una tarjeta del feed ──────────────────────────
   Es la MISMA noticia que la tarjeta de abajo, así que se lee igual: la franja
   de color del lado, la pastilla de impacto con su flecha, los chips de par, el
   titular y el pie. Antes era el titular y una línea de datos separados por
   puntos — dos aspectos para la misma cosa hacen dudar de si son la misma cosa.

   La franja va como borde del globo y no como un nodo aparte: un elemento menos
   por cada vez que se dibuja, y son ciento veinte marcas las que lo pueden
   pedir. */
.noticias-globo--up   { border-left: 3px solid var(--positive); }
.noticias-globo--down { border-left: 3px solid var(--negative); }
.noticias-globo--flat { border-left: 3px solid var(--border-strong); }

.noticias-globo__cabecera {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
}

/* El pie hereda el estilo de `noticias-globo__meta` que ya estaba; lo único que
   cambia es que ahora puede llevar adentro la pastilla de evento, así que
   necesita alinearla en vez de dejarla como texto suelto. */
.noticias-globo__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.noticias-globo__titulo {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-primary);
  line-height: var(--leading-snug);
}

.noticias-globo__meta {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

/* Fijado: el globo deja de seguir al mouse y pasa a ser una tarjeta con la que
   se puede interactuar. `pointer-events` de vuelta en auto es lo que hace
   clickeable el botón — al pasar por encima está en `none` a propósito, para
   que un globo bajo el cursor no tape la marca de al lado. */
.noticias-globo.is-fijado {
  pointer-events: auto;
  border-color: var(--border-strong);
}

.noticias-globo__accion {
  align-self: flex-start;
  margin-top: var(--space-1);
  padding: 0.15rem var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--text-primary);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  cursor: pointer;
}

.noticias-globo__accion:hover { border-color: var(--border-strong); }

/* El motivo por el que no hay botón. Ver el comentario del controlador: son
   tres motivos distintos y ninguno es "está roto". */
.noticias-globo__sin_grafico {
  margin-top: var(--space-1);
  padding-top: var(--space-1);
  border-top: 1px solid var(--border-subtle);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  color: var(--text-tertiary);
}

/* ── El pie: lo que el dibujo no muestra ──────────────────────────────────────
   Las cuentas que no cierran contra el resto de la pantalla, dichas. Va al
   final y en el gris más apagado: es una aclaración, no un dato que compita con
   el dibujo. */
.noticias-pulso__pie {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-subtle);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  color: var(--text-tertiary);
}

/* ── El contexto: público contra ficha del instrumento ────────────────────────
   La única diferencia real es la DENSIDAD, y es la misma regla que ordena todo
   el design system: afuera se lee de corrido, adentro hay que ver diez cosas a
   la vez.

   En la ficha la tarjeta la pone la vista (`.card`, con su fondo, su borde y su
   padding), así que el componente apaga los suyos: dos bordes concéntricos con
   dos radios distintos es el defecto que delata un componente pegado en un
   lugar para el que no se pensó. */
.noticias-pulso--instrumento {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  margin-bottom: 0;
}

.noticias-pulso--instrumento .noticias-pulso__cabecera { margin-bottom: var(--space-4); }

.noticias-pulso--instrumento .noticias-pulso__bajada { font-size: var(--text-xs); }

/* El radar se CAPA acá, y no en la pantalla pública, porque los anchos son
   otros: la sección pública vive adentro de `.noticias-shell` (74rem) y la
   ficha ocupa el ancho entero del contenido. Medido a 1568 px, con la fracción
   de la pública el semicírculo se iba a ~700 px de ancho y 480 de alto — más
   que el gráfico de velas de arriba, para una tarjeta secundaria. El dibujo no
   gana nada estirándose: son 60 marcas como mucho, y el eje angular ya está
   entero en 30rem.

   Lo que sobra se lo queda la lectura, que sí lo usa: las cifras pasan a
   cuatro columnas y quedan leyéndose como la franja de stats de arriba de esta
   misma pantalla, que es el vocabulario que el usuario ya tiene acá. */
.noticias-pulso--instrumento .noticias-pulso__grid {
  /* Acá el panel está SIEMPRE abierto —lleva los titulares del día— y el radar
     tiene un techo: al lado hay un gráfico de velas, y un radar que se estira a
     media pantalla le roba el lugar. */
  gap: var(--space-6);
}

.noticias-pulso--instrumento .noticias-radar { max-width: 30rem; }

/* En la ficha el radar tiene techo (30rem), así que el panel absorbe lo que
   sobra en vez de dejar un hueco a la derecha. */
.noticias-pulso--instrumento.tiene-panel .noticias-panel { flex: 1 1 18rem; }

/* `auto-fit` y no cuatro columnas fijas: la columna de la lectura es lo que
   sobra después del radar, así que su ancho depende del ancho de la pantalla —
   cuatro fijas entran cómodas en un monitor y quedan en cuatro números de un
   dígito con la etiqueta partida en tres renglones en una ventana de tablet.
   Con `auto-fit` son cuatro cuando hay lugar, dos cuando no, y una en el peor
   caso, sin un breakpoint más que mantener. */
.noticias-pulso--instrumento .noticias-cifras {
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: var(--space-2);
}

/* ── Las cifras, en la ficha: apoyo y no titular ──────────────────────────────
   ACÁ Y NO EN LA PÚBLICA, y es la misma regla de densidad que gobierna el resto
   del modificador.

   El motivo es de proporción, no de gusto. En la ficha el radar está CAPADO a
   30rem (la regla de arriba) y todo lo que sobra se lo lleva la lectura, así
   que a 1568 px las cuatro cifras se estiran a cajas anchas con `--text-lg`
   adentro: cuatro números de dos dígitos terminan pesando más que el dibujo que
   vienen a resumir, en una tarjeta que además es secundaria —arriba está el
   gráfico de velas, que es el objeto principal de la pantalla—. El radar es el
   protagonista y las cifras son la nota al pie que lo pone en palabras.

   En la sección pública la proporción es la otra y por eso no se toca: ahí el
   pulso ES la pieza principal de su franja, el radar no está capado (se reparte
   1,35fr contra 1fr dentro de un shell de 74rem) y las cifras son sólo dos por
   fila. Achicarlas allá debilitaría el único resumen numérico de una pantalla
   que no tiene un gráfico de precio con el que competir, para arreglar un
   problema que allá no existe. */
.noticias-pulso--instrumento .noticias-cifra { padding: var(--space-2); }

.noticias-pulso--instrumento .noticias-cifra__valor {
  font-size: var(--text-base);
  margin-top: 0;
}

.noticias-pulso--instrumento .noticias-cifra__de { font-size: var(--text-xs); }

/* La leyenda son cuatro frases cortas: a lo ancho de media pantalla el ojo
   pierde el renglón entre la muestra y el texto. */
.noticias-pulso--instrumento .noticias-leyenda__item { max-width: 34rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   La sección de noticias de la ficha de un par: la tarjeta que las contiene
   ═══════════════════════════════════════════════════════════════════════════
   Esto NO es el componente: es el marco que la ficha le pone alrededor, con los
   titulares del día encima del radar y el plegado. Vive en este archivo porque
   nace y muere con el pulso; la sección pública no lo usa.
   ═══════════════════════════════════════════════════════════════════════════ */

.noticias-ficha__cabecera {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* El disparador es el TÍTULO, no un ícono de 16px al costado. Plegar y
   desplegar es la acción principal de esta cabecera y el blanco tiene que ser
   del tamaño de la frase — con la sección plegada, este renglón y el resumen de
   al lado son lo único que hay. */
.noticias-ficha__disparador {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  text-align: left;
}

.noticias-ficha__flecha {
  color: var(--text-tertiary);
  font-size: var(--text-xs);
  line-height: 1;
  transition: transform 120ms ease;
}

/* Plegada la flecha apunta a la derecha, que es la convención de todo árbol. */
.noticias-ficha.is-plegada .noticias-ficha__flecha { transform: rotate(-90deg); }

.noticias-ficha__disparador:hover .noticias-ficha__flecha { color: var(--text-primary); }

/* Qué hay adentro, con los números. Plegada, esto es TODO lo que la sección
   dice de sí misma: sin este renglón, cerrarla la borra de la pantalla y de la
   cabeza del que la cerró. */
.noticias-ficha__resumen {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  line-height: var(--leading-snug);
}

/* ── Los titulares del día, adentro de la sección ──────────────────────────
   Separados del radar por una línea y no por otra tarjeta: son dos lecturas de
   las mismas filas y meterlas en dos cajas con borde las volvería a separar,
   que es de lo que se venía. */
.noticias-ficha__titulares {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-subtle);
}

/* El radar cuelga de la sección, así que su título baja un escalón. */
.noticias-ficha__subtitulo {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

/* El aire entre la cabecera y el panel. Plegado no deja hueco: `hidden` lo
   saca del flujo entero (display:none), márgenes incluidos, y por eso este
   archivo no le pone `display` a ese div — se lo llevaría puesto. */
.noticias-ficha__panel { margin-top: var(--space-4); }

/* ── Teléfono ─────────────────────────────────────────────────────────────
   Mismo corte que design_system/responsive.css. Vale para las dos pantallas:
   la pública se abre desde el teléfono más que desde ningún lado, y la ficha de
   un par también. */
@media (max-width: 768px) {
  .noticias-pulso { padding: var(--space-4); }

  .noticias-pulso--instrumento { padding: 0; }

  /* Una columna: el radar arriba, los números y la leyenda abajo. */
  /* Apilados: el panel deja de ser una columna y pasa a ser el bloque de abajo.
     `flex-basis: auto` es lo que lo saca del ancho fijo — con 22rem en una
     pantalla de 390 px se sale por la derecha. */
  .noticias-pulso__grid,
  .noticias-pulso--instrumento .noticias-pulso__grid { flex-direction: column; }

  .noticias-pulso--instrumento .noticias-radar { max-width: none; }

  .noticias-pulso.tiene-panel .noticias-panel,
  .noticias-pulso--instrumento.tiene-panel .noticias-panel {
    flex-basis: auto;
    width: 100%;
  }

  /* Y la caja puede ser más baja: apilada, abajo no hay una línea de tiempo al
     lado que se corra. */
  .noticias-marcadas { height: 20rem; }

  /* Las etiquetas de los anillos se apagan acá.
     No es esconder información: el texto de un SVG se escala con el viewBox, y
     a 390 px de ancho esos 10 px de unidad quedan en 6,8 px reales — están
     dibujados y no se leen, que para el que mira es peor que no estar, porque
     ensucian el centro del dibujo. La escala sigue estando escrita: el último
     renglón de la leyenda nombra los cinco anillos en orden, y el globo de cada
     marca dice su plazo. */
  .noticias-radar__plazo { display: none; }

  /* Dos por fila en un teléfono. La regla base ya usa `auto-fit` con mínimo de
     9rem, pero abajo de 390 px eso da una sola columna y cuatro renglones de
     una cifra cada uno: se fuerzan dos. */
  .noticias-cifras { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .noticias-globo { max-width: 14rem; }

  /* El panel de ayuda deja de colgar del ícono y pasa a ocupar el ancho de la
     tarjeta. Anclado al ícono son 22rem que arrancan donde termina el título:
     en 390 px eso se sale por la derecha y arrastra la página entera de
     costado, que es el único defecto de layout que se nota en todas las
     pantallas a la vez.

     `position: static` en el ancla hace que el panel se mida contra
     `.noticias-pulso` (que es `position: relative`), y `top: auto` lo deja
     justo donde habría caído en el flujo — o sea, debajo del renglón del
     título, que es donde tiene que estar. */
  .noticias-ayuda { position: static; }

  .noticias-ayuda__panel {
    top: auto;
    left: 0;
    right: 0;
    width: auto;
    max-width: none;
  }
}
/* Capa responsive del sistema de diseño.
 *
 * El sitio se armó para escritorio: antes de esto había UN solo breakpoint en
 * todo el sistema de diseño. En un teléfono de 375 px el tablero medía 660 px de
 * ancho, o sea que la página entera se arrastraba de costado y la mitad del
 * contenido quedaba fuera de la pantalla.
 *
 * El criterio es el mismo en todos los casos: **la página nunca scrollea de
 * costado**. Lo que no entra, envuelve; y lo que no puede envolver —una tabla de
 * catorce columnas— scrollea DENTRO de su propia caja.
 *
 * Va en su propio archivo y se carga al final (`require_tree` es alfabético y
 * `responsive` viene después de `components`) para que estas reglas ganen sin
 * tener que ensuciar cada componente con su propia media query.
 *
 * 768 px es el corte: por debajo hay un teléfono, por arriba una tablet en
 * horizontal o algo más grande.
 */

@media (max-width: 768px) {
  /* ── Lo que no entra, envuelve ───────────────────────────────────────────
   *
   * El encabezado de tarjeta pone el título a la izquierda y sus controles a la
   * derecha con `justify-content: space-between`. En un teléfono no hay lugar
   * para las dos cosas en la misma línea: el bloque de la derecha —las pastillas
   * de 7/30 días más los dos campos de fecha— medía 529 px él solo.
   */
  .card-header {
    flex-wrap: wrap;
    row-gap: var(--space-2);
  }

  /* Las filas flex del CONTENIDO envuelven.
   *
   * En escritorio una fila de botones o de badges entra siempre; en un teléfono
   * empuja el ancho de la página entera. El patrón "título a la izquierda,
   * acciones a la derecha" aparece en 54 vistas, así que la regla tiene que ser
   * general y no una corrección por pantalla.
   *
   * Envolver sólo hace algo cuando el contenido NO entra, así que en las filas
   * que ya entraban no cambia nada.
   *
   * Alcance `main`: deja afuera la barra superior, que resuelve lo suyo
   * scrolleando en su propia caja y donde envolver la rompería.
   *
   * Para que una fila NO envuelva —un grupo de pastillas que preferimos que
   * scrollee— está `.flex-nowrap`, que se respeta.
   *
   * `!important` porque son utilidades de Tailwind y de otro modo pierden.
   */
  main .flex:not(.flex-col):not(.flex-nowrap) {
    flex-wrap: wrap !important;
  }

  /* Un hijo flex no puede negarse a achicarse.
   *
   * `min-width` vale `auto` por defecto en un ítem flex, o sea "nunca más chico
   * que mi contenido". Un `flex-1` con algo ancho adentro se planta en el ancho
   * de ese contenido y arrastra la página: medido en Ajustes, un panel de
   * `flex-1` ocupaba **1072 px** en una pantalla de 375.
   *
   * Es la misma trampa que el CSS de la barra superior ya documentaba para
   * `.topbar-nav`; acá se aplica a todos los hijos flex del contenido.
   */
  main .flex > *,
  main .flex-1 { min-width: 0; }

  /* Y lo que de verdad es ancho —un bloque de código, una configuración para
   * copiar y pegar— scrollea solo en vez de estirar a su padre. */
  main pre,
  main code { max-width: 100%; overflow-x: auto; }

  /* ── Lo que no puede envolver, scrollea en su caja ───────────────────────
   *
   * Una tabla de posiciones tiene catorce columnas y no hay forma de que entre
   * en 375 px. `.table-scroll` ya existía y hace exactamente esto; acá se
   * refuerza que el contenedor no se estire más allá de la pantalla.
   */
  .table-scroll {
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* ── Las grillas pasan a una columna ─────────────────────────────────────
   *
   * Esto empezó como una LISTA de las variantes que el proyecto usaba
   * —grid-cols-2/3/4 y sus prefijos md: y lg:— y la lista se quedó corta. En
   * `/admin/llm_usages` había un `grid grid-cols-5` que no estaba enumerado y
   * arrastraba la página 49 px: la grilla medía 412 px de ancho en una pantalla
   * de 375. Contando los usos reales faltaban además `sm:grid-cols-2/3/4/5`,
   * `xl:grid-cols-3`, `lg:grid-cols-5`, `lg:grid-cols-6` y `lg:grid-cols-12`.
   *
   * Enumerar era el error: cada grilla nueva que alguien escriba vuelve a
   * romperlo en silencio. Se matchea por el nombre de la clase, con una sola
   * excepción — `grid-cols-1` ya ES de una columna y es la única forma de decir
   * "esta grilla la pensé para pantalla chica".
   */
  main [class*="grid-cols-"]:not(.grid-cols-1),
  .card [class*="grid-cols-"]:not(.grid-cols-1) {
    grid-template-columns: 1fr !important;
  }

  /* ── El aire de escritorio no cabe en un teléfono ────────────────────────
   *
   * Un padding de 24 px por lado se come el 13 % del ancho útil.
   */
  .card { padding: var(--space-3); }
  main,
  .page-shell,
  .topbar-inner { padding-left: var(--space-3); padding-right: var(--space-3); }

  /* ── El panel de detalle se lee sin scrollear de costado ─────────────────
   *
   * Un panel vive en un `<td colspan>` y por lo tanto hereda el ancho de la
   * TABLA, no el de la pantalla. En el listado de estrategias son once columnas:
   * medido en 375 px, el panel salía de **1179 px** y su gráfico de capital
   * también, así que se veía un tercio de la curva y había que arrastrar la
   * tabla de costado para leer el resto.
   *
   * La tabla sigue scrolleando —once columnas no entran y eso está bien— pero
   * el contenido del panel se acota al ancho visible: es un bloque de lectura,
   * no una fila de datos.
   */
  .row-detail > td > * {
    max-width: calc(100vw - var(--space-6));
  }

  /* ── Nada puede ser más ancho que la pantalla ────────────────────────────
   *
   * Red de último recurso para el contenido que llega de afuera —una pregunta de
   * Polymarket larguísima, el nombre de una estrategia, una URL— y para las
   * imágenes sin `max-width`.
   *
   * `height: auto` va para lo que tiene proporción propia que conservar cuando
   * el ancho se recorta: una imagen, un video. NO para los `<svg>`.
   *
   * Los gráficos de esta app son SVG inline con `preserveAspectRatio="none"` y
   * un alto explícito (`h-16`, `h-20`), y su viewBox es `0 0 <días> 100` — una
   * proporción altísima que nunca se dibuja porque el alto la fija el CSS. Con
   * `height: auto` acá adentro, en pantallas chicas el alto explícito se perdía y
   * el SVG volvía a su proporción intrínseca: medido, el acumulado del período
   * pasaba de 64 px a más de MIL de alto en un teléfono. Se veía en el inicio, en
   * el panel de sesiones, en el de estrategias y en el detalle de un backtest —
   * los cuatro por esta línea.
   *
   * No alcanza con bajarle la especificidad con `:where()`: las utilidades de
   * Tailwind viven en `@layer utilities` y cualquier regla sin capa les gana sin
   * importar la especificidad. Hay que no escribir la propiedad.
   */
  img, video, canvas { max-width: 100%; height: auto; }
  svg { max-width: 100%; }

  /* Las palabras larguísimas sin espacios cortan; el resto del texto no se toca
   * para no arruinar la tipografía. */
  .break-anywhere,
  .card-title,
  .type-mono { overflow-wrap: anywhere; }

  /* ── Los controles flotantes se corren al borde ──────────────────────────
   *
   * El widget de régimen de mercado vive en la esquina inferior derecha con un
   * margen pensado para escritorio.
   */
  .fixed.bottom-4.right-4 { bottom: var(--space-2); right: var(--space-2); }

  /* ── La barra superior no puede quedarse sin navegación ─────────────────
   *
   * `.topbar-brand` y `.topbar-actions` tienen `flex-shrink: 0`, así que cuando
   * no entran los tres bloques el único que cede es `.topbar-nav` — y cede TODO:
   * medido en el tablero de Polymarket, la marca ocupaba 214 px, las acciones
   * 159, y la navegación quedaba en **0 px de ancho**. No es que se veía mal: no
   * se veía. Sin el menú no hay forma de ir a ninguna parte.
   *
   * Se recupera espacio de donde sobra. La etiqueta con el nombre de la
   * plataforma es información repetida: el selector de espacio de trabajo, que
   * está justo al lado, ya dice en cuál estás.
   */
  .topbar-brand .platform-tag { display: none; }

  /* Y la barra pasa a DOS FILAS: arriba la marca y las acciones, abajo la
   * navegación a lo ancho.
   *
   * Pelear por píxeles entre los tres bloques no cierra —la marca y las acciones
   * juntas ya son 373 px de los 375 disponibles— y cualquier reparto deja a
   * alguno inservible. Con dos filas cada uno tiene su espacio: la navegación
   * ocupa el ancho completo y sigue scrolleando adentro suyo si hace falta.
   */
  .topbar-inner { flex-wrap: wrap; row-gap: var(--space-1); }
  .topbar-brand   { flex: 1 1 auto; min-width: 0; }
  .topbar-actions { flex: 0 0 auto; }
  .topbar-nav     { flex: 1 0 100%; order: 3; }

  /* ── Objetivos táctiles ──────────────────────────────────────────────────
   *
   * 44 px es el mínimo recomendado para tocar con el dedo. Los enlaces de la
   * barra y los botones de ícono quedaban en 32-36.
   */
  .topbar-link,
  .topbar-icon-btn,
  .btn { min-height: 44px; }
}

/* Pantallas MUY angostas (teléfonos chicos y ventanas partidas). */
@media (max-width: 400px) {
  /* Los grupos de pastillas —ventanas de tiempo, filtros de estado— scrollean
   * en lugar de envolver a tres filas. */
  .pill-group,
  .segmented { overflow-x: auto; flex-wrap: nowrap; scrollbar-width: none; }
  .pill-group::-webkit-scrollbar,
  .segmented::-webkit-scrollbar { display: none; }
}
/* ═══════════════════════════════════════════════════════════════════════════
   etrinok Design System — Shell (raíz del layout + modal de confirmación)
   spec docs/REDESIGN_SPEC.md §4/§5. Consumido por:
     - app/views/layouts/trading.html.erb          (.app-shell, @keyframes)
     - app/views/shared/_confirm_modal.html.erb     (.confirm-modal-*)

   Cero hex acá: todo color sale de tokens.css. Este archivo no se declara en
   application.css (que solo hace require explícito de tokens/base/components)
   — lo agarra el `require_tree .` de más abajo, igual que nav.css. Dentro de
   design_system/ el orden de `require_tree` es alfabético y tokens/base/
   components ya cargaron por su require explícito, así que lo único que
   compite acá es nav.css (n < s) — este archivo carga después, y por lo
   tanto gana por cascada a igual especificidad contra cualquier clase de
   components.css (ej. `.modal-panel`) sin necesitar más especificidad.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── App shell ───────────────────────────────────────────────────────────
   El body ya hereda background/color de body{} en base.css — esta clase no
   cambia el resultado visual, pero deja el markup honesto en vez de un
   <body> sin ninguna clase que documente su rol (spec: cero atributos style
   en el layout). */
.app-shell {
  background: var(--surface-canvas);
  color: var(--text-primary);
}

/* Ícono de tareas activas (engranaje) — active_tasks_controller.js anima
   `this.iconTarget.style.animation = "activetask-spin ..."` directamente
   por JS (y también en el template string de cada fila individual del
   panel), así que este keyframe no tiene call site en ninguna clase CSS.
   Vivía en un <style> suelto de trading.html.erb; se muda acá para que siga
   cargando siempre, sin el resto del bloque legacy (--trading-* / .stat-up
   /.stat-down no tenían call sites vivos — ver comentario que reemplazan en
   trading.html.erb). */
@keyframes activetask-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* ── Confirm modal (app/views/shared/_confirm_modal.html.erb) ─────────────
   Círculo de aviso del header + fila de acciones del footer. El resto del
   modal (backdrop, panel, botones) ya usa clases del design system
   (.modal-backdrop, .modal-panel, .btn) — acá solo van los dos bloques que
   antes tenían `style` inline. */
.confirm-modal-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-full);
  background: var(--warning-subtle);
  border: 1px solid var(--warning-border);
}

.confirm-modal-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border-default);
  background: var(--surface-sunken);
}
/* ═══════════════════════════════════════════════════════════════════════════
   Strategy Canvas Builder — n8n-style visual node editor
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Previsualizador del árbol ───────────────────────────────────────────────
 *
 * Vive DEBAJO del lienzo, no adentro: .strategy-canvas-root tiene alto fijo y
 * overflow:hidden, y un gráfico ahí adentro queda cortado por la mitad.
 *
 * El lenguaje visual es el del resto de los gráficos del sitio (superficie
 * hundida, borde sutil, rótulos en text-tertiary): es el mismo objeto que se ve
 * en la sesión de bot y en el backtest, sólo que armándose. */

.strategy-preview {
  margin-top: 16px;
  border: 1px solid var(--border-default);
  border-radius: 16px;
  background: var(--surface-base);
  overflow: hidden;
}

.strategy-preview-head {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border-subtle);
}

.strategy-preview-field {
  display: flex;
  align-items: center;
  gap: 6px;
}

.strategy-preview-field-label {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  white-space: nowrap;
}

.strategy-preview-hint {
  font-size: var(--text-xs);
  color: var(--text-disabled);
}

/* El estado (cargando / sin datos / error) se va al extremo derecho: es lo que
 * explica por qué el gráfico está vacío, y ahí no compite con los controles. */
.strategy-preview-estado {
  margin-left: auto;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}
.strategy-preview-estado[hidden] { display: none; }

/* El nodo que está gobernando el gráfico: aparece al clickear un nodo del
 * canvas y se apaga al mover el timeframe a mano. Lleva el acento porque es lo
 * ÚNICO de la barra que cambió por una acción del usuario en otro lado de la
 * pantalla, y sin marcarlo el gráfico parecería moverse solo. */
.strategy-preview-nodo {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--accent-border);
  background: var(--accent-subtle);
  color: var(--accent-text);
  font-size: var(--text-xs);
  white-space: nowrap;
}
.strategy-preview-nodo[hidden] { display: none; }

/* La leyenda y sus controles comparten fila: los botones de "todos / ninguno"
 * y el aviso de que esto es sólo del gráfico van pegados a las pastillas que
 * gobiernan, no en otra franja. */
.strategy-preview-legend-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 8px 14px 0;
}
.strategy-preview-legend-row:empty { display: none; }

.strategy-preview-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-width: 0;
}
.strategy-preview-legend:empty { display: none; }

.strategy-preview-legend-controles {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.strategy-preview-legend-controles[hidden] { display: none; }

.preview-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-subtle);
  background: var(--surface-sunken);
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* La pastilla como INTERRUPTOR. Es un <button>, así que hay que devolverle lo
 * que el navegador le saca (el tipo de letra) y darle el cursor de lo que es. */
.preview-chip--boton {
  cursor: pointer;
  font-family: var(--font-mono);
  transition: opacity var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}
.preview-chip--boton:hover { border-color: var(--border-strong); }
.preview-chip--boton:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
}

/* Apagado por el usuario: se atenúa y el texto se tacha. El tachado hace falta
 * además de la opacidad porque "atenuado" también podría leerse como
 * "deshabilitado", y esta pastilla se puede volver a apretar. */
.preview-chip--boton.is-apagado {
  opacity: 0.45;
  text-decoration: line-through;
}
.preview-chip--boton.is-apagado .preview-chip-dot {
  background: var(--text-disabled);
}

/* Prendido pero sin panel libre (el techo de MAX_PANELES). No es lo mismo que
 * apagado —el usuario lo quiere ver— así que se marca distinto: borde punteado
 * en vez de tachado, que es el lenguaje de "está, pero no entra". */
.preview-chip--boton.is-sin-lugar {
  opacity: 0.7;
  border-style: dashed;
  border-color: var(--warning);
}
.preview-chip--boton.is-sin-lugar .preview-chip-dot {
  background: var(--warning);
}

/* El punto lleva el color REAL de la serie, que se lo pone el JS: es lo que ata
 * la pastilla con la línea del gráfico. Es el único color de acá que no sale de
 * un token — no hay token para "la tercera EMA".
 *
 * El gris es el piso: una familia con una sola instancia se dibuja con el color
 * del catálogo y el JS no le pasa ninguno, así que sin esto el punto quedaba
 * transparente — un hueco de 8px antes del texto. */
.preview-chip-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--text-tertiary);
}

.strategy-preview-canvas {
  position: relative;
  margin: 10px 14px;
  min-height: 200px;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-sunken);
}

.strategy-preview-summary {
  margin: 0;
  padding: 0 14px 12px;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

/* ── Root layout ──────────────────────────────────────────────────────────── */

.strategy-canvas-root {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 220px);
  min-height: 540px;
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--border-default);
  background: var(--surface-canvas);
}

/* ── Toolbar ───────────────────────────────────────────────────────────────── */

.canvas-toolbar {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  background: var(--surface-base);
  border-bottom: 1px solid var(--border-subtle);
}

/* Two rows inside the toolbar.
 *
 * Una sola fila que scrollea de costado, NO envuelve. Antes era
 * `flex-wrap: wrap` con `height: 46px` fijo y `overflow: hidden`: con quince
 * plantillas, las que no entraban pasaban a una segunda fila que quedaba
 * RECORTADA a la mitad, y el panel de "Qué hace cada plantilla" de abajo
 * parecía estar tapándolas.
 *
 * Scroll horizontal y no altura libre porque la barra tiene que quedar fija:
 * el lienzo de abajo se mide con calc(100vh - …) y cada fila extra le come
 * alto. Es además la convención que el proyecto ya usa para las filas de
 * chips (ver el listado de estrategias, "una sola fila scrolleable"). */
.canvas-toolbar-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 14px;
  height: 46px;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}
/* Los chips no se comprimen: con nowrap, flex los achicaría hasta hacerlos
   ilegibles en vez de dejarlos scrollear. */
.canvas-toolbar-row > * {
  flex-shrink: 0;
}
.canvas-toolbar-row:first-child {
  border-bottom: 1px solid var(--border-subtle);
}
/* La franja por donde se empieza: la FORMA del árbol y, del otro extremo, las
   dos acciones que lo reemplazan entero (plantilla y reset). Va con fondo
   hundido para que se lea como una barra aparte y no como la primera de dos
   filas de controles iguales.

   Ésta SÍ envuelve, al revés que la fila de controles. Es la excepción y tiene
   motivo: el selector de forma no puede quedar detrás de un scroll horizontal
   —medido a 375px, en el orden anterior escondía 620px y el botón "Paralelo"
   caía a 611px del comienzo— y esta fila no tiene alto fijo, que es lo que en
   la otra hacía que la segunda línea quedara recortada al medio. */
.canvas-toolbar-row--start {
  height: auto;
  min-height: 58px;
  padding-top: 8px;
  padding-bottom: 8px;
  row-gap: 6px;
  flex-wrap: wrap;
  overflow-x: visible;
  background: var(--surface-sunken);
}

/* El hueco elástico que separa la forma (izquierda) de plantilla+reset
   (derecha) sólo tiene sentido mientras las dos cosas entran en la misma línea.
   Cuando la fila envuelve, ese div de ancho fijo cae en una línea propia y
   deja a "Elegir plantilla" y a "Vaciar árbol" escalonados en tres renglones
   distintos. Sin él, envuelven en orden. */
@media (max-width: 760px) {
  .canvas-toolbar-row--start > .flex-1 { display: none; }
}

.canvas-toolbar-row--controls {
  gap: 4px;
}

/* Deprecated: kept for compat, but rows handle spacing now */
.canvas-toolbar-group {
  display: flex;
  align-items: center;
  gap: 6px;
}
.canvas-toolbar-group:first-child { padding-left: 0; }
.canvas-toolbar-group--right { margin-left: auto; }

.canvas-toolbar-label {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-right: 4px;
  white-space: nowrap;
}

.canvas-preset-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 6px;
  border: 1px solid var(--border-default);
  background: var(--surface-raised);
  color: var(--text-tertiary);
  font-size: 11px;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.canvas-preset-btn:hover {
  border-color: var(--warning-border);
  color: var(--warning);
  background: var(--warning-subtle);
}
.canvas-preset-blank {
  color: var(--text-tertiary);
  border-style: dashed;
}
.canvas-preset-blank:hover {
  border-color: var(--border-strong);
  color: var(--text-tertiary);
  background: transparent;
}

/* Management toggles */
.canvas-mgmt-toggle { display: contents; }
.canvas-mgmt-checkbox { display: none; }
.canvas-mgmt-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 6px;
  border: 1px solid var(--border-default);
  background: var(--surface-raised);
  color: var(--text-tertiary);
  font-size: 11px;
  cursor: pointer;
  white-space: nowrap;
  transition: all 0.15s;
  user-select: none;
}
.canvas-mgmt-checkbox:checked + .canvas-mgmt-btn {
  border-color: var(--accent-border);
  color: var(--accent-text);
  background: var(--accent-subtle);
}
.canvas-mgmt-btn:hover {
  border-color: var(--accent-border);
  color: var(--accent);
}

/* Iban en --text-disabled, que en este sistema es el color de un control
   apagado: los dos botones parecían inhabilitados y encima, con dos ramas en
   pantalla, efectivamente no hacían nada. Con la acción arreglada, el color
   también tiene que decir que se pueden apretar. */
.canvas-add-btn {
  padding: 4px 10px;
  border-radius: 5px;
  border: 1px solid var(--border-default);
  background: transparent;
  color: var(--text-tertiary);
  font-size: 10px;
  cursor: pointer;
  white-space: nowrap;
  transition: all 0.15s;
}
.canvas-add-btn--entry:hover {
  border-color: var(--info-border);
  color: var(--info);
  background: var(--info-subtle);
}
.canvas-add-btn--exit:hover {
  border-color: var(--warning-border);
  color: var(--warning);
  background: var(--warning-subtle);
}

/* Un juego de botones por forma del árbol: con una regla son dos (entrada y
   salida), con dos ramas son cuatro (uno por columna). Se resuelve por CSS y no
   por JS a propósito — el servidor ya manda `data-forma` resuelto, así que el
   juego correcto está en el primer cuadro y no aparece recién cuando arranca
   Stimulus. */
.canvas-toolbar[data-forma="una"] [data-canvas-shape="dos"],
.canvas-toolbar[data-forma="dos"] [data-canvas-shape="una"] {
  display: none;
}

/* ── Selector de la forma del árbol ───────────────────────────────────────
   Tres formas mutuamente excluyentes (una regla / portón / dos en paralelo),
   así que va como segmentado y no como tres botones sueltos: la forma del
   control dice que elegís UNA.

   Es más grande que un chip de la barra a propósito. Antes tenía el mismo peso
   que los otros ocho controles y quedaba último en la fila más larga, con 4px
   de separación del grupo "+ CONDICIONES": las dos cosas se leían como una
   sola tira de palabras sueltas. */

.canvas-mode-group {
  display: inline-flex;
  border: 1px solid var(--border-default);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface-base);
}

.canvas-mode-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border: none;
  border-right: 1px solid var(--border-default);
  background: transparent;
  color: var(--text-tertiary);
  font-size: var(--text-xs);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s;
}

.canvas-mode-btn:last-child { border-right: none; }

.canvas-mode-btn:hover:not(:disabled) {
  color: var(--accent-text);
  background: var(--accent-subtle);
}

.canvas-mode-btn.is-active {
  background: var(--accent-subtle);
  color: var(--accent-text);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--accent);
}

/* Abajo de 400px las tres opciones lado a lado miden 287px y el contenido de la
   franja da 256: el selector se salía de la barra. Apiladas entran enteras y
   siguen leyéndose como un grupo, que es lo único que no se puede perder —
   quedar cortado es de dónde salió todo esto. */
@media (max-width: 400px) {
  .canvas-mode-group {
    flex-direction: column;
    width: 100%;
  }
  .canvas-mode-btn {
    border-right: none;
    border-bottom: 1px solid var(--border-default);
    justify-content: flex-start;
  }
  .canvas-mode-btn:last-child { border-bottom: none; }
}

/* El diagrama de la forma que arma cada opción. `currentColor` para que siga
   el estado del botón (apagado / hover / activo) sin una segunda regla. */
.canvas-mode-glifo {
  width: 26px;
  height: 14px;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Con un router por régimen activo el árbol se pasa crudo y reestructurarlo lo
   rompería: el selector queda apagado en vez de mentir que se puede. */
.canvas-mode-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* La consecuencia del modo, en renglón propio y a lo ancho de la franja.
   `flex-basis: 100%` la empuja a su propia línea: si compartiera línea con el
   selector, en cuanto se achica la pantalla vuelve a leerse como más palabras
   pegadas a los botones, que es el defecto que se está arreglando. */
.canvas-mode-desc {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--text-xs);
  line-height: 1.4;
  color: var(--text-tertiary);
}

/* Atajo a la plantilla-esqueleto del modo paralelo. Va como enlace y no como
   botón lleno: es una ayuda opcional, no la acción de la franja. */
.canvas-mode-esqueleto {
  padding: 4px 8px;
  border: 1px dashed var(--accent-border);
  border-radius: 6px;
  background: transparent;
  color: var(--accent-text);
  font-size: var(--text-xs);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s;
}
.canvas-mode-esqueleto:hover { background: var(--accent-subtle); }
/* Se esconde con el atributo `hidden` y no con una clase, para que el estado
   inicial lo mande el SERVIDOR y no aparezca-y-desaparezca al arrancar el JS.
   La regla es explícita porque el `display` de la hoja del navegador lo pisa
   cualquier `display` que se le agregue después a la clase. */
.canvas-mode-esqueleto[hidden] { display: none; }

/* ── Canvas + panel flex row ─────────────────────────────────────────────── */

.canvas-body-row {
  display: flex;
  flex: 1;
  overflow: hidden;
  position: relative;
}

/* ── Canvas outer ──────────────────────────────────────────────────────────── */

.canvas-outer {
  flex: 1;
  position: relative;
  overflow: auto;
  cursor: default;
  background: var(--surface-canvas);
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
  min-width: 0;
}
.canvas-outer::-webkit-scrollbar { width: 6px; height: 6px; }
.canvas-outer::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }

/* Dot grid background */
.canvas-grid-bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(circle, var(--border-default) 1px, transparent 1px);
  background-size: 28px 28px;
  opacity: 0.6;
}

/* Section header bars (static visual guide at the top) */
.canvas-section-headers {
  position: sticky;
  top: 0;
  left: 0;
  z-index: 10;
  display: flex;
  gap: 0;
  background: color-mix(in srgb, var(--surface-canvas) 93%, transparent);
  border-bottom: 1px solid var(--border-subtle);
  backdrop-filter: blur(8px);
  pointer-events: none;
}

.canvas-section-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-right: 1px solid var(--border-subtle);
  flex: 1;
}
.canvas-section-header--action { border-color: var(--positive-border); background: var(--positive-subtle); }
.canvas-section-header--sell   { border-color: var(--negative-border); background: var(--negative-subtle); border-right: none; }

.canvas-section-icon {
  font-size: 10px;
  color: var(--text-tertiary);
}
.canvas-section-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
}
.canvas-section-subtitle {
  font-size: 9px;
  color: var(--text-tertiary);
  margin-top: 1px;
  letter-spacing: 0.04em;
}

/* SVG overlay */
.canvas-svg-layer {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 1;
}

/* Nodes layer */
.canvas-nodes-layer {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  min-width: 700px;
  min-height: 500px;
}

/* ── Node enter animation ─────────────────────────────────────────────────── */

@keyframes nodeIn {
  from {
    opacity: 0;
    transform: scale(0.88) translateY(6px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

/* ── Empty state ───────────────────────────────────────────────────────────── */

.canvas-empty-state {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0.35;
}
.canvas-nodes-layer:not(:empty) + .canvas-empty-state { display: none; }

.canvas-empty-icon {
  font-size: 48px;
  color: var(--text-disabled);
  margin-bottom: 12px;
  animation: pulse 3s ease-in-out infinite;
}
.canvas-empty-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-tertiary);
  margin-bottom: 6px;
}
.canvas-empty-sub {
  font-size: 12px;
  color: var(--text-disabled);
}

@keyframes pulse {
  0%, 100% { opacity: 0.4; transform: scale(1); }
  50%       { opacity: 0.7; transform: scale(1.04); }
}

/* ── Right panel ───────────────────────────────────────────────────────────── */

.canvas-right-panel {
  position: absolute;
  top: 52px; /* below toolbar */
  right: 0;
  width: 300px;
  bottom: 0;
  background: var(--surface-base);
  border-left: 1px solid var(--border-subtle);
  z-index: 20;
  transform: translateX(100%);
  opacity: 0;
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Docked variant: sits next to canvas in flex row, expands via max-width */
.canvas-right-panel--docked {
  position: relative;
  top: auto;
  right: auto;
  bottom: auto;
  width: 0;
  max-width: 0;
  flex-shrink: 0;
  overflow: hidden;
  transform: none !important;
  opacity: 1 !important;
  transition: max-width 0.25s cubic-bezier(0.16, 1, 0.3, 1),
              width 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

/* JS opens docked panel by setting transform to translateX(0) — we intercept that */
.canvas-right-panel--docked[style*="translateX(0"] {
  width: 300px;
  max-width: 300px;
}

.canvas-panel-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border-subtle);
  flex-shrink: 0;
}
.canvas-panel-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
}
.canvas-panel-subtitle {
  font-size: 11px;
  color: var(--text-disabled);
  margin-top: 2px;
}
.canvas-panel-close {
  font-size: 18px;
  color: var(--text-disabled);
  background: none;
  border: none;
  cursor: pointer;
  line-height: 1;
  padding: 2px 4px;
  transition: color 0.1s;
}
.canvas-panel-close:hover { color: var(--negative); }

.canvas-panel-body {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}

.canvas-panel-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  gap: 12px;
  opacity: 0.3;
}
.canvas-panel-placeholder-icon {
  font-size: 28px;
  color: var(--text-tertiary);
}
.canvas-panel-placeholder-text {
  font-size: 12px;
  color: var(--text-tertiary);
  text-align: center;
  max-width: 160px;
  line-height: 1.5;
}

/* ── Node picker modal ──────────────────────────────────────────────────────
*/

.canvas-picker-overlay {
  position: absolute;
  inset: 52px 0 0 0; /* below toolbar */
  /* Velo neutro, no un color de la paleta: oscurece lo de atrás
     igual en los dos temas. Ver el guard de canvas_uses_design_system. */
  background: rgba(0, 0, 0, 0.65);
  backdrop-filter: blur(4px);
  z-index: 30;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 32px;
}

.canvas-picker-modal {
  width: 520px;
  max-width: 90%;
  max-height: calc(100% - 80px);
  background: var(--surface-base);
  border: 1px solid var(--border-default);
  border-radius: 14px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-xl);
  animation: nodeIn 0.18s ease-out;
}

.picker-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border-subtle);
  flex-shrink: 0;
}

.picker-body {
  flex: 1;
  overflow-y: auto;
  padding: 14px 16px 16px;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}

.picker-category {
  margin-bottom: 4px;
}

/* Group card in picker */
.picker-group-section {
  padding: 10px 0 12px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border-subtle);
}

/* Sub-group node card on canvas — dashed border to distinguish from regular nodes */
.canvas-group-card {
  border-style: dashed !important;
  cursor: pointer;
}

/* Group operator pills inside the panel */
.group-op-btn {
  flex: 1;
  padding: 6px;
  border-radius: 7px;
  border: 1px solid var(--border-default);
  background: var(--surface-sunken);
  color: var(--text-disabled);
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
  transition: all 0.12s;
  text-align: center;
}
.group-op-btn--active-and {
  border-color: var(--info);
  background: var(--info-subtle);
  color: var(--info);
}
.group-op-btn--active-or {
  border-color: var(--warning);
  background: var(--warning-subtle);
  color: var(--warning);
}

/* Panel input styles */
.canvas-right-panel input[type="number"],
.canvas-right-panel select {
  transition: border-color 0.15s, box-shadow 0.15s;
}
.canvas-right-panel input[type="number"]:focus,
.canvas-right-panel select:focus {
  outline: none;
  border-color: var(--info) !important;
  box-shadow: 0 0 0 2px var(--info-border);
}

/* ── Section label (rendered by JS) ─────────────────────────────────────────── */
.canvas-section-label {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  pointer-events: none;
  user-select: none;
}

/* ── Selector de plantillas ───────────────────────────────────────────────
   Una fila por plantilla: ícono, nombre, qué hace en criollo y su badge. La
   fila ENTERA es el botón — con dieciséis opciones, obligar a apuntarle a un
   chip chico es pedir puntería para una decisión que ya cuesta tomar. */
/* Cada opción es una tarjeta, no un renglón de lista.
 *
 * Con dieciséis plantillas y una explicación de dos renglones cada una, el
 * borde transparente y el padding chico las dejaban como un bloque de texto
 * corrido: no se veía dónde terminaba una y empezaba la otra, y no se leía que
 * la fila entera fuera apretable. Ahora tienen borde propio, más aire y
 * separación entre sí. */
.preset-option {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  width: 100%;
  text-align: left;
  padding: 16px 18px;
  margin-bottom: 8px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}
.preset-option:hover {
  background: var(--surface-overlay);
  border-color: var(--accent);
}
/* El foco del teclado tiene que verse tanto como el hover: esta lista se
   recorre con flechas y Tab tanto como con el mouse. */
.preset-option:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  background: var(--surface-overlay);
}
.preset-option-icon {
  font-size: 26px;
  line-height: 1.2;
  flex-shrink: 0;
}
.preset-option-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.preset-option-name {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
}
.preset-option-plain {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.5;
}
.preset-option-badge {
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  color: var(--text-tertiary);
}
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 * Design system load order matters: tokens (CSS variables) must load before
 * base/components reference them, and both must load before the legacy
 * per-feature stylesheets below so design-system classes can be overridden
 * locally when a view still needs a one-off tweak.
 *





 */
