/* ============================================================
   RPH OS — Capa de componentes
   Requiere tokens.css. Prefijo .r- para todo.
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  font-weight: var(--fw-book);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-normal);
  /* EL SUAVIZADO SE QUEDA. Decisión de Robbie, 2026-08-23, viendo los dos lados:
     «a mí me gusta más cuando sí tiene suavizado».

     Vale dejarlo escrito porque llevaba tiempo propuesto para quitarse: una
     auditoría de sensación nativa lo señaló como «el factor que más hace que el
     OS se lea como web en vez de como app de Mac», y tenía razón en el
     diagnóstico — el texto sale más delgado que en Mail o en Notas. Lo que la
     auditoría no podía decidir es si eso es un defecto o el estilo, y es el
     estilo: la referencia visual del proyecto es rondesignlab, que usa texto
     delgado a propósito.

     Y la comparación costó descubrir algo que hay que saber antes de volver a
     abrirla: **esto solo se puede juzgar en Safari.** Un agente comparó las dos
     versiones ocho veces en el navegador de pruebas —cuatro vistas por dos
     temas— y salieron IDÉNTICAS, porque `-webkit-font-smoothing` es una
     propiedad de WebKit y Chromium moderno la ignora. La app de escritorio corre
     WKWebView, que es el motor de Safari. Una comparación en Blink no dice nada.

     No se quita. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "ss01" 1, "cv01" 1;
}

img, svg, video, canvas { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }

::selection { background: var(--accent); color: var(--on-accent); }

:where(a) { color: inherit; text-decoration: none; }

:where(:focus-visible) {
  outline: var(--stroke-2) solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* ---- Un control se siente de app, no de página ----
   Dos declaraciones, y las dos existen por la misma razón: en macOS el ratón
   sobre un control de una app se ve como FLECHA, y su rótulo no se arrastra
   como si fuera un documento. Un botón con manita anuncia «soy un enlace, te
   voy a llevar a otra parte», y en el OS ninguno lleva a ninguna parte.

   · `cursor: default` — la manita se quitó de los 19 componentes que la
     traían. No se cambió por otra cosa: `default` ES la flecha, y hace falta
     decirlo porque sin ella el cursor sobre el TEXTO de un control cae en la
     viga de texto, que es peor que la manita. El sistema no tiene ni un solo
     `<a href>` con estilo propio, así que aquí no hay excepción que hacer: el
     día que haya un enlace a una URL de verdad —algo que sale del OS y abre el
     navegador— ese sí lleva manita, y la pide él.

   · `user-select: none` — el rótulo de un control no es contenido. Sin esto,
     un arrastre sobre una pestaña la selecciona en azul-ácido, ⌘A selecciona
     la interfaz entera, y el cursor delata la viga.

   **La frontera está en el CONTROL, no en la pantalla**, y es a propósito: el
   contenido —una nota, una respuesta del agente, una celda de tabla, una cifra
   que alguien quiera copiar— se sigue seleccionando como siempre. Lo que se
   apaga es solo lo que se pica.

   Va con `:where()` (especificidad 0) para que cualquier componente o página
   que necesite lo contrario gane sin pelear. */
:where(
  .r-btn, .r-icon-btn, .r-seg__i, .r-check, .r-rail__i, .r-tabs__i,
  .r-wheel__i, .r-navlist__i, .r-pager__a, .r-arow__add, .r-toolbar__a,
  .r-selectbox, .r-slider__in, .r-confirm__no, .r-pick__t, .r-pick__o,
  .r-capture__ok, .r-orb
) {
  cursor: default;
  user-select: none;
  -webkit-user-select: none;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  /* Las pastillas deslizantes SALTAN a su lugar, no se aceleran: un
     translate3d con rebote a 0.01ms alcanza a dejar un cuadro intermedio, y
     el estado activo se lee igual sin el desplazamiento — la pastilla ya
     está debajo del ítem. Dicho aquí y no por omisión. */
  .r-rail__mark, .r-seg__mark, .r-tabs__mark, .r-orbs__mark { transition: none !important; }
}

/* ============================================================
   1 · LAYOUT
   ============================================================ */

.r-page {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--s-8) var(--s-6) var(--s-24);
}
@media (max-width: 640px) {
  .r-page { padding: var(--s-6) var(--s-4) var(--s-16); }
}

/* Bento: la retícula base del OS. Hijos usan --span. */
.r-bento {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--s-3);
}
.r-bento > * { grid-column: span var(--span, 12); }
@media (max-width: 900px) {
  .r-bento > * { grid-column: span min(var(--span, 12), 6); }
}
@media (max-width: 560px) {
  .r-bento > * { grid-column: span 12; }
}

.r-stack   { display: flex; flex-direction: column; gap: var(--gap, var(--s-3)); }
.r-row     { display: flex; align-items: center; gap: var(--gap, var(--s-3)); }
.r-row--between { justify-content: space-between; }
.r-row--wrap    { flex-wrap: wrap; }
.r-spacer  { flex: 1 1 auto; }

.r-section { margin-block: var(--s-16) var(--s-6); }
.r-measure { max-width: var(--measure); }

/* ============================================================
   2 · SUPERFICIES
   ============================================================ */

/* La tarjeta es papel TRANSLÚCIDO MATE, y eso es el default del sistema: no
   se separa del fondo con una línea, recoge el color que hay detrás. Sin color
   detrás no es vidrio, es un rectángulo gris — por eso el wallpaper también
   pinta por omisión (§ 27).

   Por qué NO entra en la cascada de § 23, aunque sea vidrio: --surface-frost
   sigue al tema (blanco translúcido en claro, casi negro en cockpit), así que
   la tinta del tema ya cae del lado correcto y no hay nada que remapear. Es
   la misma razón por la que .r-mesh se queda fuera. Distinto de .r-frost, que
   es oscuro fijo porque va sobre color SATURADO y ahí la tinta sí se voltea.

   El hairline se queda: sigue siendo hairline antes que sombra, y sobre un
   fondo con manchas es lo que le da un canto al papel.

   La excepción es .r-card--solid, y es de verdad: ver abajo. */
.r-card {
  background: var(--surface-frost);
  backdrop-filter: blur(var(--frost-blur)) saturate(120%);
  -webkit-backdrop-filter: blur(var(--frost-blur)) saturate(120%);
  border: var(--stroke) solid var(--hairline);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  position: relative;
  overflow: hidden;
  transition: border-color var(--d-base) var(--ease),
              transform var(--d-base) var(--ease);
}
/* Papel OPACO. El escape para lo que no puede negociar legibilidad:
   un bloque de lectura largo, un formulario, una tabla densa, algo que se va
   a imprimir o a capturar. También es lo que hay que usar encima de .r-mesh o
   de una foto, donde el fondo no tiene velo que lo calme. */
.r-card--solid {
  background: var(--surface);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.r-card--flush  { padding: 0; }
.r-card--sunk   { background: var(--surface-sunk); border-color: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; }
.r-card--invert { background: var(--surface-invert); color: var(--ink-invert); border-color: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; }
.r-card--invert .r-label,
.r-card--invert .r-micro { color: rgba(255,255,255,0.55); }
/* La única superficie que se voltea con el tema —oscura en claro, clara en
   cockpit—, así que su acento va al revés que el del resto: ácido cuando
   la tarjeta es oscura, tinta normal cuando es clara. Medido: sin esto, en
   cockpit un .r-t-accent aquí daba 1.31:1 (ácido sobre papel blanco). */
.r-card--invert {
  --accent-ink:  var(--accent-ink-invert);
  --accent-mark: var(--accent-mark-invert);
}
.r-card--xl     { border-radius: var(--r-2xl); padding: var(--s-8); }
.r-card--tall   { min-height: 260px; }
.r-card--lift:hover { transform: translateY(-2px); border-color: var(--hairline-strong); }

/* Panel de datos oscuro — el look "cockpit" sobre mapa/foto */
.r-panel {
  background: rgba(14, 14, 16, 0.82);
  color: #f6f6f4;
  border: var(--stroke) solid rgba(255, 255, 255, 0.10);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  /* Oscuro en los dos temas (por eso también parcha sus labels abajo): el
     ácido aquí SÍ es tinta —9.2:1 en claro, 14.9:1 en cockpit— y la marca
     no hace falta. */
  --accent-ink:  var(--accent-press);
  --accent-mark: none;
  /* Misma razón que en .r-frost: la tinta de aquí es clara y fija. */
  --surface-frost: var(--glass-raise);
}
.r-panel .r-label, .r-panel .r-micro { color: rgba(255, 255, 255, 0.52); }

/* Glass claro — para overlays sobre imagen */
.r-glass {
  background: var(--glass-bg);
  border: var(--stroke) solid var(--glass-border);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: var(--e-2);
}

/* Mesh: fondo de gradiente difuso. Nunca detrás de texto largo. */
.r-mesh {
  position: relative;
  isolation: isolate;
  background: var(--canvas-deep);
  overflow: hidden;
}
.r-mesh::before {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: -1;
  background:
    radial-gradient(38% 42% at 22% 28%, var(--acid-300) 0%, transparent 62%),
    radial-gradient(34% 38% at 76% 26%, var(--blue-300) 0%, transparent 60%),
    radial-gradient(46% 46% at 62% 82%, var(--coral-300) 0%, transparent 62%),
    radial-gradient(30% 34% at 12% 84%, var(--violet-500) 0%, transparent 60%);
  filter: blur(56px) saturate(130%);
  opacity: 0.85;
}

.r-divider {
  height: var(--stroke);
  background: var(--hairline);
  border: 0;
  margin-block: var(--s-5);
}
/* Vertical, para separar grupos DENTRO de una barra de controles. Existe
   porque no existía: los tres consumidores del OS escribían las mismas cuatro
   declaraciones a mano —`width: var(--stroke); height: 18px; margin: 0;
   flex: 0 0 auto`— cada uno con su propia clase (.os-bar__div,
   .rel-bar__div, .wk-tools__div). Tres copias del mismo hairline es el síntoma
   de una variante que le faltaba al sistema, no de tres necesidades distintas.

   El alto es el de una barra de controles, no el del contenido: un divisor que
   creciera con la fila competiría con el hairline de la tarjeta. --tall para
   una barra alta. */
.r-divider--v {
  width: var(--stroke);
  height: var(--div-v-h);
  margin: 0;
  flex: 0 0 auto;
  align-self: center;
}
.r-divider--v.r-divider--tall { height: var(--div-v-h-tall); }

/* ============================================================
   3 · TIPOGRAFÍA
   ============================================================ */

.r-display {
  font-family: var(--font-display);
  font-size: var(--t-display);
  font-weight: var(--fw-light);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
  margin: 0;
  text-wrap: balance;
}
.r-h1 {
  font-family: var(--font-display);
  font-size: var(--t-h1);
  font-weight: var(--fw-light);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-display);
  margin: 0;
  text-wrap: balance;
}
.r-h2 {
  font-size: var(--t-h2);
  font-weight: var(--fw-book);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-tight);
  margin: 0;
}
.r-h3 {
  font-size: var(--t-h3);
  font-weight: var(--fw-medium);
  line-height: 1.3;
  letter-spacing: var(--tr-tight);
  margin: 0;
}
.r-body   { font-size: var(--t-body); color: var(--ink-2); margin: 0; }
.r-body--lead { font-size: 1.0625rem; color: var(--ink-2); line-height: 1.45; }
.r-body--sm { font-size: var(--t-body-sm); color: var(--ink-2); margin: 0; }

.r-label {
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  color: var(--ink-3);
  letter-spacing: var(--tr-normal);
  margin: 0;
}
.r-micro {
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--tr-micro);
  color: var(--ink-3);
  margin: 0;
}

/* Label con slash — firma de sección: "/ Finanzas" */
/* Rótulo de sección. SIN diagonal: Robbie la quitó de todas partes, no solo
   de la barra superior donde repetía el nombre de la vista.

   Se quita aquí, en el pseudo-elemento, y con eso desaparece en los seis
   consumidores de golpe — ninguno la escribe a mano en su marcado (verificado).

   DEUDA: el nombre ya no describe lo que hace, y sin la diagonal esta regla es
   idéntica declaración por declaración a .r-label —la diagonal era lo ÚNICO
   que las distinguía—. Renombrar toca una decena de archivos con dueños
   distintos, así que la clase se queda funcionando y .r-eyebrow queda como el
   nombre para lo que se escriba de aquí en adelante. Ver README. */
.r-slash,
.r-eyebrow {
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  color: var(--ink-3);
  letter-spacing: var(--tr-normal);
  margin: 0;
}

/* Marcador ácido sobre texto */
.r-mark {
  background: var(--accent);
  color: var(--on-accent);
  padding: 0.05em 0.28em;
  border-radius: var(--r-xs);
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

.r-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.r-dim  { color: var(--ink-3); }

/* ============================================================
   4 · MÉTRICAS  — número grande + unidad diminuta
   ============================================================ */

.r-metric {
  /* La unidad mide 0.3em del NÚMERO. Se pasa por variable —y no como
     `font-size: 0.3em` en la unidad misma— porque .r-metric__unit también
     se usa donde no hay número del que escalar (dentro de un .r-lcdrow el
     numeral es un SVG y el contenedor es texto de cuerpo: 0.3em la dejaba
     en 4.5px). Ahí cae al fallback --t-unit. Misma especificidad que antes,
     así que ninguna métrica existente cambia de tamaño. */
  --unit-fs: 0.3em;
  display: flex;
  align-items: baseline;
  gap: 0.18em;
  font-family: var(--font-display);
  font-size: var(--t-metric);
  font-weight: var(--fw-light);
  line-height: 1;
  letter-spacing: var(--tr-display);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.r-metric--sm { font-size: var(--t-metric-sm); }
.r-metric--lg { font-size: clamp(3rem, 7vw, 5.5rem); }

.r-metric__unit {
  font-size: var(--unit-fs, var(--t-unit));
  font-weight: var(--fw-book);
  letter-spacing: var(--tr-normal);
  color: var(--ink-3);
  transform: translateY(-0.15em);
}
/* Fracción atenuada: 142,<span class="r-metric__fade">580</span> */
.r-metric__fade { color: var(--ink-3); }

/* ---- Marca de acento ----
   La barra ácida chica que carga el acento cuando el número NO puede ser
   ácido, o sea sobre superficie clara: ahí el dato va en tinta normal (ver
   --accent-ink en tokens.css) y el color de la marca se mueve al canto.

   Se enciende y se apaga sola con --accent-mark, que viaja pegado a
   --accent-ink: donde el número YA es ácido —cockpit, gradiente, vidrio,
   panel— la marca se va a `none`, porque dos acentos en la misma vista es
   justo lo que el sistema prohíbe. Todo por cascada del tema: cero JS.

   `display: none` y no opacidad ni ancho 0: así no deja hueco y el número
   arranca en el mismo sitio en los dos temas.

   Barra y no punto cuadrado a propósito: el punto cuadrado ya significa
   ESTADO (.r-status) y junto a una métrica se leería como un semáforo. */
.r-metric--mark::before,
.r-lcdrow--mark::before {
  content: "";
  display: var(--accent-mark, none);
  flex: 0 0 auto;
  align-self: center;
  width:  var(--amark-w);
  height: var(--amark-h);
  background: var(--accent);
}
/* Junto a un número de tipo la marca escala con él. Junto a un numeral
   dot-matrix no: ese es un SVG y no hay em del que escalar, así que se
   queda con los px del token (misma razón que --t-unit). */
.r-metric--mark::before { width: 0.11em; height: 0.46em; }

.r-delta {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-normal);
}
.r-delta--up   { color: var(--ok); }
.r-delta--down { color: var(--crit); }
.r-delta--flat { color: var(--ink-3); }
.r-delta--up::before   { content: "↗"; }
.r-delta--down::before { content: "↘"; }

/* Bloque completo: label / métrica / spark */
.r-stat { display: flex; flex-direction: column; gap: var(--s-3); }
.r-stat__head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
.r-stat__foot { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s-3); }

/* ============================================================
   5 · TICKS / REGLAS  — la textura firma
   ============================================================ */

.r-ticks {
  --tick-gap: 6px;
  --tick-h: 10px;
  height: var(--tick-h);
  width: 100%;
  background-image: repeating-linear-gradient(
    to right,
    var(--trace) 0, var(--trace) var(--stroke),
    transparent var(--stroke), transparent var(--tick-gap)
  );
  background-repeat: repeat-x;
  opacity: 0.7;
}
.r-ticks--dense { --tick-gap: 3px; --tick-h: 8px; }
.r-ticks--tall  { --tick-h: 18px; }

/* Regla con eje y marca de posición */
.r-ruler { position: relative; padding-block: var(--s-2); }
.r-ruler__marker {
  position: absolute;
  top: 0; bottom: 0;
  left: var(--at, 50%);
  width: var(--stroke-2);
  background: var(--accent);
}
.r-ruler__marker::after {
  content: "";
  position: absolute;
  top: -3px; left: 50%;
  translate: -50% 0;
  width: 6px; height: 6px;
  border-radius: var(--r-pill);
  background: var(--accent);
}

/* Track: progreso sólido + resto punteado (timeline de entrega) */
.r-track {
  --at: 40%;
  position: relative;
  height: 14px;
  display: flex;
  align-items: center;
}
.r-track::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  height: var(--stroke);
  background-image: repeating-linear-gradient(
    to right, var(--trace) 0, var(--trace) 2px, transparent 2px, transparent 5px
  );
}
.r-track::after {
  content: "";
  position: absolute;
  left: 0;
  width: var(--at);
  height: var(--stroke);
  background: var(--ink);
}
.r-track__pin {
  position: absolute;
  left: var(--at);
  translate: -50% 0;
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--ink);
  top: 1px;
}
.r-track__ends {
  display: flex;
  justify-content: space-between;
  font-size: var(--t-micro);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  margin-top: var(--s-1);
}

/* ============================================================
   6 · SPARKS Y BARRAS  (r-spark lo pinta ron.js)
   ============================================================ */

.r-spark {
  display: block;
  width: 100%;
  height: var(--spark-h, 34px);
  overflow: visible;
}
.r-spark__line { fill: none; stroke: var(--ink); stroke-width: var(--stroke-2); stroke-linejoin: round; stroke-linecap: round; }
.r-spark__area { fill: var(--ink); opacity: 0.06; }
.r-spark__dot  { fill: var(--accent); stroke: var(--surface); stroke-width: 2; }
.r-spark--accent .r-spark__line { stroke: var(--accent-press); }
.r-spark--crit   .r-spark__line { stroke: var(--crit); }
.r-spark--inline { height: 18px; width: 64px; display: inline-block; vertical-align: middle; }

/* Barras micro, sin ejes */
.r-bars { display: flex; align-items: flex-end; gap: 2px; height: var(--bars-h, 36px); }
.r-bars__b {
  flex: 1 1 0;
  min-width: 2px;
  height: var(--v, 50%);
  background: var(--trace);
  border-radius: 1px;
}
.r-bars__b[data-peak] { background: var(--accent-press); }

/* Matriz de puntos — densidad tipo "15 transactions" */
.r-dots { display: grid; grid-template-columns: repeat(var(--cols, 8), 1fr); gap: 6px; }
.r-dots span {
  aspect-ratio: 1;
  border-radius: var(--r-pill);
  border: var(--stroke) solid var(--trace);
}
.r-dots span[data-on] { background: var(--ink); border-color: var(--ink); }

/* ============================================================
   7 · CONTROLES
   ============================================================ */

.r-btn {
  --btn-bg: var(--surface);
  --btn-fg: var(--ink);
  --btn-bd: var(--hairline-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  height: 38px;
  padding-inline: var(--s-4);
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: var(--stroke) solid var(--btn-bd);
  border-radius: var(--r-pill);
  font-size: var(--t-body-sm);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-normal);
  white-space: nowrap;
  flex-shrink: 0;
  transition: background var(--d-fast) var(--ease),
              border-color var(--d-fast) var(--ease),
              transform var(--d-fast) var(--ease);
}
.r-btn:hover  { background: var(--surface-2); }
.r-btn:active { transform: scale(0.98); }
.r-btn--accent { --btn-bg: var(--accent); --btn-fg: var(--on-accent); --btn-bd: transparent; }
.r-btn--accent:hover { background: var(--accent-press); }
.r-btn--solid  { --btn-bg: var(--surface-invert); --btn-fg: var(--ink-invert); --btn-bd: transparent; }
.r-btn--ghost  { --btn-bg: transparent; --btn-bd: transparent; }
.r-btn--ghost:hover { background: var(--idle-bg); }
.r-btn--sm { height: 30px; padding-inline: var(--s-3); font-size: var(--t-label); }
.r-btn[disabled] { opacity: 0.4; pointer-events: none; }

/* Botón circular de trazo delgado */
.r-icon-btn {
  display: inline-grid;
  place-items: center;
  width: 36px; height: 36px;
  flex-shrink: 0;
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--ink);
  border: var(--stroke) solid var(--hairline-strong);
  transition: background var(--d-fast) var(--ease), transform var(--d-fast) var(--ease);
}
.r-icon-btn:hover  { background: var(--surface-2); }
.r-icon-btn:active { transform: scale(0.94); }
.r-icon-btn--lg { width: 46px; height: 46px; }
/* Talla de invocador: el botón redondo que vive suelto sobre una superficie,
   no dentro de una barra. Entró al irse .r-liquid-btn, que era el único que
   daba esta escala y venía de la familia brillante; el dibujo es el mismo
   círculo, en mate. */
.r-icon-btn--xl { width: 56px; height: 56px; --ico: 22px; }
/* Talla de fila: una acción que vive DENTRO de un renglón de tabla o de
   lista, donde 36px empuja el alto de la fila. El icono baja con ella para
   que el trazo no se coma la caja. */
.r-icon-btn--sm { width: var(--ctl-sm); height: var(--ctl-sm); --ico: 15px; }
/* Sin aro: la acción secundaria que se revela al hover de su fila. Un anillo
   permanente por renglón es justo el clutter que el sistema no quiere; el
   borde se gana al pasar encima. */
.r-icon-btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--ink-3);
}
.r-icon-btn--ghost:hover { background: var(--idle-bg); color: var(--ink); }
.r-icon-btn--accent { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.r-icon-btn--crit   { background: var(--crit); color: var(--on-crit); border-color: transparent; }

/* Segmented control — activo en negro sólido */
.r-seg {
  position: relative;   /* el marcador se mide desde aquí */
  display: inline-flex;
  flex-shrink: 0;
  padding: 3px;
  gap: 2px;
  background: var(--surface-sunk);
  border: var(--stroke) solid var(--hairline);
  border-radius: var(--r-pill);
}

/* Pastilla deslizante — misma mecánica que la del rail, la inyecta y la mide
   ron.js. Dos decisiones que no se ven:

   · El fondo NO es blanco fijo, es --surface-invert. La pastilla se VOLTEA
     con el tema —negra sobre papel claro, blanca en cockpit— y sobre una
     superficie de color la cascada ya la remapea a --on-grad con
     --ink-invert oscuro. Con --fixed-ink se rompería justo el tema claro,
     donde la pastilla es la oscura.
   · --d-base y no --d-slow: el rail es un dock vertical de 6 posiciones y
     380ms se leen bien ahí; un segmentado de dos o tres opciones con la
     misma duración se siente pesado. El rebote sí es el mismo. */
.r-seg__mark {
  position: absolute;
  top: 0;
  left: 0;
  height: 30px;
  border-radius: var(--r-pill);
  background: var(--surface-invert);
  pointer-events: none;
  z-index: 0;
}
.r-seg[data-mark-ready] .r-seg__mark {
  transition: transform var(--d-base) var(--ease-spring),
              width var(--d-base) var(--ease),
              height var(--d-base) var(--ease);
}
/* Con pastilla, el activo ya no pinta su propio fondo: sería doble. El
   :has() es también el respaldo — sin ron.js no hay marcador, no entra esta
   regla y el activo sigue leyéndose con su fondo de siempre. */
.r-seg:has(.r-seg__mark) .r-seg__i { position: relative; z-index: 1; }
.r-seg:has(.r-seg__mark) .r-seg__i[aria-selected="true"],
.r-seg:has(.r-seg__mark) .r-seg__i[aria-current="page"] {
  background: transparent;
  color: var(--ink-invert);
}
.r-seg__i {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-3);
  height: 30px;
  padding-inline: var(--s-4);
  border-radius: var(--r-pill);
  font-size: var(--t-body-sm);
  font-weight: var(--fw-medium);
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
.r-seg__i:hover { color: var(--ink); }
.r-seg__i[aria-selected="true"] {
  background: var(--surface-invert);
  color: var(--ink-invert);
}

/* Chip / pastilla */
.r-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  height: 26px;
  padding-inline: var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface-sunk);
  color: var(--ink-2);
  border: var(--stroke) solid transparent;
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}
.r-chip--accent { background: var(--accent); color: var(--on-accent); }
.r-chip--outline { background: transparent; border-color: var(--hairline-strong); color: var(--ink); }
.r-chip--info { background: var(--info-bg); color: var(--info); }
.r-chip--ok   { background: var(--ok-bg);   color: var(--ok); }
.r-chip--warn { background: var(--warn-bg); color: var(--warn); }
.r-chip--crit { background: var(--crit-bg); color: var(--crit); }

/* Contador de índice — "4/13" */
.r-index {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding-inline: var(--s-3);
  border-radius: var(--r-pill);
  background: rgba(10, 10, 12, 0.55);
  color: #fff;
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.r-input {
  height: 38px;
  width: 100%;
  padding-inline: var(--s-4);
  background: var(--surface-sunk);
  border: var(--stroke) solid transparent;
  border-radius: var(--r-pill);
  font-size: var(--t-body-sm);
  color: var(--ink);
  transition: border-color var(--d-fast) var(--ease), background var(--d-fast) var(--ease);
}
.r-input::placeholder { color: var(--ink-3); }
.r-input:focus { background: var(--surface); border-color: var(--hairline-strong); outline: none; }
.r-input--area { height: auto; min-height: 96px; padding: var(--s-3) var(--s-4); border-radius: var(--r-md); resize: vertical; }
/* Campo compacto, para una fila de captura en línea —la de una tarjeta, no la
   de un formulario—. Existe porque dos consumidores del OS ya lo pedían
   sobrescribiendo la regla del sistema (`.os-quick .r-input`, `.fin-add
   .r-input`, los dos con `height: 34px`): cuando la app tiene que apagar una
   declaración del design system, lo que falta es la variante.

   OJO: el campo del panel de .r-capture mide 32px y NO es esta variante. Ahí el
   alto lo manda su botón de confirmar, que es cuadrado de 32: son dos piezas
   emparejadas dentro de un panel, no un campo suelto en una fila. */
.r-input--sm { height: 34px; }

/* Checkbox cuadrado de trazo fino */
.r-check {
  appearance: none;
  width: 16px; height: 16px;
  flex: 0 0 auto;
  border: var(--stroke-2) solid var(--trace);
  border-radius: var(--r-xs);
  background: transparent;
  display: inline-grid;
  place-items: center;
  transition: background var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease);
}
.r-check:checked { background: var(--ink); border-color: var(--ink); }
.r-check:checked::after {
  content: "";
  width: 8px; height: 4px;
  border-left: var(--stroke-2) solid var(--ink-invert);
  border-bottom: var(--stroke-2) solid var(--ink-invert);
  rotate: -45deg;
  translate: 0 -1px;
}

/* ============================================================
   8 · ESTADO
   ============================================================ */

/* Punto de estado CUADRADO — no redondo. Detalle del estudio. */
.r-status {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  color: var(--ink-2);
}
.r-status::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 1px;
  background: var(--idle);
}
.r-status--ok::before   { background: var(--ok); }
.r-status--warn::before { background: var(--warn); }
.r-status--crit::before { background: var(--crit); }
.r-status--info::before { background: var(--info); }
.r-status--live::before { background: var(--ok); animation: r-pulse var(--d-pulse) var(--ease) infinite; }
/* «TRABAJO EN VUELO» Y «ESO SOY YO», y las dos suben porque ya hay dos
   superficies pidiéndolas: `app/value.css` (actividad por sistema) las estaba
   consiguiendo por la única costura que tenía sin editar este archivo —el
   `::before` de su propia clase—, y el contador de sesiones del chasis necesita
   la misma señal. Es el criterio del sistema: sube lo que más de una superficie
   necesita.

   `--info` y no `--ok` para lo que está corriendo: con `--ok` una fila normal
   tendría DOS cuadros verdes a 300 px uno del otro, uno de ellos latiendo, y no
   se distinguen de un golpe. Es el mismo azul con el que una hoja dice «PR
   abierto», o sea trabajo en vuelo.

   El acento NO late: el ácido ya es la señal más fuerte de la vista y ponerle
   ritmo encima lo vuelve una alarma. Late lo ajeno, que es lo que hay que
   mirar; lo propio sólo se marca. */
.r-status--busy::before   { background: var(--info); animation: r-pulse var(--d-pulse) var(--ease) infinite; }
.r-status--accent::before { background: var(--accent); animation: none; }

@keyframes r-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

/* Bloque de alerta (panel oscuro tipo cockpit) */
.r-alert {
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4);
  background: var(--warn-bg);
  border: var(--stroke) solid transparent;
  display: flex;
  gap: var(--s-3);
}
.r-alert--crit { background: var(--crit-bg); }
.r-alert--info { background: var(--info-bg); }
.r-alert__n {
  flex: 0 0 auto;
  width: 18px; height: 18px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--crit);
  color: #fff;
  font-size: 0.625rem;
  font-weight: var(--fw-bold);
}

/* ============================================================
   9 · LISTAS Y TABLAS
   ============================================================ */

.r-kv { display: grid; gap: var(--s-2); }
.r-kv__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  padding-block: var(--s-2);
  border-bottom: var(--stroke) solid var(--hairline);
}
.r-kv__row:last-child { border-bottom: 0; }
.r-kv__k { font-size: var(--t-body-sm); color: var(--ink-3); }
.r-kv__v { font-size: var(--t-body-sm); font-weight: var(--fw-medium); font-variant-numeric: tabular-nums; }

.r-table-wrap { overflow-x: auto; }
.r-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-body-sm);
  font-variant-numeric: tabular-nums;
}
.r-table th {
  text-align: left;
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--tr-micro);
  color: var(--ink-3);
  padding: var(--s-2) var(--s-3);
  border-bottom: var(--stroke) solid var(--hairline-strong);
  white-space: nowrap;
}
.r-table td {
  padding: var(--s-3);
  border-bottom: var(--stroke) solid var(--hairline);
  color: var(--ink-2);
}
.r-table td:first-child { color: var(--ink); font-weight: var(--fw-medium); }
.r-table tbody tr:last-child td { border-bottom: 0; }
.r-table--num td:not(:first-child) { text-align: right; }

/* Fila de registro tipo "orden" */
.r-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: var(--stroke) solid var(--hairline);
}
.r-item:last-child { border-bottom: 0; }
.r-item__id { font-family: var(--font-mono); font-size: var(--t-body-sm); }

/* ============================================================
   10 · DOT-MATRIX  (numerales LED — los pinta ron.js)
   <p class="r-lcd" data-lcd="$25,678.00" data-ghost></p>

   SE OYE, y no hay que acordarse de nada. La cifra que este componente
   dibuja es la que el sistema declara «la que manda en la vista», así que
   no puede llegar solo como forma: `ron.js` le pone su nombre accesible
   derivándolo de `data-lcd` —una cifra ya es texto— más la unidad de al
   lado cuando esa unidad está fuera del árbol. Si el consumidor ya dijo
   algo (etiqueta propia, o un `.r-lcdrow` con `role="img"` que la nombre,
   que es lo que hacen los ocho módulos), manda el consumidor y el numeral
   se calla: repetir la cifra es peor que no decirla. El contrato completo
   y el porqué de que NO lleve `aria-live` están junto a `lcdSpeak` en
   `ron.js`.

   Los `<circle>` de la matriz son TRAZO, no contenido. `role="img"` en el
   `<svg>` lo vuelve una hoja del árbol, así que los ochenta puntos del
   balance de Finanzas no se pueden recorrer.
   ============================================================ */

.r-lcd { display: block; overflow: visible; }
.r-lcd__on    { fill: var(--lcd-on); }
.r-lcd__off   { fill: var(--lcd-ghost); }
/* Mismo criterio que .r-t-accent: el punteado ácido sobre papel blanco da
   1.31:1. Sobre un .r-grad o un panel —oscuros en los DOS temas—
   --accent-ink sigue siendo ácido, así que ahí no se degrada. */
.r-lcd--accent .r-lcd__on { fill: var(--accent-ink); }
.r-lcd--grad  .r-lcd__on  { fill: var(--on-grad); }
.r-lcd--grad  .r-lcd__off { fill: rgba(255, 255, 255, 0.20); }
/* Cola atenuada: los centavos en gris. Se marca con data-fade="3". */
.r-lcd__on[data-tail] { fill: var(--ink-3); }
/* La cola atenuada es parte de una CIFRA, así que sube al nivel de texto
   secundario igual que .r-metric__fade: --on-grad-3 (blanco al 40%) se compone
   contra el propio gradiente y en su tramo claro medía 1.4–1.9:1. */
.r-lcd--grad .r-lcd__on[data-tail] { fill: var(--on-grad-2); }

/* ---- Tamaños: una escala, dos puertas ----
   El punto crece, no el SVG: así nunca se pixela.

   La MISMA regla se declara para el numeral y para el renglón, y esa es
   toda la gracia: el tamaño es un valor, no dos, y quien lo pide puede
   ser cualquiera de los dos elementos.

   · En el RENGLÓN (`.r-lcdrow--xl`) es el camino bueno y el que se
     escribe de aquí en adelante: --lcd-dot y --lcd-gap se declaran
     arriba y bajan por herencia al numeral, así que el renglón puede
     medir su propia separación contra la matriz sin tener que
     adivinar qué trae adentro.
   · En el NUMERAL (`.r-lcd--xl`) sigue vivo, y tiene que seguirlo: es
     lo único que existe cuando el numeral va solo, sin unidad al lado
     y sin renglón que lo envuelva. */
.r-lcd--sm,  .r-lcdrow--sm  { --lcd-dot: 3px;  --lcd-gap: var(--lcd-gap-sm); }
.r-lcd--lg,  .r-lcdrow--lg  { --lcd-dot: 6px;  --lcd-gap: var(--lcd-gap-lg); }
.r-lcd--xl,  .r-lcdrow--xl  { --lcd-dot: 9px;  --lcd-gap: var(--lcd-gap-xl); }

/* ---- Y la marca ácida crece con el numeral ----
   Mismo mecanismo, misma razón, y hace falta por una que es solo de aquí:
   en tema CLARO el numeral va en tinta y la marca es lo único que carga el
   acento de la vista. Con --amark-h fija en 22px, la métrica principal de un
   renglón --xl (90px de alto) quedaba acentuada por una barra de 5×22 —110px²
   contra el numeral entero en ácido en cockpit—, así que los dos temas no
   tenían la misma presencia. Los cuatro valores viven en tokens.css con su
   proporción; aquí solo se elige cuál.

   Va en el RENGLÓN y no en el numeral porque el ::before es del renglón: un
   --amark-* declarado en .r-lcd--xl no sube. Ver la nota de --lcd-gap de más
   abajo para la puerta del tamaño declarado en el hijo. */
.r-lcdrow--sm { --amark-w: var(--amark-w-sm); --amark-h: var(--amark-h-sm); }
.r-lcdrow--lg { --amark-w: var(--amark-w-lg); --amark-h: var(--amark-h-lg); }
.r-lcdrow--xl { --amark-w: var(--amark-w-xl); --amark-h: var(--amark-h-xl); }

/* Dot-matrix + unidad en tipo normal: "$6.3 k".
   Aquí la unidad NO escala con el numeral (es un SVG, no tipo), así que
   toma su tamaño absoluto de --t-unit; ver .r-metric__unit.

   Y por lo mismo la separación tampoco puede ir en `em`: es el TERCER
   caso del mismo tropiezo (los otros dos están documentados en --t-unit
   y en la marca ácida junto a un dot-matrix). Un `gap: 0.4em` se
   resolvía contra el renglón —texto de cuerpo, 15px ⇒ 6px fijos—, así
   que no se movía con el tamaño de la matriz: 3× la separación interna
   en el default y 1.33× en --xl, donde la unidad se leía como otra
   columna de puntos pegada al número (se veía en el Inbox de Home). Y
   al revés: donde el consumidor le subía el font-size al renglón para
   agrandar la unidad, el mismo 0.4em se disparaba a 14–22px.

   La separación se mide entonces contra la MATRIZ, y con el tamaño
   declarado en el renglón sale de un solo lugar: --lcd-gap ya vale lo
   que vale la matriz de adentro. Medido: 6 / 4.5 / 9 / 13.5 px para
   base / --sm / --lg / --xl. */
.r-lcdrow { display: flex; align-items: flex-end; gap: calc(var(--lcd-gap) * var(--lcd-air)); }

/* Y esto es lo que sostiene el camino viejo, nada más: con el tamaño
   declarado en el HIJO el renglón no puede leer ese --lcd-gap —la
   herencia va hacia abajo—, así que tiene que volver a nombrarlo por
   tamaño. Son tres reglas que no hacen falta en el modelo nuevo; viven
   aquí para que un consumidor sin migrar no se rompa.

   Cuando los dos caminos coinciden dan el mismo número, y si alguien
   los mezcla mal (renglón --xl con numeral --lg) gana el :has(), que es
   lo correcto: la separación tiene que seguir a la matriz que de verdad
   se dibujó, y esa la decide el hijo. */
.r-lcdrow:has(.r-lcd--sm) { gap: calc(var(--lcd-gap-sm) * var(--lcd-air)); --amark-w: var(--amark-w-sm); --amark-h: var(--amark-h-sm); }
.r-lcdrow:has(.r-lcd--lg) { gap: calc(var(--lcd-gap-lg) * var(--lcd-air)); --amark-w: var(--amark-w-lg); --amark-h: var(--amark-h-lg); }
.r-lcdrow:has(.r-lcd--xl) { gap: calc(var(--lcd-gap-xl) * var(--lcd-air)); --amark-w: var(--amark-w-xl); --amark-h: var(--amark-h-xl); }
.r-lcdrow .r-metric__unit { transform: none; margin-bottom: 0.15em; }

/* ============================================================
   11 · GRÁFICA DE LÍNEA  (la pinta ron.js)
   Bandas nombradas + cuña ácida en el endpoint.
   ============================================================ */

/* El padding superior reserva el espacio del valor del endpoint, que
   se dibuja por encima de la línea y si no se recortaría. */
.r-chart { position: relative; width: 100%; padding-top: var(--s-10); }
.r-chart__svg { display: block; width: 100%; height: var(--chart-h); overflow: visible; }
.r-chart__grid {
  stroke: currentColor;
  stroke-opacity: var(--chart-grid-o);
  stroke-width: var(--stroke);
  stroke-dasharray: var(--dash-fine);
}
.r-chart__line { fill: none; stroke: var(--chart-line); stroke-width: var(--stroke-2); stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.r-chart__dot  { fill: var(--chart-dot); }
.r-chart__seed { fill: currentColor; fill-opacity: 0.45; }

/* Etiquetas de banda, a la derecha, muy atenuadas */
.r-chart__band {
  fill: currentColor;
  fill-opacity: var(--chart-band-o);
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  font-weight: var(--fw-book);
  text-anchor: end;
}
.r-chart__x {
  fill: currentColor;
  fill-opacity: 0.55;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  text-anchor: middle;
}

/* Valor del endpoint, sobrepuesto */
.r-chart__cursor {
  position: absolute;
  transform: translate(-50%, -115%);
  text-align: center;
  pointer-events: none;
  white-space: nowrap;
}
.r-chart__cursor .r-metric { justify-content: center; }

/* Tooltip flotante tipo pastilla */
.r-tooltip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  height: 26px;
  padding-inline: var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  box-shadow: var(--e-2);
  white-space: nowrap;
}
.r-tooltip--abs { position: absolute; transform: translate(-50%, -50%); }

/* ============================================================
   12 · REGLA GRADUADA  (ticks mayores y menores)
   ============================================================ */

.r-scale {
  position: relative;
  height: var(--scale-major-h);
  width: 100%;
  background-repeat: repeat-x;
  background-position: left bottom, left bottom;
  background-image:
    /* mayores */
    repeating-linear-gradient(to right,
      var(--scale-ink) 0, var(--scale-ink) var(--stroke),
      transparent var(--stroke),
      transparent calc(var(--scale-minor) * var(--scale-major-every))),
    /* menores */
    repeating-linear-gradient(to right,
      var(--scale-ink) 0, var(--scale-ink) var(--stroke),
      transparent var(--stroke), transparent var(--scale-minor));
  background-size:
    100% var(--scale-major-h),
    100% var(--scale-minor-h);
}
.r-scale__labels {
  display: flex;
  justify-content: space-between;
  margin-top: var(--s-1);
  font-size: var(--t-micro);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* Marca de posición sobre la regla */
.r-scale__at {
  position: absolute;
  bottom: 0;
  left: var(--at, 50%);
  width: var(--stroke-2);
  height: calc(var(--scale-major-h) + 4px);
  background: var(--accent-press);
}

/* ============================================================
   13 · NAVEGACIÓN
   ============================================================ */

/* Rail tipo dock. De pie por omisión, y puede acostarse a propósito.

   EL EJE ES UNA VARIABLE, y eso es el punto de este bloque: antes la fila
   solo existía dentro de `@media (max-width: 720px)`, así que "acostado" y
   "pantalla angosta" eran la misma cosa y un consumidor que quisiera la tira
   en un monitor de 27" no tenía cómo pedirla. Hoy el eje vive en
   --rail-dir y hay DOS disparadores para el mismo estado:

     .r-rail--row     lo eligió el consumidor
     @media …720px    la red por pantalla angosta (.r-rail--adapt)

   Todo lo que dependa de la orientación cuelga de --rail-dir —no del ancho
   de la ventana— y por eso el separador de .r-rail__i--action se resuelve
   con una consulta de estilo más abajo en vez de repetir el media query.

   CONTRATO CON LA APP: quién pone .r-rail--row es la página. El OS lleva
   `data-rail` en :root y os.css decide con él; ron.css NUNCA mira
   `data-rail` — el sistema no sabe de los atributos de sus consumidores. */
.r-rail {
  position: relative;
  display: inline-flex;
  --rail-dir: column;
  flex-direction: var(--rail-dir);
  /* stretch y no center: de pie, los ítems miden lo que mida el rail, así
     que al expandirse todos comparten el mismo ancho y las pastillas de
     hover quedan parejas en vez de una por nombre. Acostado no cambia nada:
     los ítems traen alto fijo, así que no hay nada que estirar. */
  align-items: stretch;
  /* Los tres de la fila viven aquí y no en .r-rail--row a propósito: de pie
     son inocuos —una columna sin alto que forzar nunca envuelve— y puestos
     abajo habría que repetirlos en los dos disparadores. */
  flex-wrap: wrap;
  justify-content: center;
  max-width: 100%;
  /* Un dock mide lo que mide su contenido: no se estira ni se encoge. Hace
     falta desde que `wrap` vive en la clase base — dentro de un flex
     horizontal el rail acostado se dejaba encoger hasta el ancho de un ítem y
     envolvía teniendo sitio de sobra (medido en el showcase: 88×48 pasaba a
     48×90). La envoltura tiene que venir de que no CABE, no de que lo
     apretaron. No estorba a la red por pantalla angosta: ahí el rail vive en
     un flex de columna, así que el eje principal del padre es el otro y lo
     que lo ajusta es `max-width`. */
  flex: none;
  /* La caja del icono. Es la unidad de este componente: el ítem, el
     marcador y el largo del separador salen de ella. */
  --rail-item: 38px;
  /* Techo del nombre cuando el rail se abre (ver .r-rail__name). */
  --rail-name: 15ch;
  /* El hueco es UN número, no dos iguales copiados: el aire entre ítems, el
     relleno de la cápsula y la línea de .r-rail__i--action —que vive centrada
     en ese hueco— tienen que moverse juntos. Cuando el dock se acuesta el
     hueco se achica, y con el token eso es una línea en vez de tres. */
  --rail-gap: var(--s-2);
  gap: var(--rail-gap);
  padding: var(--rail-gap);
  /* El radio se dice en función del ítem y no con --r-pill, y el número es el
     MISMO que --r-pill daba comprimido: --rail-item/2 + hueco + borde = 28 de
     pie (38/2+8+1) y 24 acostado (38/2+4+1), que es exactamente a lo que el
     navegador recortaba el pill en una cápsula de 56 y de 48.

     Hace falta porque el rail ya no tiene un ancho fijo: expandido mide ~163
     y ahí --r-pill se recorta a 81, o sea una cápsula en forma de lozenge
     cuyas curvas se comen las esquinas — medido, la pastilla del ítem activo
     asomaba 6px por fuera del canto arriba. Con el radio atado al ítem la
     cápsula conserva su canto sin importar cuánto se abra. */
  border-radius: calc(var(--rail-item) / 2 + var(--rail-gap) + var(--stroke));
  background: rgba(16, 16, 18, 0.72);
  border: var(--stroke) solid rgba(255, 255, 255, 0.10);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
}

/* El estado acostado. Los dos disparadores ponen lo MISMO —el eje y el
   hueco—; si algún día fueran tres declaraciones, sale una clase interna en
   vez de un tercer copiado.

   Se acuesta COMPLETO, no solo el eje. Medido con ocho ítems, que es lo
   que tiene el dock desde que el invocador se metió a la cápsula:
   8×38 + 7 huecos + 2 rellenos + el borde. Con el --s-2 de la vertical eso
   da 378px y no cabe en los 343 útiles de una pantalla de 375; con --s-1
   da 342 y entra por un pelo. El hueco puede achicarse porque no es
   táctil — el objetivo son los 38px del ítem, y esos no se tocan.

   La envoltura es red, no lujo: a 320 ni con --s-1 caben los ocho, y dos
   renglones le ganan a una barra que se lee con la rueda del ratón
   (CLAUDE.md: segmentar antes que scrollear). El marcador deslizante no se
   entera: ron.js lo coloca con offsetLeft Y offsetTop, así que el renglón
   de abajo le sale gratis. */
.r-rail--row {
  --rail-dir: row;
  --rail-gap: var(--s-1);
}
@media (max-width: 720px) {
  .r-rail--adapt {
    --rail-dir: row;
    --rail-gap: var(--s-1);
  }
}

/* En este dock hay algo que NO es navegación: el invocador. La línea vive
   centrada en el hueco que el rail ya tenía —no agrega espacio— y gira con
   el EJE: horizontal cuando el dock está de pie, vertical cuando se acuesta.

   Gira con el eje y no con el ancho de la ventana, que es lo que estaba mal:
   con `.r-rail--row` en una pantalla grande la línea se quedaba acostada
   sobre un dock ya acostado. La consulta de estilo lee --rail-dir heredado
   —el propio ítem es el contenedor— así que el separador no puede
   contradecir al eje. Donde no haya consultas de estilo (Chrome < 111,
   Safari < 18) la línea se queda horizontal: se pierde el giro, no el
   separador.

   Y el largo sale de --rail-item, no de un porcentaje del ítem: al
   expandirse el rail el ítem se hace ancho, y con `left/right: 20%` la
   línea crecía con él. El separador pertenece a la columna del icono.

   Va en --on-grad-line y no en --hairline a propósito: la cápsula del rail
   es oscura en los DOS temas, así que un hairline de tema saldría negro
   sobre negro en claro. Es el mismo criterio que ya usa .r-frost al
   remapear sus líneas. */
.r-rail__i--action { position: relative; }
.r-rail__i--action::before {
  content: "";
  position: absolute;
  background: var(--on-grad-line);
  pointer-events: none;
  left: calc(var(--rail-item) * 0.2);
  width: calc(var(--rail-item) * 0.6);
  top: calc(var(--rail-gap) / -2);
  height: var(--stroke);
}
@container style(--rail-dir: row) {
  .r-rail__i--action::before {
    left: calc(var(--rail-gap) / -2);
    width: var(--stroke);
    top: calc(var(--rail-item) * 0.2);
    height: calc(var(--rail-item) * 0.6);
  }
}

/* Pastilla que se desliza al ítem activo.
   La geometría la escribe ron.js directo en el style del nodo, no en
   variables: un cambio de custom property NO interpola, así que con
   var() la pastilla saltaba en vez de deslizarse.
   El rebote solo entra después del primer cuadro, o la pastilla "vuela"
   desde la esquina al cargar la página. */
.r-rail__mark {
  position: absolute;
  /* top/left en 0, no en el relleno del rail: ron.js usa offsetLeft/offsetTop
     crudos, que ya se miden desde esta misma caja de relleno. Antes el
     marcador repetía el padding del contenedor y ron.js lo restaba; se
     anulaban, y había que mantener el par en sincronía en cada consumidor. */
  top: 0;
  left: 0;
  width: var(--rail-item);
  height: var(--rail-item);
  border-radius: var(--r-pill);
  background: #fff;
  pointer-events: none;
  z-index: 0;
}
.r-rail[data-mark-ready] .r-rail__mark {
  transition: transform var(--d-slow) var(--ease-spring),
              width var(--d-base) var(--ease),
              height var(--d-base) var(--ease);
}
/* Con pastilla, el activo ya no pinta su propio fondo: sería doble. */
.r-rail:has(.r-rail__mark) .r-rail__i { position: relative; z-index: 1; }
.r-rail:has(.r-rail__mark) .r-rail__i[aria-current="page"] {
  background: transparent;
  color: var(--fixed-ink);
}
/* El ítem son DOS columnas: la caja del icono, que siempre mide --rail-item,
   y la del nombre, que empieza en cero. Que la primera sea fija es lo que
   hace que al expandirse el rail el icono NO se mueva ni un píxel: crece la
   segunda columna, no el dibujo. */
.r-rail__i {
  display: grid;
  grid-template-columns: var(--rail-item) auto;
  align-items: center;
  justify-items: center;
  min-width: var(--rail-item);
  height: var(--rail-item);
  border: 0;
  /* Explícito, y hace falta: el ítem ya no lleva `width` fijo —lo miden sus
     columnas— así que el relleno que el navegador le pone a un <button> por
     omisión dejó de quedar absorbido y le sumaba 12px de ancho. Medido: la
     cápsula pasaba de 56 a 68 sin que nada lo pidiera. */
  padding: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: rgba(255, 255, 255, 0.60);
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
.r-rail__i:hover { background: rgba(255, 255, 255, 0.10); color: #fff; }
/* Fondo claro fijo ⇒ tinta oscura fija. Con var(--ink) saldría blanco
   sobre blanco dentro de .r-aurora / .r-grad / .r-map. */
.r-rail__i[aria-current="page"] { background: #fff; color: var(--fixed-ink); }
.r-rail__i--accent { background: var(--info); color: #fff; }

/* ---- El nombre del módulo, y el rail que se abre al hover ----

   El dato NO se duplica: el nombre ya vive en el `aria-label` de cada ítem
   —es su etiqueta accesible y tiene que estar ahí de todos modos, porque un
   botón de solo icono sin nombre no existe— y ron.js lo copia a este <span>
   con `aria-hidden`. Se descartaron las dos alternativas obvias:
   pedirle al consumidor que escriba el nombre otra vez en el HTML (dos
   fuentes de verdad que se pueden contradecir, justo lo que .r-confirm y
   .r-run resuelven al revés), y `content: attr(aria-label)` en un
   pseudoelemento, que no cabe: en el rail del OS `::after` ya es el aura de
   .r-beam del invocador y `::before` es el separador de --action. El <span>
   inyectado es el mismo patrón que el marcador de .r-seg — el consumidor no
   escribe nada.

   La mecánica de la apertura es una sola declaración: el nombre tiene
   `max-width: 0`, y su contribución de tamaño al track `auto` está acotada
   por ese cero, así que de entrada el ítem mide exactamente --rail-item y el
   rail se ve como siempre. Al abrir, el techo pasa a --rail-name y todo lo
   demás lo hace el layout: el ítem crece, el rail —que es inline-flex y mide
   lo que su contenido— crece con él, y los demás ítems se estiran al nuevo
   ancho porque el rail alinea con `stretch`. No hay ancho total escrito a
   mano en ningún lado.

   --rail-name es un TECHO, no una medida: el rail termina midiendo el nombre
   más largo que tenga, y el techo solo evita que un nombre absurdo estire la
   cápsula media pantalla. Se dice en `ch` porque lo que tiene que caber es
   texto, no píxeles; lo que no cabe se recorta con elipsis y sigue completo
   en el `title` y en la etiqueta accesible — la misma salida que usan las
   etiquetas de .r-radar.

   `prefers-reduced-motion` no necesita nada aquí: la regla global de arriba
   deja las transiciones en 0.01ms, así que el rail sigue abriéndose, sin
   animar. Es lo correcto — quitarle la animación no es quitarle la función. */
.r-rail__name {
  justify-self: stretch;
  text-align: start;
  min-width: 0;
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--t-body-sm);
  font-weight: var(--fw-medium);
  opacity: 0;
  /* No captura el ratón: el clic es del botón, y así el nombre no se puede
     quedar a medio revelar por un hover que en realidad era suyo. */
  pointer-events: none;
  transition: max-width var(--d-base) var(--ease),
              opacity var(--d-fast) var(--ease);
}

/* Solo de pie. Acostado el rail ya usa todo el ancho de la pantalla —ocho
   ítems en 343px— así que abrirlo no es una opción geométrica, y por eso la
   condición es el EJE y no una media query: un rail acostado a propósito en
   un monitor grande tampoco tiene que abrirse.

   Dos disparadores, y son los que pidió el encargo:

   · `:hover` — se abre al pasar el ratón y se cierra al salir.
   · `:focus-visible` — y NO `:focus-within`. Es exactamente la diferencia
     que Robbie describió: al picar un ítem con el ratón el botón queda
     enfocado pero sin `:focus-visible`, así que el rail se comprime al
     quitar el ratón; llegando con Tab el navegador sí lo marca, y el nombre
     aparece. Sin esto el rail sería inservible con teclado: ocho iconos sin
     una palabra.

   En táctil no hay hover que esperar, así que el rail se queda ABIERTO. Un
   dock que solo dice los nombres al pasar el ratón, en una pantalla sin
   ratón, no dice los nombres nunca. */
@container style(--rail-dir: column) {
  .r-rail:hover .r-rail__name,
  .r-rail:has(.r-rail__i:focus-visible) .r-rail__name {
    max-width: var(--rail-name);
    opacity: 1;
  }
  @media (hover: none) {
    .r-rail__name { max-width: var(--rail-name); opacity: 1; }
  }
}

/* Tabs superiores: activo en pastilla sólida */
.r-tabs {
  position: relative;   /* el marcador se mide desde aquí */
  display: flex;
  align-items: center;
  gap: var(--s-2);
  overflow-x: auto;
  scrollbar-width: none;
}
.r-tabs::-webkit-scrollbar { display: none; }

/* Pastilla deslizante. Va absoluta DENTRO del contenedor que hace scroll, así
   que se desplaza con las pestañas y no se despega al arrastrar la barra.
   Lleva el mismo relieve que traía el activo (--surface + --e-1) para que el
   cambio sea solo el movimiento. */
.r-tabs__mark {
  position: absolute;
  top: 0;
  left: 0;
  height: 34px;
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: var(--e-1);
  pointer-events: none;
  z-index: 0;
}
.r-tabs[data-mark-ready] .r-tabs__mark {
  transition: transform var(--d-base) var(--ease-spring),
              width var(--d-base) var(--ease),
              height var(--d-base) var(--ease);
}
.r-tabs:has(.r-tabs__mark) .r-tabs__i { position: relative; z-index: 1; }
.r-tabs:has(.r-tabs__mark) .r-tabs__i[aria-current="page"],
.r-tabs:has(.r-tabs__mark) .r-tabs__i[aria-selected="true"] {
  background: transparent;
  box-shadow: none;
  color: var(--ink);
}
.r-tabs__i {
  border: 0;
  background: transparent;
  color: var(--ink-3);
  height: 34px;
  padding-inline: var(--s-4);
  border-radius: var(--r-pill);
  font-size: var(--t-body-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  flex-shrink: 0;
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
.r-tabs__i:hover { color: var(--ink); }
.r-tabs__i[aria-current="page"] { background: var(--surface); color: var(--ink); box-shadow: var(--e-1); }

/* Icono: caja cuadrada, hereda color y tamaño del contexto */
.r-ico {
  display: inline-grid;
  place-items: center;
  width: var(--ico, 20px);
  height: var(--ico, 20px);
  flex: 0 0 auto;
  color: inherit;
}
.r-ico > svg { width: 100%; height: 100%; display: block; }
.r-ico--sm { --ico: 16px; }
.r-ico--lg { --ico: 26px; }

/* ---- Wheel picker ----
   Rueda vertical con perspectiva. El transform de cada ítem lo
   calcula ron.js según su distancia al centro. */
.r-wheel {
  --wheel-item: 46px;
  position: relative;
  height: calc(var(--wheel-item) * 5);
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  transform-style: preserve-3d;
  /* Difuminado en los extremos: refuerza la sensación de rueda */
  -webkit-mask: linear-gradient(to bottom, transparent 0, #000 26%, #000 74%, transparent 100%);
          mask: linear-gradient(to bottom, transparent 0, #000 26%, #000 74%, transparent 100%);
  /* Relleno para que el primero y el último puedan centrarse */
  padding-block: calc(var(--wheel-item) * 2);
}
.r-wheel::-webkit-scrollbar { display: none; }

.r-wheel__i {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  height: var(--wheel-item);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3vw, 2.125rem);
  font-weight: var(--fw-light);
  letter-spacing: var(--tr-display);
  line-height: 1;
  text-align: left;
  scroll-snap-align: center;
  transform-origin: center center;
  backface-visibility: hidden;
  transition: color var(--d-base) var(--ease);
}
/* El activo lleva la flecha; el resto queda atenuado por opacidad */
.r-wheel__i::before {
  content: "→";
  font-size: 0.5em;
  opacity: 0;
  transition: opacity var(--d-base) var(--ease);
}
.r-wheel__i[aria-current="true"]::before { opacity: 1; }

/* Guía del centro, opcional */
.r-wheel-frame { position: relative; }
.r-wheel-frame::before,
.r-wheel-frame::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: var(--stroke);
  background: var(--hairline);
  pointer-events: none;
  z-index: 1;
}
.r-wheel-frame::before { top: calc(50% - var(--wheel-item, 46px) / 2); }
.r-wheel-frame::after  { top: calc(50% + var(--wheel-item, 46px) / 2); }

/* Lista de navegación en display grande: solo el activo en tinta plena */
.r-navlist { display: flex; flex-direction: column; gap: var(--s-1); }
.r-navlist__i {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: clamp(1.75rem, 4vw, 3rem);
  font-weight: var(--fw-light);
  letter-spacing: var(--tr-display);
  line-height: 1.12;
  color: var(--ink-3);
  transition: color var(--d-base) var(--ease);
}
.r-navlist__i::before {
  content: "→";
  font-size: 0.55em;
  opacity: 0;
  transition: opacity var(--d-base) var(--ease);
}
.r-navlist__i:hover { color: var(--ink-2); }
.r-navlist__i[aria-current="page"] { color: var(--ink); }
.r-navlist__i[aria-current="page"]::before { opacity: 1; }

/* Pager: ← ▬ → */
.r-pager { display: flex; align-items: center; gap: var(--s-4); }
.r-pager__bar {
  flex: 1 1 auto;
  height: var(--stroke-2);
  background: var(--trace);
  position: relative;
  border-radius: var(--r-pill);
}
.r-pager__bar::after {
  content: "";
  position: absolute;
  inset-block: 0;
  left: var(--at, 0%);
  width: var(--of, 25%);
  background: var(--ink);
  border-radius: var(--r-pill);
}
.r-pager__a {
  border: 0; background: transparent;
  color: var(--ink-3); font-size: var(--t-body);
  padding: 0 var(--s-1);
}
.r-pager__a:hover { color: var(--ink); }

/* ============================================================
   14 · CONTEOS, LEYENDAS, TIRAS
   ============================================================ */

/* Leyenda: punto lleno = serie principal, hueco = comparación */
.r-legend { display: flex; flex-direction: column; gap: var(--s-1); }
.r-legend--row { flex-direction: row; gap: var(--s-4); }
.r-legend__i {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-label);
  color: var(--ink-2);
}
.r-legend__i::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: var(--r-pill);
  background: currentColor;
}
.r-legend__i--hollow::before {
  background: transparent;
  border: var(--stroke-2) solid currentColor;
}

/* Badge de conteo circular */
.r-count {
  display: inline-grid;
  place-items: center;
  min-width: 22px; height: 22px;
  padding-inline: 6px;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: var(--stroke) solid var(--hairline);
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
}
.r-count--solid { background: var(--surface-invert); color: var(--ink-invert); border-color: transparent; }
.r-count--accent { background: var(--accent); color: var(--on-accent); border-color: transparent; }
/* Tinta oscura, no blanca: blanco sobre --crit mide 3.57:1 y este contador
   es texto de 11px. Con --on-crit sube a 5.4:1. */
.r-count--crit  { background: var(--crit); color: var(--on-crit); border-color: transparent; }

/* Icono con conteo en superíndice */
.r-iconcount { position: relative; display: inline-grid; place-items: center; }
.r-iconcount > .r-count {
  position: absolute;
  top: -6px; right: -8px;
  min-width: 17px; height: 17px;
  font-size: 0.625rem;
}

/* Tira horizontal de avatares / logos */
.r-strip {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  overflow-x: auto;
  scrollbar-width: none;
}
.r-strip::-webkit-scrollbar { display: none; }
.r-strip__a {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: var(--stroke) solid var(--hairline);
  overflow: hidden;
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
}
.r-strip__a img { width: 100%; height: 100%; object-fit: cover; }
/* Apiladas, con recorte del vecino */
.r-strip--stack { gap: 0; }
.r-strip--stack .r-strip__a + .r-strip__a { margin-left: -10px; box-shadow: -2px 0 0 var(--surface); }

/* Fracción: numerador en tinta, denominador atenuado */
.r-frac {
  display: inline-flex;
  align-items: baseline;
  gap: 0.25em;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-tight);
}
.r-frac__n { color: var(--ink); font-weight: var(--fw-medium); }
.r-frac__d { color: var(--ink-3); }
.r-frac__d::before { content: "/ "; }
.r-frac__u { font-size: 0.75em; color: var(--ink-3); }

/* Subtítulo atenuado bajo un título */
.r-sub { font-size: var(--t-body-sm); color: var(--ink-3); margin: 0; }
.r-panel .r-sub, .r-on-grad .r-sub { color: var(--on-grad-2); }

/* ============================================================
   15 · SUPERFICIES DE GRADIENTE
   ============================================================ */

.r-grad {
  position: relative;
  border-radius: var(--r-xl);
  padding: var(--s-5);
  color: var(--on-grad);
  background: var(--grad-magenta);
  overflow: hidden;
  isolation: isolate;
  /* Termina claro (#ff5fa2): el ácido no aguanta ahí, así que el número se
     comporta como el resto del texto de la superficie y el acento se muda a la
     marca. Es también el DEFAULT de .r-grad — ver la nota de § 23. */
  --tip-ink:  var(--on-grad);
  --tip-mark: block;
}
/* Cada variante declara su propio tramo final. `initial` devuelve el fallback
   de § 23, o sea "oscuro de punta a punta: el ácido es tinta y la marca se
   apaga". Se declaran las dos familias aunque una sea el default, para que se
   pueda leer de corrido cuál es cuál sin ir a medir. */
.r-grad--violet { background: var(--grad-violet); --tip-ink: initial; --tip-mark: initial; }  /* 3.50 ✓ */
.r-grad--slate  { background: var(--grad-slate);  --tip-ink: initial; --tip-mark: initial; }  /* 14.81 ✓ */
.r-grad--ocean  { background: var(--grad-ocean);  --tip-ink: var(--on-grad); --tip-mark: block; }  /* 2.90 */
.r-grad--ember  { background: var(--grad-ember);  --tip-ink: var(--on-grad); --tip-mark: block; }  /* 1.80 */
/* La difusa: para módulos ALTOS Y ANGOSTOS, donde los cinco lineales de
   arriba dejan medio módulo en negro y el color solo llega al pie. Radial
   y de baja saturación, así que no tiene arriba ni abajo. Ver --grad-mist
   en tokens.css para el por qué de los tonos y de la base. */
/* Radial sobre base --n-900/--n-950: no tiene "final", y su punto más claro
   —teal al 44% sobre casi negro— sigue siendo oscuro. Oscuro de punta a punta. */
.r-grad--mist   { background: var(--grad-mist); --tip-ink: initial; --tip-mark: initial; }
/* La tinta la resuelve la cascada de dos niveles de arriba; aquí solo
   los detalles que no salen de --ink. */
.r-grad > .r-metric__unit, .r-grad .r-metric__fade { color: var(--on-grad-2); }

/* Utilidad: tinta clara sobre cualquier fondo oscuro */
.r-on-grad { color: var(--on-grad); }
.r-on-grad .r-label, .r-on-grad .r-micro { color: var(--on-grad-2); }

/* Divisor de ola: corte orgánico entre dos superficies */
.r-wavecut { position: relative; }
.r-wavecut::before {
  content: "";
  position: absolute;
  top: -28px; left: 0; right: 0;
  height: 30px;
  background: inherit;
  border-radius: 0 0 50% 50% / 0 0 100% 100%;
}

/* ============================================================
   16 · MAPA  (rutas y marcadores)
   ============================================================ */

.r-map {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--grad-slate);
  min-height: 260px;
  /* slate: oscuro de punta a punta (14.81). El ácido sí es tinta aquí. */
  --tip-ink:  initial;
  --tip-mark: initial;
}
/* Retícula tenue de fondo cuando no hay teselas reales */
.r-map--grid::before {
  content: "";
  position: absolute; inset: 0; opacity: 0.5;
  background-image:
    linear-gradient(to right, rgba(255,255,255,.055) var(--stroke), transparent var(--stroke)),
    linear-gradient(to bottom, rgba(255,255,255,.055) var(--stroke), transparent var(--stroke));
  background-size: 34px 34px;
}
.r-map > * { position: relative; }

.r-route      { fill: none; stroke: var(--map-path); stroke-width: var(--map-path-w); stroke-linejoin: round; stroke-linecap: round; }
.r-route--dash { stroke-dasharray: var(--map-dash); }
.r-route--ghost { stroke: rgba(255, 255, 255, 0.35); }
.r-route__node { fill: var(--map-path); }
.r-route__node--stop   { fill: var(--accent-300, var(--accent)); }
.r-route__node--origin { fill: var(--info); }

/* Marcador de vehículo: burbuja congelada con glifo */
.r-marker {
  position: absolute;
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: 46% 54% 52% 48% / 50% 46% 54% 50%;
  background: rgba(255, 255, 255, 0.22);
  border: var(--stroke) solid rgba(255, 255, 255, 0.45);
  color: #fff;
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  transform: translate(-50%, -50%);
}
/* Cerco de alcance punteado */
.r-fence {
  position: absolute;
  width: var(--d, 140px); height: var(--d, 140px);
  border-radius: var(--r-pill);
  border: var(--stroke-2) dashed var(--map-fence);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
/* Pin circular */
.r-pin {
  position: absolute;
  display: grid; place-items: center;
  width: 26px; height: 26px;
  border-radius: var(--r-pill);
  background: #fff;
  color: var(--fixed-ink);   /* fondo claro fijo ⇒ tinta oscura fija */
  font-size: var(--t-micro);
  transform: translate(-50%, -50%);
  box-shadow: var(--e-2);
}
.r-pin--info { background: var(--info); color: #fff; }
.r-pin--accent { background: var(--accent); color: var(--on-accent); }

/* ============================================================
   17 · CALLOUT DE OPORTUNIDAD  (el "+$1,100")
   ============================================================ */

.r-callout {
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4);
  background: var(--accent);
  color: var(--on-accent);
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  max-width: 260px;
}
.r-callout__k {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--tr-micro);
  opacity: 0.7;
}
.r-callout__v {
  font-size: 1.5rem;
  font-weight: var(--fw-light);
  letter-spacing: var(--tr-display);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.r-callout__note { font-size: var(--t-micro); opacity: 0.7; }

/* ============================================================
   18 · FILA ÁCIDA  (selección / oportunidad en tabla)
   ============================================================ */

.r-arow {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--surface-sunk);
  transition: background var(--d-fast) var(--ease);
}
/* Fondo claro FIJO: la escala de tinta se la remapea el bloque "nivel 2
   bis" de §23, no este selector. */
.r-arow--on { background: var(--accent-soft); }
.r-arow--on .r-arow__add { background: var(--accent); color: var(--on-accent); }
.r-arow__add {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 18px; height: 18px;
  border: 0;
  border-radius: var(--r-xs);
  background: var(--trace);
  color: var(--surface);
  font-size: 0.75rem;
  line-height: 1;
}
.r-arow__id { font-family: var(--font-mono); font-size: var(--t-body-sm); }
.r-arow__meta { font-size: var(--t-micro); color: var(--ink-3); }

/* ============================================================
   19 · CONSIGNACIÓN  (tarjeta compuesta de ruta)
   ============================================================ */

.r-consign {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-4);
  background: var(--surface);
  border: var(--stroke) solid var(--hairline);
  border-radius: var(--r-md);
}
.r-consign__top { display: flex; align-items: center; gap: var(--s-3); }
.r-consign__id { font-size: var(--t-body-sm); font-weight: var(--fw-medium); font-family: var(--font-mono); }
.r-consign__ends {
  display: flex;
  justify-content: space-between;
  gap: var(--s-3);
  font-size: var(--t-body-sm);
}
.r-consign__ends span:last-child { color: var(--ink-3); }
/* Barra de carga sobre la silueta.
   El relleno es ácido fijo y el texto lo cruza, así que toda la pieza va en
   la escala fija: la pista le llega por --surface-sunk, remapeado en el
   bloque "nivel 2 bis" de §23. */
.r-load {
  position: relative;
  height: 20px;
  border-radius: var(--r-xs);
  background: var(--surface-sunk);
  overflow: hidden;
  display: flex;
  align-items: center;
  padding-inline: var(--s-2);
}
.r-load::before {
  content: "";
  position: absolute;
  inset-block: 0; left: 0;
  width: var(--at, 50%);
  background: var(--accent-soft);
}
.r-load span {
  position: relative;
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-medium);
}

/* ============================================================
   20 · ORBES GLOSSY — BORRADO. El nombre se reusa en § 33.

   Aquí vivía una bola de color con brillo especular y sombra interior
   (.r-orb__b, .r-orb__add, .r-orb__n, .r-orb__p) para enseñar una
   estadística: "Enfoque · 4.2 h".

   Se fue por el mismo argumento y el mismo precedente que .r-liquid
   (§ 22), y los dos hechos son verificables:

   · CERO consumidores en `app/`. Tres muestras, todas en el propio
     showcase — o sea, la especificación enseñaba un lenguaje que el OS
     no habla, que es la definición de catálogo-ficción.
   · Un `radial-gradient` de blanco al 92% en la esquina superior
     izquierda ES el reflejo especular, y el no-negociable dice
     "vidrio: una sola familia, y es mate".

   Lo que hacía no dejó hueco: una estadística con nombre y cifra es
   .r-metric (o .r-lcdrow si es LA cifra de la vista), y el color por
   categoría lo llevan los semánticos o .r-chip.

   El nombre pasa a los botones del chasis (§ 33b) porque es como
   Robbie los llama, y un nombre libre vale más ocupado por lo que el
   OS de verdad tiene.
   ============================================================ */

/* ============================================================
   21 · TOOLBAR  (búsqueda + acciones etiquetadas)
   ============================================================ */

.r-toolbar {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2);
  border-radius: var(--r-pill);
  background: var(--surface);
  border: var(--stroke) solid var(--hairline);
  overflow-x: auto;
  scrollbar-width: none;
}
.r-toolbar::-webkit-scrollbar { display: none; }
.r-toolbar__a {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  height: 30px;
  padding-inline: var(--s-3);
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--ink-2);
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}
.r-toolbar__a:hover { background: var(--surface-sunk); color: var(--ink); }

/* ============================================================
   22 · VIDRIO MATE
   El translúcido del sistema, y desde hoy el ÚNICO. Borde de
   línea uniforme, sin brillo especular ni canto refractado.

   Aquí vivía también .r-liquid, la familia brillante "para
   controles". Se borró, y el argumento no fue de gusto: tenía
   CERO consumidores en la app —once usos, todos en el propio
   showcase, más un botón del laboratorio— así que la
   especificación estaba enseñando un lenguaje que el OS no
   habla. Un catálogo que documenta lo que nadie usa no es una
   referencia, es ficción.

   Los controles no se quedaron sin superficie: .r-btn,
   .r-icon-btn, .r-input, .r-chip y .r-check ya se remapean
   dentro de esta familia y sobre color (ver § 23), que es
   exactamente por lo que nunca hizo falta la otra.
   ============================================================ */

.r-frost {
  position: relative;
  border-radius: var(--r-xl);
  padding: var(--s-5);
  background: var(--frost-dark);
  border: var(--stroke) solid var(--frost-line);
  backdrop-filter: blur(var(--frost-blur)) saturate(115%);
  -webkit-backdrop-filter: blur(var(--frost-blur)) saturate(115%);
  color: var(--on-grad);
  --ink:   var(--on-grad);
  --ink-2: var(--on-grad-2);
  --ink-3: var(--on-grad-2);
  --ink-4: var(--on-grad-3);
  --trace: var(--on-grad-3);
  /* Remapeo de superficies y líneas: es lo que hace
     que .r-input, .r-btn, .r-icon-btn, .r-chip y .r-check funcionen dentro
     de una tarjeta de vidrio sin variante aparte. */
  --surface:         var(--glass-raise);
  --surface-2:       var(--glass-raise);
  --surface-sunk:    var(--glass-sunk);
  /* Y el papel translúcido con ellas: aquí la tinta ya se remapeó a clara, así
     que un .r-card adentro con el --surface-frost del TEMA saldría casi blanco
     en claro con tinta clara encima. Con --glass-raise se comporta igual que
     antes de que .r-card fuera vidrio. */
  --surface-frost:   var(--glass-raise);
  --ink-invert:      var(--fixed-ink);
  --surface-invert:  var(--on-grad);
  --hairline:        var(--on-grad-line);
  --hairline-strong: var(--on-grad-line-strong);
  /* Vidrio oscuro en los dos temas ⇒ el ácido sigue siendo tinta. */
  --accent-ink:      var(--accent-press);
  --accent-mark:     none;
}

/* Lechoso: deja pasar el color del fondo, muy difuminado */
.r-frost--light {
  background: var(--frost-light);
  border-color: rgba(255, 255, 255, 0.42);
}

/* Casi opaco, para bloques de lectura sobre foto */
.r-frost--solid { background: var(--frost-solid); }

.r-frost__t { font-size: 1.25rem; font-weight: var(--fw-book); letter-spacing: var(--tr-tight); margin: 0; }
.r-frost__s { font-size: 1.0625rem; color: var(--on-grad-2); margin: 0; }

/* Casilla de selección de tarjeta: cuadrado redondeado, blanco al marcar */
.r-selectbox {
  appearance: none;
  width: 34px; height: 34px;
  flex: 0 0 auto;
  border-radius: var(--r-sm);
  border: var(--stroke) solid rgba(255, 255, 255, 0.30);
  background: rgba(255, 255, 255, 0.10);
  display: inline-grid;
  place-items: center;
  transition: background var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease);
}
.r-selectbox:hover { background: rgba(255, 255, 255, 0.18); }
.r-selectbox:checked {
  background: var(--fixed-surface);
  border-color: transparent;
}
.r-selectbox:checked::after {
  content: "";
  width: 12px; height: 6px;
  border-left: 2px solid var(--fixed-ink);
  border-bottom: 2px solid var(--fixed-ink);
  rotate: -45deg;
  translate: 0 -2px;
}
/* Chica: la que cabe en una FILA DE LISTA, al lado de una línea de texto.
   La de 34px es para tarjetas de selección enteras y en una lista de
   tareas empujaba la fila al doble de alto. Es el mismo control, no otro:
   solo cambia la caja y el largo de la paloma. */
.r-selectbox--sm {
  width: 20px; height: 20px;
  border-radius: var(--r-xs);
}
.r-selectbox--sm:checked::after {
  width: 9px; height: 5px;
  border-left-width: var(--stroke-2);
  border-bottom-width: var(--stroke-2);
  translate: 0 -1px;
}

/* ============================================================
   22c · BURBUJA MATE  (marcador flotante sobre un track)
   ============================================================ */

.r-bubble {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-5);
  border-radius: var(--r-pill);
  background: var(--bubble-bg);
  color: var(--fixed-ink);
  border: var(--stroke) solid rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  box-shadow: 0 12px 34px -12px rgba(0, 0, 0, 0.22);
}
.r-bubble__t { font-size: 1.0625rem; font-weight: var(--fw-medium); letter-spacing: var(--tr-tight); margin: 0; }
.r-bubble__s { font-size: var(--t-body-sm); color: var(--fixed-ink-3); margin: 0; }
.r-bubble--abs { position: absolute; transform: translate(-50%, -50%); }

/* ============================================================
   22d · SCATTER TRACK  (lo pinta ron.js)
   Eventos como puntos de tamaño y color variable sobre una regla.
   ============================================================ */

.r-scatter {
  position: relative;
  width: 100%;
  height: var(--scatter-h, 84px);
  overflow: visible;
}
.r-scatter__tick { stroke: currentColor; stroke-opacity: 0.22; stroke-width: var(--stroke); }
.r-scatter__d    { fill: currentColor; fill-opacity: 0.30; }
.r-scatter__d--ok   { fill: var(--ok);   fill-opacity: 0.9; }
.r-scatter__d--info { fill: var(--info); fill-opacity: 0.9; }
.r-scatter__d--crit { fill: var(--crit); fill-opacity: 0.9; }
.r-scatter__d--warn { fill: var(--warn); fill-opacity: 0.9; }
.r-scatter__d--accent { fill: var(--accent-press); fill-opacity: 1; }

/* ============================================================
   22e · SLIDER  (range nativo con burbuja de valor)
   ============================================================ */

.r-slider { position: relative; padding-block: var(--s-6) var(--s-2); }
.r-slider__in {
  appearance: none;
  width: 100%;
  height: 4px;
  border-radius: var(--r-pill);
  background: linear-gradient(
    to right,
    var(--acid-300) 0%, var(--acid-400) var(--pct, 50%),
    var(--surface-sunk) var(--pct, 50%), var(--surface-sunk) 100%
  );
  outline: none;
}
.r-slider__in::-webkit-slider-thumb {
  appearance: none;
  width: 18px; height: 18px;
  border-radius: var(--r-pill);
  background: var(--fixed-surface);
  border: var(--stroke) solid var(--fixed-hairline);
  box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.3);
  cursor: grab;
}
.r-slider__in::-moz-range-thumb {
  width: 18px; height: 18px;
  border-radius: var(--r-pill);
  background: var(--fixed-surface);
  border: var(--stroke) solid var(--fixed-hairline);
  box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.3);
  cursor: grab;
}
.r-slider__in::-moz-range-track { height: 4px; background: transparent; }
.r-slider__bubble {
  position: absolute;
  top: 0;
  left: var(--pct, 50%);
  transform: translate(-50%, 0);
  white-space: nowrap;
  pointer-events: none;
}
/* Pista callada: el valor vive en un `r-tnum` al lado de la
   etiqueta, no en una burbuja sobre el pulgar. */
.r-slider--quiet { padding-block: var(--s-2); }
.r-slider--quiet .r-slider__bubble { display: none; }

/* ============================================================
   23 · AURORA  (fondo ambiental sin banding)
   ============================================================ */

/* ---- Cascada de dos niveles sobre superficies de color ----
   Nivel 1: el contenedor de color remapea la tinta a la escala clara,
   así .r-label / .r-micro / .r-metric funcionan sin reglas especiales.
   Nivel 2: el papel que flota encima (.r-card, .r-sheet, .r-pillcard)
   vuelve a la escala oscura fija — blanco en los dos temas.

   La regla que hace que esto no se rompa: cada vez que se remapea --ink
   hay que remapear con él a sus TRES acompañantes, porque tres componentes
   los leen en pareja y si uno se queda con el valor del tema, el par se
   invierte y la pieza desaparece:
     · --ink-invert      la paloma de .r-check:checked va sobre --ink
     · --surface-invert  el fondo de .r-btn--solid / .r-count--solid /
                         .r-seg__i[aria-selected] va con --ink-invert
     · --hairline-strong el borde de .r-btn / .r-icon-btn /
                         .r-chip--outline / .r-count
   Sin ellos: en cockpit la paloma salía oscura sobre caja oscura y el
   borde del .r-icon-btn salía blanco sobre papel blanco. Medido, no
   supuesto.

   .r-mesh NO entra en esta cascada, y es a propósito: aurora, gradiente y
   mapa son oscuros en los dos temas, pero el mesh se pinta sobre
   --canvas-deep, así que sigue al tema (lavado pálido en claro, oscuro en
   cockpit). Con tinta clara forzada, en claro quedaría blanco sobre un
   fondo casi blanco. Medido: hoy un .r-card encima del mesh se lee bien en
   los dos temas; meterlo aquí lo rompería. */

.r-aurora, .r-grad, .r-map {
  color: var(--on-grad);
  --ink:   var(--on-grad);
  --ink-2: var(--on-grad-2);
  --ink-3: var(--on-grad-2);
  --ink-4: var(--on-grad-3);
  --trace: var(--on-grad-3);
  --hairline: var(--on-grad-line);
  /* Aquí --ink es claro, así que su inverso es la tinta oscura fija y el
     relleno que la acompaña es luz. */
  --ink-invert:      var(--fixed-ink);
  --surface-invert:  var(--on-grad);
  --hairline-strong: var(--on-grad-line-strong);
  /* Y las superficies, por lo mismo: un control que cae DIRECTO sobre el
     color (sin papel debajo) pedía --surface y le llegaba la del tema —en
     claro, pastilla blanca con tinta blanca encima; medido: .r-btn y
     .r-chip desaparecían. Sobre color un control es vidrio, así que van a
     los mismos pozos que .r-frost y quedan iguales en los dos
     temas. El papel que flota encima las vuelve a pisar (nivel 2). */
  --surface:      var(--glass-raise);
  --surface-2:    var(--glass-raise);
  --surface-sunk: var(--glass-sunk);
  /* Y el PAPEL TRANSLÚCIDO con ellas, que era el hueco de esta lista.
     --surface-frost sigue al tema (blanco al 58% en claro, casi negro en
     cockpit) y aquí arriba la tinta ya se remapeó a clara: cualquier pieza
     que se pinte con papel translúcido DIRECTO sobre el color —sin ser una
     de las cuatro del nivel 2— salía blanco al 58% con tinta blanca encima.
     Es exactamente el bug que el README llama "fondo claro fijo con --ink",
     dicho con superficies en vez de con tinta, y estaba abierto porque
     cuando se escribió esta lista .r-card todavía era opaca: el único
     consumidor de --surface-frost ya venía cubierto por el nivel 2.
     Hoy lo piden además la caja de prompt, el invocador y los orbes del
     chasis, que sí caen directo. Sobre color el papel translúcido es
     vidrio, así que va al mismo relieve que el resto de los controles; el
     nivel 2 lo vuelve a pisar con --fixed-surface unas líneas abajo. */
  --surface-frost: var(--glass-raise);
  /* ---- El acento sobre color: lo DECLARA la superficie, no lo adivina la
     cascada ----
     Esto decía "superficie oscura en los DOS temas ⇒ el ácido sigue siendo
     tinta y la marca se apaga". La primera mitad es falsa: las rampas
     lineales son oscuras al empezar y CLARAS al terminar, y ahí el ácido se
     cae. Medido en el último tramo declarado de cada una:

       ember   #ff8a3d   ácido 1.80   blanco 2.35
       magenta #ff5fa2   ácido 2.17   blanco 2.83
       ocean   #1c8fa8   ácido 2.90   blanco 3.79
       violet  #6d5bff   ácido 3.50   blanco 4.57   ✓
       slate   #0c0e10   ácido 14.81  blanco 19.34  ✓
       aurora  #cdd5d1   ácido 1.15   blanco 1.50
       …warm   #e4d9cc   ácido 1.07   blanco 1.39
       …cool   #dfe6ee   ácido 1.04   blanco 1.26

     Y lo grave no era el número: era que la marca se apagaba JUSTO donde hacía
     falta, así que el acento se quedaba sin nadie que lo cargara.

     El arreglo es de mecanismo, no de valores: cada superficie declara qué pasa
     en su tramo más claro (--tip-ink / --tip-mark, junto a su propio
     `background`) y estas dos líneas lo leen. El fallback es el comportamiento
     de siempre, así que las que se quedan oscuras de punta a punta no declaran
     nada… salvo que se declaran igual, para que el catálogo se lea completo.

     La polaridad es a propósito: el DEFAULT de .r-grad es "termina claro". Un
     gradiente nuevo queda cubierto sin que nadie se acuerde de esto, y quien
     agregue uno oscuro tiene que decirlo — que es el lado seguro del error.

     Lo que este mecanismo NO arregla, y hay que decirlo: en el tramo claro de
     una aurora ninguna tinta funciona (blanco 1.26–1.50, oscura 13–15 pero
     entonces se cae en el tramo oscuro del principio). Ahí el acento lo carga
     la marca y el número se comporta como todo el demás texto de esa
     superficie. Que un número acentuado no vaya directo sobre una aurora sigue
     siendo la regla; esto solo deja de mentir sobre el acento. */
  --accent-ink:  var(--tip-ink, var(--accent-press));
  --accent-mark: var(--tip-mark, none);
}

/* Y sin la capa de desenfoque: ahí el papel es opaco, así que el
   backdrop-filter no tiene nada que revelar y solo cuesta una capa de
   composición por tarjeta. */
.r-aurora .r-card, .r-grad .r-card, .r-map .r-card,
.r-aurora .r-sheet, .r-grad .r-sheet, .r-map .r-sheet {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.r-aurora .r-card,    .r-grad .r-card,    .r-map .r-card,
.r-aurora .r-sheet,   .r-grad .r-sheet,   .r-map .r-sheet,
.r-aurora .r-pillcard, .r-grad .r-pillcard, .r-map .r-pillcard,
.r-aurora .r-consign, .r-grad .r-consign, .r-map .r-consign {
  color: var(--fixed-ink);
  --surface:      var(--fixed-surface);
  --surface-2:    var(--fixed-surface);
  --surface-sunk: var(--fixed-sunk);
  /* El papel que flota sobre color SATURADO no es vidrio: es opaco y blanco en
     los dos temas. Si se quedara con el --surface-frost del tema, en cockpit
     sería una tarjeta casi negra sobre el gradiente —el "agujero" que esta
     cascada existe para evitar— y en claro dejaría pasar el gradiente por
     debajo del texto. Este renglón es lo que mantiene viva esa regla ahora que
     .r-card es translúcida por omisión. */
  --surface-frost: var(--fixed-surface);
  --ink:          var(--fixed-ink);
  --ink-2:        var(--fixed-ink-2);
  --ink-3:        var(--fixed-ink-3);
  --ink-4:        var(--fixed-ink-4);
  --trace:        var(--fixed-ink-4);
  --hairline:     var(--fixed-hairline);
  /* El papel es el del tema claro, así que sus tres acompañantes también:
     inverso claro, relleno sólido oscuro, borde fuerte oscuro. */
  --ink-invert:      var(--fixed-surface);
  --surface-invert:  var(--fixed-ink);
  --hairline-strong: var(--fixed-hairline-strong);
  /* El papel es claro en los dos temas, así que el ácido deja de ser tinta
     y el acento se va a la marca. Medido: sin esto, un .r-t-accent en una
     tarjeta sobre gradiente daba 1.31:1 en AMBOS temas. */
  --accent-ink:  var(--fixed-ink);
  --accent-mark: block;
}

/* ---- Nivel 2 bis: el componente que trae su propio fondo claro FIJO ----
   No necesita superficie de color debajo: pinta ácido o blanco por su
   cuenta, y ese fondo no cambia con el tema. La regla del README aplica
   igual —fondo claro fijo ⇒ tinta oscura fija—, y aplica a TODA la escala,
   no solo al color del bloque: por eso va la misma lista de remapeos que
   el papel sobre color, y no un `color:` en cada hijo. Así .r-arow__meta
   (que va en --ink-3) y cualquier .r-micro o .r-chip que alguien meta
   adentro quedan cubiertos sin tocarlos.

   Medido antes de escribirlo: en cockpit .r-arow__id salía #f6f6f4 sobre
   --accent-soft (#f4ffc9), 1.03:1. .r-load es el mismo caso a medias: su
   relleno es ácido fijo y el texto lo cruza, así que la pista también se
   va a la escala fija (le llega por --surface-sunk, que es de donde ya
   sacaba su fondo) y la barra queda idéntica en los dos temas.

   Los que NO entran porque ya estaban bien: .r-callout, .r-chip--accent,
   .r-count--accent, .r-icon-btn--accent, .r-mark y .r-bg-accent declaran
   --on-accent; .r-pin, .r-bubble, .r-selectbox:checked y
   .r-rail__i[aria-current] declaran --fixed-ink. */
.r-arow--on,
.r-load {
  color: var(--fixed-ink);
  --ink:   var(--fixed-ink);
  --ink-2: var(--fixed-ink-2);
  --ink-3: var(--fixed-ink-3);
  --ink-4: var(--fixed-ink-4);
  --trace: var(--fixed-ink-4);
  --hairline:        var(--fixed-hairline);
  --hairline-strong: var(--fixed-hairline-strong);
  --ink-invert:      var(--fixed-surface);
  --surface:         var(--fixed-surface);
  --surface-2:       var(--fixed-surface);
  --surface-sunk:    var(--fixed-sunk);
  --surface-invert:  var(--fixed-ink);
  /* Fondo claro ⇒ el ácido no es tinta. Y la marca también se apaga: aquí
     el propio fondo ES el acento, y una barra ácida encima de ácido no se
     vería (1.2:1 medido). */
  --accent-ink:      var(--fixed-ink);
  --accent-mark:     none;
}

.r-aurora {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--aurora);
  /* Las tres auroras terminan en gris pálido (#cdd5d1 / #e4d9cc / #dfe6ee),
     que es el peor caso de color del sistema: ahí el ácido mide 1.04–1.15 y el
     blanco 1.26–1.50. El acento se va a la marca; el número no se puede
     rescatar con un token en esta superficie (ver § 23). */
  --tip-ink:  var(--on-grad);
  --tip-mark: block;
}
/* Manchas difusas encima: rompen las bandas del degradado lineal */
.r-aurora::before {
  content: "";
  position: absolute;
  inset: -20%;
  z-index: -1;
  background:
    radial-gradient(42% 30% at 24% 12%, rgba(120, 255, 240, 0.55) 0%, transparent 66%),
    radial-gradient(38% 26% at 82% 30%, rgba(90, 220, 210, 0.42) 0%, transparent 66%),
    radial-gradient(52% 34% at 60% 74%, rgba(255, 178, 176, 0.52) 0%, transparent 68%),
    radial-gradient(40% 26% at 16% 88%, rgba(214, 226, 218, 0.45) 0%, transparent 66%);
  filter: blur(48px);
}
.r-aurora--warm { background: var(--aurora-warm); }
.r-aurora--cool { background: var(--aurora-cool); }

/* ============================================================
   24 · ONDA LIMPIA  (la pinta ron.js)
   ============================================================ */

.r-wave { display: block; width: 100%; height: var(--wave-h, 96px); overflow: visible; color: var(--on-grad-2); }
.r-wave__line {
  fill: none;
  stroke: var(--wave-ink, rgba(255, 255, 255, 0.85));
  stroke-width: var(--stroke-2);
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.r-wave__base { stroke-width: var(--stroke-2); stroke-dasharray: 1 7; stroke-linecap: round; }
.r-wave__node { fill: var(--wave-ink, #fff); }
.r-wave__node--accent { fill: var(--accent); }
/* Sobre fondo claro */
.r-wave--ink { --wave-ink: var(--ink); color: var(--trace); }

/* ============================================================
   24b · TELARAÑA  (radar de habilidades — la pinta ron.js)

   Dos contornos sobre la misma retícula: el NIVEL actual —el dato que
   manda— y, opcional, el OBJETIVO como referencia. La jerarquía no se
   hace con un segundo color (eso serían dos acentos) sino con relleno
   y trazo: el nivel lleva el relleno ácido y trazo continuo, el
   objetivo va punteado, hueco y atenuado.

   El trazo del nivel va a --accent-ink, no a --accent: el ácido es
   tinta solo sobre oscuro. En cockpit el contorno ES ácido; sobre
   papel claro pasa a tinta normal y el acento lo carga el relleno,
   que es el otro recurso permitido. Sobre .r-grad / .r-panel /
   .r-frost la cascada de §23 lo devuelve a ácido sola.
   ============================================================ */

/* El alto sale del ancho: una telaraña deformada no se lee. Así el
   consumidor nunca tiene que fijar altura a mano. */
.r-radar {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--radar-ar);
  font-family: var(--font-sans);
  font-size: var(--t-micro);
}

/* Retícula: textura, no protagonista. Mismo punteado que .r-chart__grid. */
.r-radar__ring {
  fill: none;
  stroke: currentColor;
  stroke-opacity: var(--radar-grid-o);
  stroke-width: var(--stroke);
  stroke-dasharray: var(--dash-fine);
  vector-effect: non-scaling-stroke;
}
.r-radar__ring--edge { stroke-opacity: var(--radar-edge-o); stroke-dasharray: none; }
.r-radar__spoke {
  stroke: currentColor;
  stroke-opacity: var(--radar-spoke-o);
  stroke-width: var(--stroke);
  vector-effect: non-scaling-stroke;
}

/* Nivel actual: relleno ácido + trazo de tinta acentuada */
.r-radar__lvl {
  fill: var(--radar-fill);
  fill-opacity: var(--radar-fill-o);
  stroke: var(--accent-ink);
  stroke-width: var(--stroke-2);
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.r-radar__node { fill: var(--accent-ink); }

/* Objetivo: hueco, punteado y atenuado. Es referencia, no dato. */
.r-radar__tgt {
  fill: none;
  stroke: currentColor;
  stroke-opacity: var(--radar-target-o);
  stroke-width: var(--stroke-2);
  stroke-dasharray: var(--dash-sm);
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.r-radar__lbl {
  fill: currentColor;
  fill-opacity: var(--radar-label-o);
  font-size: inherit;
}

/* Entrada: crece desde el centro. Solo en el primer dibujo — al
   redibujar por tema o por resize no se vuelve a animar.

   `forwards` y NO `both`: con `both` el estado inicial (opacidad 0)
   se aplica también ANTES de que la animación corra, y si por
   cualquier razón no llega a correr el contorno se queda invisible
   para siempre. Con `forwards` el estado natural del grupo es
   "ya dibujado" y la animación solo lo acompaña — que es justo lo
   que hace falta con prefers-reduced-motion: la regla global de
   hasta arriba del archivo aplasta la duración a 0.01ms y la
   telaraña aparece completa, sin parpadeo. */
.r-radar__series--in {
  animation: r-radar-in var(--d-slow) var(--ease-out) forwards;
}
@keyframes r-radar-in {
  from { transform: scale(0.88); opacity: 0; }
  to   { transform: scale(1);    opacity: 1; }
}

/* ============================================================
   24c · GRAFO EGO  (red de conocidos — lo pinta ron.js)

   Un nodo al centro y los demás alrededor. La DISTANCIA al centro es un
   dato —la cercanía de la relación—, no estética; el ÁNGULO es libre, y
   es lo único que la relajación mueve. Por eso el acomodo no puede
   mentir: aunque el reparto quede feo, el radio sigue diciendo lo que
   decían los datos.

   Los cables entre pares son la información nueva (quién conoce a
   quién) y hacen que los que se conocen caigan juntos solos. Los cables
   al centro son redundantes con el radio, así que van un paso más
   apagados: son estructura, no dato.

   Dos pesos de nodo, y se separan por TAMAÑO y TRAZO, nunca por color:
   la trama es un punto suave con aro de hairline, el peso alto es más
   grande y con aro firme. El acento entra una sola vez, en el nodo que
   manda. El centro no es un tercer tamaño —ya lo distingue estar en el
   centro y ser el único ácido—.

   No hay puntas de flecha, a propósito: este grafo no es dirigido. Si
   hace falta dirección, eso es un flowchart y lo dibuja mermaid.js.
   ============================================================ */

/* El alto sale del ancho: el consumidor nunca fija altura a mano. El
   lienzo es un <svg> que ron.js inyecta dentro; el host es un <div>
   porque es la forma que ya tiene .r-chart y deja el hueco para poner
   algo de HTML encima sin volver a inventar la caja. */
.r-graph {
  display: block;
  width: 100%;
  aspect-ratio: var(--graph-ar);
  font-family: var(--font-sans);
  font-size: var(--t-micro);
}
.r-graph__c { display: block; width: 100%; height: 100%; }

/* Cables: --dash-sm y no --map-dash. En un tramo de 24–58px el guion
   largo apenas da para dos y deja de leerse como punteado. */
.r-graph__e {
  fill: none;
  stroke: currentColor;
  stroke-opacity: var(--graph-edge-o);
  stroke-width: var(--stroke);
  stroke-dasharray: var(--dash-sm);
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.r-graph__e--ego { stroke-opacity: var(--graph-spoke-o); }

/* Un cable a una fila que ya no existe se apaga con ella: si conservara
   la tinta del cable vivo, la evidencia ida se leería igual de firme. */
.r-graph__e--ido { stroke: var(--trace); stroke-opacity: 1; }

/* Trama: punto suave con aro de hairline. */
.r-graph__n {
  fill: currentColor;
  fill-opacity: var(--graph-fill-o);
  stroke: currentColor;
  stroke-opacity: var(--graph-ring-o);
  stroke-width: var(--stroke);
  vector-effect: non-scaling-stroke;
}

/* Peso alto: más grande (lo pone ron.js) y aro firme. Mismo color. */
.r-graph__n--foco {
  fill-opacity: var(--graph-foco-o);
  stroke-opacity: 1;
  stroke-width: var(--stroke-2);
}

/* El nodo que manda: el único ácido de la vista. Sobre papel claro
   --accent-ink cae a tinta normal y el ácido lo carga el relleno, igual
   que el contorno de la telaraña; sobre color y sobre vidrio la cascada
   de §23 lo devuelve a ácido sola. */
.r-graph__n--ego {
  fill: var(--graph-ego-fill);
  fill-opacity: 1;
  stroke: var(--accent-ink);
  stroke-opacity: 1;
  stroke-width: var(--stroke-2);
}

/* La fila que ya no existe: hueca y punteada, la misma lengua que
   .lv-ref--ido y .intel-in--ido. Sin esto el grafo miente donde la
   tarjeta no miente. */
.r-graph__n--ido {
  fill: none;
  stroke: var(--trace);
  stroke-opacity: var(--graph-ido-o);
  stroke-width: var(--stroke);
  stroke-dasharray: var(--dash-sm);
}

.r-graph__lbl {
  fill: currentColor;
  fill-opacity: var(--graph-label-o);
  font-size: inherit;
}
/* La etiqueta de una fila ida se lee, pero más callada: es memoria. */
.r-graph__lbl--ido { fill: var(--ink-3); fill-opacity: 1; }

/* Entrada: crece desde el centro, y solo la primera vez. `forwards` y no
   `both`, por la razón que ya documentó la telaraña: con `both` el estado
   inicial se aplica también antes de correr, y si la animación no llega a
   correr el grafo se queda invisible para siempre. */
.r-graph__series--in {
  animation: r-graph-in var(--d-slow) var(--ease-out) forwards;
}
@keyframes r-graph-in {
  from { transform: scale(0.9); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}

/* ============================================================
   25 · PASTILLA CON AVATAR
   ============================================================ */

.r-pillcard {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-2) var(--s-2) var(--s-2) var(--s-5);
  background: var(--surface);
  border-radius: var(--r-pill);
  box-shadow: var(--e-2);
  min-height: 68px;
}
.r-pillcard__n { font-size: 1.0625rem; font-weight: var(--fw-book); letter-spacing: var(--tr-tight); }
.r-avatar {
  flex: 0 0 auto;
  width: 52px; height: 52px;
  border-radius: var(--r-pill);
  overflow: hidden;
  background: var(--surface-sunk);
  display: grid; place-items: center;
}
.r-avatar img { width: 100%; height: 100%; object-fit: cover; }
.r-avatar--accent { box-shadow: 0 0 0 2px var(--accent); }
.r-avatar--sm { width: 38px; height: 38px; }
.r-avatar--lg { width: 68px; height: 68px; }

/* Tarjeta blanca de detalle, radio grande */
.r-sheet {
  background: var(--surface);
  border-radius: var(--r-2xl);
  padding: var(--s-5);
  box-shadow: var(--e-2);
}

/* ============================================================
   26 · LÍNEA DE PROGRESO ÁCIDA
   ============================================================ */

.r-prog {
  position: relative;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--surface-sunk);
}
.r-prog::before {
  content: "";
  position: absolute;
  inset-block: 0;
  left: 0;
  width: var(--at, 60%);
  border-radius: var(--r-pill);
  background: linear-gradient(to right, var(--acid-100) 0%, var(--acid-300) 70%, var(--acid-400) 100%);
}
/* Cabeza redonda al final del avance */
.r-prog::after {
  content: "";
  position: absolute;
  top: 50%;
  left: var(--at, 60%);
  width: 9px; height: 9px;
  border-radius: var(--r-pill);
  background: var(--acid-400);
  transform: translate(-50%, -50%);
}

/* ============================================================
   27 · WALLPAPER AMBIENTAL
   ============================================================ */

/* Una sola capa detrás de toda la interfaz:
   <div class="r-wall" aria-hidden="true"></div> como primer hijo de body.
   La variante se elige con data-wall en :root, no con clases en el div:
   así el OS cambia de humor con un atributo y el div nunca tiene que
   saber cuál está puesto. Sin data-wall (o con "none") no pinta nada.

   Sin animación a propósito: mover una capa del tamaño del viewport es lo
   más caro que se puede hacer en esta página y el fondo es atmósfera, no
   espectáculo. */

.r-wall {
  position: fixed;
  inset: 0;
  /* z-index negativo, no 0: el fondo de body se propaga al lienzo del
     documento y se pinta por DEBAJO de los hijos con z-index negativo, así
     que la capa se ve sin que nadie le ceda su fondo, y sigue quedando
     debajo del fondo de cualquier bloque en flujo. Con z-index: 0 taparía
     el texto. Es el piso de la escala de capas (ver tokens.css). */
  z-index: var(--z-below);
  pointer-events: none;
  user-select: none;
}

/* SIN atributo, la capa PINTA. Es la inversión del default: el color detrás
   es la postura del sistema, no una excepción que haya que pedir, porque
   .r-card es papel translúcido y sin color detrás no es vidrio, es un
   rectángulo gris. Los tonos por omisión son los de `aurora` (ver abajo).

   "none" sigue siendo el apagador explícito: una vista que necesite lienzo
   plano —una hoja de lectura larga, una captura— lo pide y lo obtiene. */
:root[data-wall="none"] .r-wall { display: none; }

:root:not([data-wall="none"]) .r-wall {
  background-color: var(--canvas);
  background-image: var(--wall-blooms);
  background-repeat: no-repeat;
}

/* El velo va ENCIMA de las manchas y lleva el color del lienzo del tema:
   el mismo tono se aclara en claro y se apaga en cockpit. Si una variante
   le come contraste al texto suelto, esto es lo que se sube. */
:root:not([data-wall="none"]) .r-wall::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--canvas);
  opacity: var(--wall-veil);
}

/* ---- Los ocho climas ----
   Solo cambian los cuatro tonos; la geometría es una sola y vive en
   --wall-blooms. Orden de las manchas: a = arriba-izquierda,
   b = derecha, c = abajo-derecha, d = abajo-izquierda.

   Los tonos se ponen en el elemento que lleva data-wall, no en .r-wall:
   --wall-blooms se declara en :root y sus var() internos se resuelven ahí
   mismo, así que si --wall-a viviera en el div el gradiente entero quedaría
   inválido y no pintaría nada. Lección aprendida en carne propia.

   Por eso el selector va sin :root: además de la capa, cualquier elemento
   con data-wall="…" hereda los cuatro tonos y puede pintarse el mismo fondo
   con var(--wall-blooms) — una miniatura de 40px para el selector de
   wallpaper, por ejemplo, sin volver a escribir las paletas. Para que a ese
   tamaño se distingan, la miniatura remapea --wall-veil a
   var(--wall-veil-preview); con el velo del wallpaper se ven todas iguales. */

/* El de la casa, y la paleta POR OMISIÓN: naranja cálido, azul acero, rosa
   polvo y verde. `:root:not([data-wall])` entra aquí para que una página que
   no elige clima igual tenga color detrás — es el default invertido.
   `bloom-acid` NO puede ser el default: compite con el acento en cualquier
   vista que tenga el ácido en un dato (su propia advertencia, más abajo). */
/* `:where(.r-iris)` entra aquí, con especificidad CERO, y es lo que evita
   copiar la paleta de la casa en un segundo lugar: el campo de color de
   § 33c siempre tiene tonos —aunque el tapiz esté en "none" y --wall-a…d
   se queden sin valor— y cualquier `data-wall` propio (0-1-0) le gana. Sin
   esto, apagar el tapiz dejaba al panel sin nada que recoger, que es
   exactamente lo que esa pieza existe para impedir. */
:root:not([data-wall]),
[data-wall="aurora"],
:where(.r-iris) {
  --wall-a: var(--amber-500);
  --wall-b: var(--blue-300);
  --wall-c: var(--rose-300);
  --wall-d: var(--green-500);
}
/* --coral-700 y --n-600 quedaron fuera de todas las paletas: son los dos
   tonos más oscuros del sistema y en tema claro son los únicos que bajan
   --ink-3 por debajo de 2.1:1. Su lugar lo toma el escalón de arriba. */
/* Cálida PÁLIDA: rosa polvo y durazno. La saturada es `ember`; si las dos
   llevan coral y ámbar, en miniatura son la misma. */
[data-wall="aurora-warm"] {
  --wall-a: var(--rose-300);
  --wall-b: var(--amber-500);
  --wall-c: var(--rose-300);
  --wall-d: var(--n-300);
}
/* Las tres frías tienen que distinguirse en una miniatura de 40px, que es
   donde se eligen: aurora-cool se queda con TODO el azul acero (y nada de
   teal ni de violeta), ocean se va al teal, violet al violeta. Cuando las
   tres compartían --blue-500 eran la misma opción tres veces. */
[data-wall="aurora-cool"] {
  --wall-a: var(--blue-300);
  --wall-b: var(--blue-500);
  --wall-c: var(--n-300);
  --wall-d: var(--blue-500);
}
/* La única que roza el ácido. Se queda, pero como OLIVO atmosférico, no como
   el acento derramado: --acid-500 mezclado con verde, que es el escalón más
   apagado de la familia. Con --acid-200 la mancha era casi invisible en claro
   y en cockpit encendía el fondo lo suficiente para bajar --ink-3 a 2.86:1.

   ADVERTENCIA DE USO: en una vista donde el acento ya vive en un dato
   (--accent en una métrica, un .r-prog, la cuña de una gráfica) esta variante
   compite con él. Ahí va `slate` o cualquiera de las frías. */
[data-wall="bloom-acid"] {
  --wall-a: color-mix(in srgb, var(--acid-500) 72%, var(--green-500));
  --wall-b: var(--teal-300);
  --wall-c: var(--n-300);
  --wall-d: var(--acid-500);
}
/* Brasa: naranja y rojo, sin rosa que la suavice. */
[data-wall="ember"] {
  --wall-a: var(--amber-500);
  --wall-b: var(--coral-500);
  --wall-c: var(--amber-500);
  --wall-d: var(--coral-500);
}
/* Violeta de verdad, con rosa polvo de contrapunto. Sin azul: el azul es de
   aurora-cool. */
[data-wall="violet"] {
  --wall-a: var(--violet-500);
  --wall-b: var(--rose-300);
  --wall-c: var(--violet-500);
  --wall-d: var(--coral-300);
}
/* Teal, que es lo que lo hace océano y no cielo. */
[data-wall="ocean"] {
  --wall-a: var(--teal-500);
  --wall-b: var(--teal-300);
  --wall-c: var(--teal-500);
  --wall-d: var(--green-500);
}
/* Grafito: la opción "fondo sin color", y tiene que leerse como decisión, no
   como haber dejado el lienzo pelón. De ahí las tres cosas que la hacen
   distinta: niebla FRÍA (cada neutro va mezclado con azul, no gris puro),
   geometría cerrada y el par denso de tinte. Es la única variante que se
   sale de los valores globales, y por eso se sale desde tokens. */
/* El selector va doble a propósito: --wall-bloom también se declara en los
   bloques de tema (`:root[data-theme="light"]`, 0-2-0), así que un
   `[data-wall="slate"]` pelón (0-1-0) perdía y la variante se quedaba con el
   tinte normal. Medido antes de creerlo. La forma con :root empata en
   especificidad y gana por orden de archivo; la pelona sigue ahí para los
   portadores que no son :root, como una miniatura. */
:root[data-wall="slate"],
[data-wall="slate"] {
  --wall-a: color-mix(in srgb, var(--n-500) 82%, var(--blue-500));
  --wall-b: color-mix(in srgb, var(--n-300) 78%, var(--blue-300));
  --wall-c: color-mix(in srgb, var(--n-400) 74%, var(--blue-500));
  --wall-d: color-mix(in srgb, var(--n-500) 70%, var(--blue-300));
  --wall-blooms:  var(--wall-blooms-dense);
  --wall-bloom:   var(--wall-bloom-dense);
  --wall-bloom-2: var(--wall-bloom-2-dense);
}

/* ============================================================
   27b · LA RAMPA DEL CIELO — el tapiz deriva con la hora
   ============================================================ */

/* Las ocho variantes de arriba siguen existiendo para quien elija una a mano
   (una miniatura, una página del catálogo, un artifact). Lo que el OS hace hoy
   es otra cosa: NO elige. El fondo deriva a lo largo del día por una curva de
   seis paradas en anillo, declaradas en tokens.css.

   El reparto de trabajo es la parte que vale copiar: el JS no escribe UN SOLO
   COLOR. Escribe dos cosas en `:root` —`data-sky` (en qué parada va) y
   `--sky-t` (0–1, cuánto lleva recorrido hacia la siguiente)— y la MEZCLA la
   hace el CSS con `color-mix`. De ahí salen tres propiedades:

     · La paleta se lee y se afina en tokens.css, junto a todas las demás.
       Cero valores crudos escritos por JS, que es la regla de siempre.
     · Se puede inspeccionar: `data-sky="tarde"` y `--sky-t: .38` en el
       elemento raíz dicen la hora del OS sin abrir un depurador.
     · Se puede congelar. Una prueba pone los dos atributos y el fondo queda
       quieto, sin tener que fingir un reloj.

   `in oklab` y no `in srgb`: entre dos tonos saturados de distinta familia
   —el ámbar del ocaso y el azul de la noche— la interpolación en sRGB pasa por
   un gris sucio a medio camino. Es la única diferencia visible entre las dos, y
   se ve justo en la hora que más se mira.

   Cada parada declara CÓMO SALE de sí misma, no cómo llega: la regla de una
   parada nombra a la siguiente, y la última cierra el anillo con la primera.
   Así agregar una parada es un bloque más y cambiar el vecino de una es una
   línea, no una cacería. */

/* CADA PARADA VA CON SELECTOR DOBLE, y las dos formas hacen falta — es el
   mismo par que `slate` documenta más arriba:

     · La forma PELONA (`[data-sky="alba"]`, 0-1-0) es la que permite que
       cualquier portador del atributo —una miniatura de 40px en el catálogo—
       vuelva a resolver la receta con SUS tonos. Los `var()` de un custom
       property se sustituyen en el elemento que lo declara, así que sin esta
       forma las seis muestras heredarían el gradiente ya resuelto de la raíz
       y saldrían todas iguales.
     · La forma con `:root` (0-2-0) es la que GANA en la raíz. La paleta por
       omisión se declara en `:root:not([data-wall])`, que pesa 0-2-0: con solo
       la forma pelona, la casa le ganaba a la parada y el fondo del OS no se
       movía en todo el día. Medido en carne propia — aplicar `ocaso` a la
       página no cambiaba un píxel del tapiz.

   Empatan en peso con la paleta de la casa y ganan por orden de archivo, que
   es exactamente como `slate` resolvió lo mismo. */
:root[data-sky], [data-sky] { --sky-mix: calc(var(--sky-t, 0) * 100%); }

:root[data-sky="noche"],
[data-sky="noche"] {
  --wall-a: color-mix(in oklab, var(--sky-noche-a), var(--sky-alba-a) var(--sky-mix));
  --wall-b: color-mix(in oklab, var(--sky-noche-b), var(--sky-alba-b) var(--sky-mix));
  --wall-c: color-mix(in oklab, var(--sky-noche-c), var(--sky-alba-c) var(--sky-mix));
  --wall-d: color-mix(in oklab, var(--sky-noche-d), var(--sky-alba-d) var(--sky-mix));
}
:root[data-sky="alba"],
[data-sky="alba"] {
  --wall-a: color-mix(in oklab, var(--sky-alba-a), var(--sky-manana-a) var(--sky-mix));
  --wall-b: color-mix(in oklab, var(--sky-alba-b), var(--sky-manana-b) var(--sky-mix));
  --wall-c: color-mix(in oklab, var(--sky-alba-c), var(--sky-manana-c) var(--sky-mix));
  --wall-d: color-mix(in oklab, var(--sky-alba-d), var(--sky-manana-d) var(--sky-mix));
}
:root[data-sky="manana"],
[data-sky="manana"] {
  --wall-a: color-mix(in oklab, var(--sky-manana-a), var(--sky-mediodia-a) var(--sky-mix));
  --wall-b: color-mix(in oklab, var(--sky-manana-b), var(--sky-mediodia-b) var(--sky-mix));
  --wall-c: color-mix(in oklab, var(--sky-manana-c), var(--sky-mediodia-c) var(--sky-mix));
  --wall-d: color-mix(in oklab, var(--sky-manana-d), var(--sky-mediodia-d) var(--sky-mix));
}
:root[data-sky="mediodia"],
[data-sky="mediodia"] {
  --wall-a: color-mix(in oklab, var(--sky-mediodia-a), var(--sky-tarde-a) var(--sky-mix));
  --wall-b: color-mix(in oklab, var(--sky-mediodia-b), var(--sky-tarde-b) var(--sky-mix));
  --wall-c: color-mix(in oklab, var(--sky-mediodia-c), var(--sky-tarde-c) var(--sky-mix));
  --wall-d: color-mix(in oklab, var(--sky-mediodia-d), var(--sky-tarde-d) var(--sky-mix));
}
:root[data-sky="tarde"],
[data-sky="tarde"] {
  --wall-a: color-mix(in oklab, var(--sky-tarde-a), var(--sky-ocaso-a) var(--sky-mix));
  --wall-b: color-mix(in oklab, var(--sky-tarde-b), var(--sky-ocaso-b) var(--sky-mix));
  --wall-c: color-mix(in oklab, var(--sky-tarde-c), var(--sky-ocaso-c) var(--sky-mix));
  --wall-d: color-mix(in oklab, var(--sky-tarde-d), var(--sky-ocaso-d) var(--sky-mix));
}
:root[data-sky="ocaso"],
[data-sky="ocaso"] {
  --wall-a: color-mix(in oklab, var(--sky-ocaso-a), var(--sky-noche-a) var(--sky-mix));
  --wall-b: color-mix(in oklab, var(--sky-ocaso-b), var(--sky-noche-b) var(--sky-mix));
  --wall-c: color-mix(in oklab, var(--sky-ocaso-c), var(--sky-noche-c) var(--sky-mix));
  --wall-d: color-mix(in oklab, var(--sky-ocaso-d), var(--sky-noche-d) var(--sky-mix));
}

/* El campo de color del panel (§ 33c) recoge la hora igual que el tapiz. Sin
   esto se quedaría en la paleta de la casa mientras el fondo deriva, o sea dos
   horas distintas en la misma pantalla. `inherit` y no una copia de la rampa:
   el valor ya está resuelto en `:root` y bajarlo por herencia es la única
   forma de no tenerlo declarado dos veces. `:where(.r-iris)` de arriba tiene
   especificidad CERO, así que esto le gana sin pelear — y un `data-wall`
   propio en la pieza (0-1-0) le sigue ganando a `:root[data-sky]` porque no
   compiten: aquel declara en el elemento, este en la raíz. */
:root[data-sky] .r-iris:not([data-wall]) {
  --wall-a: inherit;
  --wall-b: inherit;
  --wall-c: inherit;
  --wall-d: inherit;
}

/* ============================================================
   28 · CONFIRMACIÓN DESTRUCTIVA  (la arma ron.js)
   ============================================================ */

/* Borrar es el bote de basura. Al tocarlo se expande EN SU LUGAR y pregunta
   con palabras completas. Nada de confirm() del navegador: no se puede
   estilar, rompe el lenguaje y se ve igual en un OS cuidado que en cualquier
   página. Es la excepción de "la claridad gana": el icono es la entrada, la
   palabra es el compromiso.

     <span class="r-confirm r-confirm--sm" data-confirm="la nota Retro Q3"></span>

   ron.js arma el disparador y el panel; el consumidor solo escucha el evento
   r-confirm, que burbujea.

   ── Por qué el panel va FUERA DE FLUJO ──────────────────────────────────
   En una fila de tabla o en una barra apretada, un control que crece
   reacomoda a todos sus vecinos: el nombre se corta, los números saltan de
   columna y la fila cambia de alto. Así que la caja en reposo es la única
   que existe en el flujo —cuadrada, del tamaño del bote— y el panel es
   `position: absolute` encima. Medido en el showcase, fila de tabla armada
   contra desarmada: 637×46 y 637×46 a 1280; 341×46 y 341×46 a 375. La celda
   del nombre no se mueve (291.46px las dos veces) y el scrollWidth de
   `.r-table-wrap` no cambia (637). El panel mide 111.3×28: cabe dentro de la
   fila con 9px de sobra arriba y abajo.

   De ahí salen dos restricciones que no se ven:
   · El panel mide EXACTAMENTE lo alto que el disparador. Un ancestro con
     overflow-x: auto (`.r-table-wrap`) computa su overflow-y a `auto`
     también, así que un panel más alto que la fila se cortaría por arriba.
   · Se abre hacia el INICIO. El bote vive al final de una fila; crecer
     hacia afuera lo sacaría de la tarjeta. `.r-confirm--open-end` invierte
     el sentido para cuando el bote está al principio de una barra.        */

.r-confirm {
  --cf-h: var(--ctl-md);
  --cf-ico: 17px;
  position: relative;
  display: inline-grid;
  place-items: center;
  /* La caja del flujo, y la única que se mide desde afuera. */
  width: var(--cf-h);
  height: var(--cf-h);
  flex: 0 0 auto;
  vertical-align: middle;
}
.r-confirm--sm { --cf-h: var(--ctl-sm); --cf-ico: 15px; }

/* Reposo: el bote y nada más. */
.r-confirm__t {
  width: 100%;
  height: 100%;
  --ico: var(--cf-ico);
  transition: background var(--d-fast) var(--ease),
              color var(--d-fast) var(--ease),
              opacity var(--d-fast) var(--ease);
}
.r-confirm__t:hover { color: var(--crit); background: var(--crit-bg); }

/* Armado: el disparador se apaga y deja de recibir clics — así un segundo
   clic en el mismo punto cae en Cancelar, que es lo que queda debajo del
   cursor. Un borrado irreversible no se completa sin mover la mano. */
.r-confirm[data-armed] .r-confirm__t { opacity: 0; pointer-events: none; }

/* El panel. Relleno crit sólido: es señal, no acento, y siendo opaco se lee
   igual sobre papel, sobre cockpit y sobre un gradiente, sin una sola regla
   por superficie. La tinta va con --on-crit (oscura fija), que es la misma
   regla de --on-accent sobre el ácido. */
.r-confirm__ask {
  position: absolute;
  top: 50%;
  right: 0;
  translate: 0 -50%;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: var(--s-1);
  height: var(--cf-h);
  width: max-content;
  padding-inline: var(--s-3) 3px;
  border-radius: var(--r-pill);
  background: var(--crit);
  color: var(--on-crit);
  /* Sombra y no hairline: esto sí flota encima del contenido de la fila. */
  box-shadow: var(--e-2);
  /* La cortina se abre hacia el inicio desde la huella del disparador. El
     `round` mantiene la pastilla con sus cantos durante el barrido. */
  clip-path: inset(0 0 0 calc(100% - var(--cf-h)) round var(--r-pill));
  opacity: 0;
  pointer-events: none;
  transition: clip-path var(--d-base) var(--ease-out),
              opacity var(--d-fast) var(--ease);
}
.r-confirm[data-armed] .r-confirm__ask {
  clip-path: inset(0 round var(--r-pill));
  opacity: 1;
  pointer-events: auto;
}

/* Hacia el final. Lo decide ron.js midiendo la huella del panel contra el
   espacio disponible (`data-open="end"`); la clase queda como forzado manual para
   el caso raro en que el consumidor sepa algo que la medición no.

   Y con este sentido los hijos van al revés —lo hace ron.js— porque si no se
   pierde en silencio la propiedad que hace seguro al componente: la huella del
   disparador tiene que quedar debajo de CANCELAR, no debajo del texto. Con el
   orden normal, al abrir hacia el final el cursor se quedaba sobre la pregunta y
   un segundo clic reflejo ya no cancelaba nada. */
.r-confirm--open-end .r-confirm__ask,
.r-confirm[data-open="end"] .r-confirm__ask {
  right: auto;
  left: 0;
  clip-path: inset(0 calc(100% - var(--cf-h)) 0 0 round var(--r-pill));
}
.r-confirm--open-end[data-armed] .r-confirm__ask,
.r-confirm[data-open="end"][data-armed] .r-confirm__ask { clip-path: inset(0 round var(--r-pill)); }
/* Abriendo hacia el final, el relleno se invierte: el aire va del lado del texto
   y el canto pegado al disparador lo ocupa Cancelar. */
.r-confirm--open-end .r-confirm__ask,
.r-confirm[data-open="end"] .r-confirm__ask { padding-inline: 3px var(--s-3); }

/* La pregunta, con palabras completas.

   El tope de ancho es una red, no un diseño: el verbo debe ser corto ("¿Borrar?",
   "¿Borrar todo?") y ron.js avisa en consola cuando llega una frase. Si de todos
   modos llega, se recorta con elipsis en vez de desbordar el panel y romper la
   fila — la frase entera sigue en el `title` y en la etiqueta del grupo, así que
   un lector de pantalla no oye el recorte. 20ch le sobra a cualquier verbo real
   y le queda corto a una oración, que es justo lo que se quiere que se note. */
.r-confirm__q {
  font-size: var(--t-label);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-normal);
  white-space: nowrap;
  max-width: 20ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

.r-confirm__yes,
.r-confirm__no {
  display: grid;
  place-items: center;
  height: calc(var(--cf-h) - 8px);
  aspect-ratio: 1;
  flex: 0 0 auto;
  --ico: var(--cf-ico);
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: inherit;
  transition: background var(--d-fast) var(--ease), transform var(--d-fast) var(--ease);
}
/* El lavado sale de la propia tinta del panel, no de --idle-bg: ese cambia
   con el tema y el relleno crit no. */
.r-confirm__yes:hover,
.r-confirm__no:hover { background: color-mix(in srgb, var(--on-crit) 16%, transparent); }
.r-confirm__yes:active,
.r-confirm__no:active { transform: scale(0.92); }
/* El foco del sistema es ácido; sobre crit sería un segundo acento y encima
   pierde contraste. Aquí el aro es la propia tinta del panel. */
.r-confirm__yes:focus-visible,
.r-confirm__no:focus-visible {
  outline: var(--stroke-2) solid var(--on-crit);
  outline-offset: 1px;
}

/* Movimiento reducido: el panel aparece puesto, sin barrido. La cortina se
   quita por completo —no se acelera— porque un clip-path a 0.01ms deja un
   cuadro con la pastilla recortada a medias. El estado armado se lee igual:
   es un cambio de pieza, no una animación. */
@media (prefers-reduced-motion: reduce) {
  .r-confirm__ask { transition: none; clip-path: none; visibility: hidden; }
  .r-confirm[data-armed] .r-confirm__ask { clip-path: none; visibility: visible; }
  .r-confirm__t { transition: none; }
}

/* ============================================================
   29 · SELECTOR COMPRIMIDO  (la arma ron.js)
   ============================================================ */

/* Comprimido de entrada, expande al tocar. Cinco opciones apiladas ocupan
   cinco renglones para decir un dato; comprimidas ocupan uno y dicen el mismo.

     <div class="r-pick" data-pick="Prioridad"
          data-pick-options="none:Ninguna, low:Baja, med:Media, high:Alta"
          data-pick-value="med"></div>

   DÓNDE NO USARLO: cuando son dos o tres opciones y VER las alternativas es el
   punto —Kanban/Gantt, Preguntar/Capturar—, el segmentado se queda a la vista.
   La frontera es un renglón y tres opciones o menos. Comprimir ahí no ahorra
   espacio, esconde la decisión.

   ── Por qué la lista va en la CAPA SUPERIOR ──────────────────────────────
   El selector vive dentro de un panel que scrollea, y una lista de cinco
   opciones NO puede medir lo que mide su disparador —que es lo que salvó al
   panel del bote de basura de que lo recortaran—. Cualquier ancestro con
   `overflow` distinto de `visible` la cortaría, y en el OS hay varios:
   `.r-table-wrap` (overflow-x, que arrastra el overflow-y a `auto`), `.r-tabs`,
   `.r-toolbar`, `.r-strip` y el propio `.r-card`, que lleva `overflow: hidden`.

   `popover` lo resuelve de raíz: el navegador la promueve a la capa superior,
   fuera del flujo de recorte y fuera de cualquier guerra de z-index, y de
   pasadita trae cierre al tocar fuera y con Escape.

   Lo que `popover` NO trae es la posición —el anclaje de CSS todavía no está
   en las dos máquinas—, así que la mide ron.js contra el rect del disparador.
   Y trae un problema propio: al estar fuera de su caja, la lista ya no está
   sobre la superficie que la contenía, pero en el DOM sigue adentro y hereda
   lo que § 23 haya remapeado. De ahí los --theme-* de tokens.css: la lista se
   viste con el TEMA, no con el contexto del que salió.                     */

.r-pick {
  position: relative;
  display: inline-flex;
  max-width: 100%;
}

.r-pick__t {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  height: 30px;
  max-width: 100%;
  padding-inline: var(--s-3) var(--s-2);
  background: var(--surface-sunk);
  color: var(--ink);
  border: var(--stroke) solid transparent;
  border-radius: var(--r-pill);
  font-size: var(--t-body-sm);
  font-weight: var(--fw-medium);
  transition: background var(--d-fast) var(--ease),
              border-color var(--d-fast) var(--ease);
}
.r-pick__t:hover { border-color: var(--hairline-strong); }
.r-pick__t[aria-expanded="true"] { background: var(--surface); border-color: var(--hairline-strong); }
/* La flecha gira: es la única señal de que esto se abre, y con el rótulo
   afuera no hay palabra que lo diga. */
.r-pick__t .r-ico {
  --ico: 15px;
  color: var(--ink-3);
  transition: rotate var(--d-fast) var(--ease);
}
.r-pick__t[aria-expanded="true"] .r-ico { rotate: 180deg; }

/* El valor se recorta antes de empujar la fila: el selector vive en barras
   apretadas y un valor largo no puede decidir el ancho del panel. */
.r-pick__v {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.r-pick__list {
  /* Reset de las UA styles de [popover]: centrado con inset:0 + margin:auto,
     borde sólido y fondo `canvas`. La posición la escribe ron.js. */
  position: fixed;
  inset: auto;
  margin: 0;
  padding: var(--s-1);
  width: max-content;
  max-width: min(320px, calc(100vw - var(--s-6)));
  max-height: min(300px, 44vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* El TEMA, no el contexto: ver el comentario de arriba y los --theme-* de
     tokens.css. Opaca a propósito —una lista translúcida encima de contenido
     arbitrario es lo único que este sistema no puede hacer legible—. */
  background: var(--theme-surface);
  color: var(--theme-ink);
  border: var(--stroke) solid var(--theme-hairline);
  border-radius: var(--r-md);
  /* Flota de verdad ⇒ sombra, no hairline. */
  box-shadow: var(--e-3);
  opacity: 0;
  translate: 0 -4px;
  transition: opacity var(--d-fast) var(--ease),
              translate var(--d-fast) var(--ease-out),
              overlay var(--d-fast) var(--ease) allow-discrete,
              display var(--d-fast) var(--ease) allow-discrete;
}
.r-pick__list:popover-open { opacity: 1; translate: 0 0; }
@starting-style {
  .r-pick__list:popover-open { opacity: 0; translate: 0 -4px; }
}
/* Sin velo: es un desplegable, no un diálogo. */
.r-pick__list::backdrop { background: transparent; }

.r-pick__o {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  min-height: 32px;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  font-size: var(--t-body-sm);
  color: var(--theme-ink-2);
  white-space: nowrap;
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
.r-pick__o:hover { background: var(--theme-surface-sunk); color: var(--theme-ink); }
/* El foco del teclado NO usa el aro ácido: aquí se mueve un
   aria-activedescendant, no el foco del navegador, así que el resalte lo pinta
   esta clase. Es también el que sigue a las flechas. */
.r-pick__o[data-at] {
  background: var(--theme-surface-sunk);
  color: var(--theme-ink);
  box-shadow: inset 0 0 0 var(--stroke) var(--theme-hairline);
}
.r-pick__o[aria-selected="true"] {
  color: var(--theme-ink);
  font-weight: var(--fw-medium);
}
/* La paloma marca lo elegido. Va al final y con espacio reservado, así que
   mover la selección no reacomoda el ancho de la lista. */
.r-pick__o .r-ico {
  --ico: 15px;
  margin-left: auto;
  opacity: 0;
  color: var(--theme-ink);
}
.r-pick__o[aria-selected="true"] .r-ico { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* La lista aparece puesta. La flecha tampoco gira: a 0.01ms el giro deja un
     cuadro a medio camino y se lee como un glifo torcido. */
  .r-pick__list { transition: none; translate: none; }
  .r-pick__t .r-ico { transition: none; }
}

/* ============================================================
   30 · CAPTURA EN LÍNEA  (la arma ron.js)
   ============================================================ */

/* La pieza hermana del bote: aquel confirma sin diálogo, esta captura un nombre
   sin diálogo. Es el patrón del Inbox de Home —campo en línea + Enter— subido al
   sistema, con dos diferencias: arranca COMPRIMIDO (un disparador que se expande,
   como manda CLAUDE.md) y la validación deja de repetirse por consumidor.

     <span class="r-capture" data-capture="¿Qué habilidad?"></span>

   El panel va en la CAPA SUPERIOR (`popover`) y no en un absolute como el del
   bote, y la razón es la misma que en .r-pick: un campo de texto necesita ancho
   de verdad —no puede medir lo que mide su disparador— así que cualquier ancestro
   que recorte lo cortaría, y en el OS hay cinco. De pasada trae Escape y cierre
   al tocar fuera. La posición la mide ron.js (geometría compartida con .r-pick).  */

.r-capture {
  position: relative;
  display: inline-grid;
  place-items: center;
  /* La caja del flujo es SOLO el disparador y nunca cambia: es lo que garantiza
     que abrir el campo no reacomode la fila. */
  width: var(--ctl-sm);
  height: var(--ctl-sm);
  flex: 0 0 auto;
  vertical-align: middle;
}

.r-capture__t { width: 100%; height: 100%; --ico: 15px; }
.r-capture__t:hover { color: var(--ink); }

/* El `display` va en :popover-open, NO aquí, y es la única forma correcta:
   declararlo en la base GANA a la regla del navegador
   `[popover]:not(:popover-open) { display: none }` —misma especificidad, y la
   del autor manda—, así que el panel cerrado se queda con CAJA REAL en el
   layout, invisible e `inert`. Medido antes del arreglo, en carga fresca a
   1280: Workspace y Relaciones daban `scrollWidth 1376` contra `clientWidth
   1280` y `scrollTo(600,0)` movía 96px de verdad —barra de scroll horizontal
   en un OS que no debería tener ninguna—, y en Growth el panel caía en
   `x = -13` y ensuciaba cualquier plomada medida.

   La prueba de que este es el arreglo está en la pieza hermana: .r-pick__list
   NO declara `display` en su base y nunca tuvo el bug. El
   `transition: display allow-discrete` de abajo sigue haciendo la salida. */
.r-capture__panel {
  position: fixed;
  inset: auto;
  margin: 0;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1);
  width: max-content;
  /* Ancho de captura cómodo, y que no se pase de la pantalla angosta. */
  max-width: calc(100vw - var(--s-6));
  /* El TEMA, no el contexto: en la capa superior el panel ya no está sobre la
     superficie que lo contenía, pero en el DOM sigue adentro y heredaría lo que
     § 23 haya remapeado. Misma razón que la lista de .r-pick. */
  background: var(--theme-surface);
  color: var(--theme-ink);
  border: var(--stroke) solid var(--theme-hairline);
  border-radius: var(--r-pill);
  box-shadow: var(--e-3);
  opacity: 0;
  translate: 0 -4px;
  transition: opacity var(--d-fast) var(--ease),
              translate var(--d-fast) var(--ease-out),
              overlay var(--d-fast) var(--ease) allow-discrete,
              display var(--d-fast) var(--ease) allow-discrete;
}
.r-capture__panel:popover-open { display: flex; opacity: 1; translate: 0 0; }
@starting-style {
  .r-capture__panel:popover-open { opacity: 0; translate: 0 -4px; }
}
.r-capture__panel::backdrop { background: transparent; }

.r-capture__in {
  width: 15rem;
  height: 32px;
  background: var(--theme-surface-sunk);
  color: var(--theme-ink);
}
.r-capture__in::placeholder { color: var(--theme-ink-3); }
.r-capture__in:focus { background: var(--theme-surface); }

.r-capture__ok {
  display: grid;
  place-items: center;
  height: 32px;
  aspect-ratio: 1;
  flex: 0 0 auto;
  --ico: 16px;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--on-accent);
  transition: background var(--d-fast) var(--ease),
              opacity var(--d-fast) var(--ease),
              transform var(--d-fast) var(--ease);
}
.r-capture__ok:hover { background: var(--accent-press); }
.r-capture__ok:active { transform: scale(0.92); }
/* Vacío no crea, y se VE: el botón se apaga en vez de dejar que el usuario lo
   descubra apretándolo. Solo espacios cuenta como vacío. */
.r-capture__ok[disabled] { opacity: 0.32; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .r-capture__panel { transition: none; translate: none; }
}

/* ============================================================
   31 · UTILIDADES
   ============================================================ */

.r-t-ink { color: var(--ink); }   .r-t-2 { color: var(--ink-2); }  .r-t-3 { color: var(--ink-3); }
/* El ácido es tinta solo sobre oscuro: --accent-ink lo resuelve por
   superficie (ver tokens.css). Cualquier pieza que ya use esta utilidad se
   corrige sola; el acento en claro lo carga .r-metric--mark. */
.r-t-accent { color: var(--accent-ink); }
.r-t-crit { color: var(--crit); } .r-t-ok { color: var(--ok); }
/* La tercera del juego, y faltaba. `app/settings.js` la pidió con un
   `style` en línea y lo dejó anotado en vez de escribir un override local
   de `.r-*` — que es lo correcto y por eso el hueco se cierra aquí. */
.r-t-warn { color: var(--warn); }
.r-bg-accent { background: var(--accent); color: var(--on-accent); }
.r-tnum { font-variant-numeric: tabular-nums; }
.r-nowrap { white-space: nowrap; }
.r-abs-tr { position: absolute; top: var(--s-4); right: var(--s-4); }
.r-abs-br { position: absolute; bottom: var(--s-4); right: var(--s-4); }
.r-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ============================================================
   32 · IA — lo que existe PORQUE HAY UN MODELO DETRÁS
   Una familia, no piezas sueltas. Hoy el OS tiene seis
   superficies contra el mismo motor (Home, el invocador ⌘K,
   LEVER, las dos de Growth y Ajustes) y cada una se dibujaba su
   propia versión de "estoy pensando". Eso no es variedad, es la
   señal de que faltaba el componente.
   ============================================================ */

/* ---- 32a · .r-beam — el aura de "está trabajando" ----

   No es un canto teñido: es LUZ. La diferencia no es de intensidad, es de
   estructura. Un solo tono alrededor de una caja se lee siempre como
   borde brillante, por fuerte que se ponga; la luz real llega de varios
   puntos y con varios matices. De ahí las TRES fuentes desaturadas
   —turquesa, rosa polvo y violeta— que sangran hacia adentro, se
   disuelven antes del centro y DAN LA VUELTA por el canto, repartidas a
   120° sobre la misma elipse.

   Que viajen las tres, y no una, es la diferencia entre que se lea una
   vuelta y que se lea un punto paseando: con una móvil y dos clavadas lo
   que se ve es una mancha recorriendo dos manchas quietas. Repartidas, en
   cada momento hay luz en tres lugares del canto, el conjunto gira, y de
   paso cada esquina va cambiando de matiz.

   Los tres colores NO son una paleta nueva: son las primitivas que ya
   sostienen la aurora y el wallpaper (--teal-500, --rose-300,
   --violet-500). Y por eso el default dejó de ser el ácido: la regla de
   un solo acento por vista es sobre SEÑAL —el ácido marca el dato que
   manda— y un aura desaturada es luz ambiente, la categoría del
   wallpaper, no la del acento. El ácido se queda libre para lo que sí
   tiene que mandar en la vista.

   La receta es la del wallpaper, dicha en chico: el corazón de cada
   radial queda FUERA de la caja y solo entra su falda.

   Y de ahí sale el número que de verdad decide si esto se lee como luz o
   como lavado: EL TAMAÑO DE CADA RADIAL, no su opacidad. La primera
   versión les puso 66–88% de la caja apagando hasta el 68–72%, y tres de
   esas encimadas no dejan un píxel sin teñir: la tarjeta salía pintada de
   pastel y el cuerpo dejaba de ser oscuro. Bajar la opacidad no arregla
   eso — una mancha grande y tenue sigue siendo una mancha grande.

   Hoy cada fuente mide 46–52% del ANCHO y se apaga en el 64–66% de su
   propio radio, o sea que alcanza como un TERCIO de la caja: lo mismo que
   la referencia. Y ese tercio no es un gusto, es el borde de una
   propiedad que sí se puede afirmar — el píxel central es EL MISMO con
   aura y sin ella, porque ahí las tres evalúan a `transparent`.

   Con las fuentes viajando eso hay que sostenerlo EN TODO EL CICLO, no en
   una pose. Barrido de 360° en pasos de 2°, con las tres fuentes contra
   cinco puntos de la caja, el resultado es el par que define la pieza:

     · el CENTRO nunca se pinta — el peor instante deja 1.41× de sobra
       sobre el corte (1.13× en el botón chico, que lleva una sola fuente)
     · los cuatro MEDIOS DE CANTO sí se pintan, cada uno en su momento

   O sea: la luz recorre el perímetro entero y el cuerpo no se entera. Eso
   es lo que hay que conservar si alguien toca estos números:

     > El centro de la caja se queda casi negro y el texto se lee con el
     > mismo contraste que sin el efecto. Si al terminar la caja se ve más
     > CLARA que sin beam, está mal.

   La forma de juzgarlo tampoco es a ojo suelto: se ponen dos cajas iguales
   lado a lado, una con [data-beam-on] y otra sin él, y se comparan.

   Tres propiedades que salieron de la técnica y no de una regla aparte:

   · Se adapta al radio solo. La capa hereda border-radius, así que el
     mismo .r-beam sirve en un botón redondo y en una caja --r-xl sin que
     el consumidor declare nada.
   · No pide overflow: hidden. El fondo de la capa ya se recorta contra su
     propio radio y NADA sale del elemento — la caja de prompt no tiene
     que recortar su contenido para llevar el aura.
   · Va DEBAJO del contenido, no encima. La capa se hunde con z-index: -1
     dentro del contexto de apilamiento del propio elemento, que en el
     orden de pintado cae después del fondo y del borde del elemento y
     antes de su texto. Por eso el hairline se sigue viendo por debajo del
     aura y el contraste del contenido no se mueve.
   · Apagado es apagado. Sin [data-beam-on] la regla ni siquiera declara
     `content`, o sea que el pseudoelemento NO EXISTE: cero capa
     compuesta, cero animación corriendo, cero repintado. Importa porque
     esto vive en un botón que está siempre en pantalla.

   Y la vuelta no gira una capa —girar un rectángulo redondeado lo
   deforma—: mueve el ORIGEN de las tres luces sobre una elipse por fuera
   de la caja, con cos()/sin() de un @property de tipo <angle>. El punto
   caliente recorre el perímetro sin que la forma cambie nunca, y por eso
   funciona igual en un círculo de 38px y en una caja de 720.

   El contrato:
     .r-beam                        la clase
     data-beam-on                   prendido (sin el atributo es inerte)
     data-beam-busy                 EL MODELO ESTÁ TRABAJANDO. Lo pone el
                                    consumidor; lo que pasa al ponerlo lo
                                    decide el sistema (ver 32a-bis).
     data-beam="rotate"             el DEFAULT: las tres luces dan la
                                    vuelta. El nombre existe para poder
                                    decirlo, no hace falta escribirlo.
     data-beam="pulse"              la excepción: se clavan y solo laten
     data-beam-tight                lo pone ron.js midiendo, no el
                                    consumidor: debajo de 140px el aura
                                    degrada a una sola fuente
     --beam-ink                     colapsa las tres fuentes a UN color
     --beam-strength                0–1; default .7                       */

@property --beam-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

/* Especificidad CERO a propósito: la capa necesita un ancestro posicionado,
   pero un consumidor que ya se posiciona solo —el invocador flotante es
   absoluto— no puede perder su posición por ponerse un aura. Con :where()
   cualquier regla del consumidor gana sin pelear. */
:where(.r-beam) { position: relative; }

/* El contexto de apilamiento propio es lo que hace que el z-index: -1 de la
   capa signifique "debajo del texto de ESTE elemento" y no "debajo de la
   tarjeta que lo contiene". */
.r-beam[data-beam-on] { isolation: isolate; }

.r-beam[data-beam-on]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;

  /* Las tres fuentes. --beam-ink las colapsa a una sola: es la salida para
     quien quiera un aura de un color concreto —el invocador la usa con
     --ok mientras el modelo escribe— sin que el default tenga que elegir. */
  --beam-a: var(--beam-ink, var(--teal-500));
  --beam-b: var(--beam-ink, var(--rose-300));
  --beam-c: var(--beam-ink, var(--violet-500));
  --beam-o: calc(var(--beam-strength, .7) * var(--beam-gain));

  /* Las TRES fuentes dan la vuelta, repartidas a 120° sobre la misma
     elipse. Que viajen las tres es lo que hace que el recorrido se lea
     como una vuelta: con una sola móvil y dos clavadas, lo que se ve es
     un punto paseando entre dos manchas quietas, no luz circulando.
     Repartidas, en cada momento hay luz en tres lugares del canto y el
     conjunto gira — y de paso cada esquina va cambiando de matiz. */
  --bx1: calc(50% + cos(var(--beam-angle))          * 62%);
  --by1: calc(50% + sin(var(--beam-angle))          * 78%);
  --bx2: calc(50% + cos(var(--beam-angle) + 120deg) * 62%);
  --by2: calc(50% + sin(var(--beam-angle) + 120deg) * 78%);
  --bx3: calc(50% + cos(var(--beam-angle) + 240deg) * 62%);
  --by3: calc(50% + sin(var(--beam-angle) + 240deg) * 78%);

  background:
    radial-gradient(52% 86% at var(--bx1) var(--by1),
      color-mix(in oklab, var(--beam-a) var(--beam-core), transparent) 0%,
      color-mix(in oklab, var(--beam-a) var(--beam-skirt), transparent) 34%,
      transparent 64%),
    radial-gradient(48% 82% at var(--bx2) var(--by2),
      color-mix(in oklab, var(--beam-b) var(--beam-core), transparent) 0%,
      color-mix(in oklab, var(--beam-b) var(--beam-skirt), transparent) 34%,
      transparent 64%),
    radial-gradient(46% 84% at var(--bx3) var(--by3),
      color-mix(in oklab, var(--beam-c) var(--beam-core), transparent) 0%,
      color-mix(in oklab, var(--beam-c) var(--beam-skirt), transparent) 36%,
      transparent 66%);

  opacity: var(--beam-o);
  /* El periodo se lee de una variable y no del token directo para que el
     estado de trabajo lo cambie sin reescribir la taquigrafía entera: con
     `animation: a X, b Y` puesta dos veces, cambiar un periodo obliga a
     repetir el otro y a que las dos copias no se separen nunca. */
  --beam-orbit: var(--d-orbit);
  animation:
    r-beam-orbit var(--beam-orbit) linear infinite,
    r-beam-breathe var(--d-breathe) var(--ease) infinite;
}

/* ---- 32a-bis · data-beam-busy — el mismo aura, trabajando ----

   El estado subió al sistema y no se quedó en la app, por la razón de
   siempre: hay dos superficies diciendo el MISMO hecho —"el modelo está
   escribiendo"— y si cada una elige su color y su velocidad, el OS deja de
   verse como un programa. El consumidor declara el ESTADO; qué se ve al
   declararlo lo decide esta pieza.

   Antes esto era `data-busy` en `.os-composer` (vocabulario de la app) y
   sólo cambiaba el color; el rayo del rail decía lo mismo prendiendo y
   apagando el aura entera. Cuando el rayo pasó a estar encendido en reposo
   —como la caja— esa forma de decirlo se quedó sin sitio, y en vez de
   inventar una segunda señal se reusó la que la caja ya había resuelto.

   Dos cosas cambian, y ninguna es la fuerza:

   · EL COLOR. La terna ambiente colapsa a `--ok`, el mismo de
     `.r-status--live` y el de la burbuja en vivo del chat: un hecho, un
     color. Deliberadamente NO el ácido — el acento es uno por vista y le
     toca al dato que manda, no a un indicador de proceso.
   · EL RITMO. La vuelta pasa de ambiente a urgencia (--d-orbit →
     --d-orbit-fast, 2.7×). El respiro NO cambia: la opacidad ya está
     diciendo "encendido" y acelerarla también convierte urgencia en
     nervios.

   Lo que NO cambia es `--beam-strength`, y está medido del lado de la caja
   de prompt (os.css): subirla dejaba el placeholder por debajo de 3:1 en
   tema claro para comprar un brillo que el cambio de color ya estaba
   dando. El estado nunca cuesta contraste.

   LA COSTURA la cose ron.js, no el CSS. Cambiar `animation-duration` en
   vivo conserva el tiempo transcurrido y recalcula el avance contra el
   periodo nuevo: con la pestaña abierta un rato, el salto de fase llega a
   ser de media vuelta y la luz se teletransporta al canto de enfrente
   justo cuando el modelo arranca. `beamShift()` recuerda el periodo viejo,
   saca el avance con él y lo devuelve con el nuevo — medido: 0° de salto
   contra 121° al arrancar y 162° al terminar. Sin ron.js el ritmo igual
   cambia —esto es CSS— y lo único que se pierde es la continuidad del
   brinco. */
.r-beam[data-beam-on][data-beam-busy]::after {
  --beam-ink: var(--ok);
  --beam-orbit: var(--d-orbit-fast);
}

/* Respira en su sitio: las tres fuentes se clavan en tres esquinas y solo
   late la opacidad. NO es el default, y su caso propio es SEMÁNTICO, no
   geométrico:

     el recorrido dice QUE ALGO AVANZA; el latido dice QUE ALGO ESTÁ
     ENCENDIDO.

   Son dos estados distintos. Y ya no hay que imaginarse cuándo el OS
   querría los dos: `data-beam-busy` separa "trabajando" de "encendido"
   SIN cambiar de modo —cambia color y ritmo— así que el recorrido sirve
   para los dos estados y `pulse` se queda para la pieza que de verdad
   sólo quiere decir "estoy aquí". Como el estado normal de esta pieza es
   el recorrido, ése es el default.

   Lo que NO es argumento, y conviene dejarlo escrito para que nadie lo
   repita: se pensó que en una caja muy ancha y baja (720×56) la elipse
   quedaría tan aplastada que la vuelta no se leería. Medido en captura,
   ocho momentos del ciclo sobre esa caja exacta: la luz barre el largo de
   la barra y se lee perfectamente. La forma de la caja no decide esto.   */
.r-beam[data-beam-on][data-beam="pulse"]::after {
  --bx1: 8%;   --by1: -10%;
  --bx2: 97%;  --by2: -6%;
  --bx3: 12%;  --by3: 110%;
  animation: r-beam-breathe var(--d-breathe) var(--ease) infinite;
}

/* Chico: tres fuentes en 38px no son tres, son una mancha sucia. Debajo de
   cierto ancho el aura degrada a una fuente que va del turquesa al rosa
   dentro del mismo radial, más su CONTRARIA a media fuerza: siguen
   habiendo dos matices —que es lo que la hace leerse como luz— sin tres
   corazones peleándose el mismo píxel, y con un eje que gira en vez de un
   punto suelto (el porqué, en el bloque de la órbita circular, abajo).

   Quien pone el atributo es ron.js, midiendo: el consumidor no elige el
   tamaño de su propio efecto. Sin ron.js el aura queda en su forma de
   tres fuentes, que en chico se ve más pobre pero nunca rota. */
.r-beam[data-beam-on][data-beam-tight]::after {
  background:
    radial-gradient(58% 58% at var(--bx1) var(--by1),
      color-mix(in oklab, var(--beam-a) 96%, transparent) 0%,
      color-mix(in oklab, var(--beam-a) 62%, transparent) 34%,
      color-mix(in oklab, var(--beam-b) 28%, transparent) 60%,
      transparent 88%),
    radial-gradient(46% 46% at var(--bx2) var(--by2),
      color-mix(in oklab, var(--beam-b) 44%, transparent) 0%,
      color-mix(in oklab, var(--beam-a) 20%, transparent) 40%,
      transparent 80%);
}

/* ---- La órbita chica es CIRCULAR, y esto es lo que arregla la mancha ----

   La elipse del default (62% del ancho, 78% del alto) está calibrada para
   una caja ancha y baja: reparte la luz por el largo de la barra. En un
   botón CUADRADO de 38 esa misma elipse es el bug. Medido sobre 38×38 con
   la fuente de antes: en el costado el centro del radial queda a 23.6px del
   centro de la caja y la falda entra 16px; arriba y abajo queda a 29.6px y
   entra 10px. O sea que la misma luz es casi el doble de grande a los lados
   que en las puntas — y eso, dando vueltas, no se lee como algo que
   circula: se lee como una mancha que crece, se encoge y vuelve a crecer.
   Ése era el "punto paseando" por el que este botón declaraba `pulse` como
   excepción, y no venía de tener UNA sola fuente: venía de la órbita.

   Con la órbita circular (72% en los dos ejes) la falda entra los mismos
   11px en cualquier punto del recorrido y el barrido queda parejo. De paso
   el margen del centro deja de depender del ángulo: la fuente vive a 27.4px
   del centro y se apaga a los 19.4, o sea 1.41× de sobra en TODO el ciclo,
   contra el 1.13× del peor instante de la elipse. La propiedad que define
   la pieza —el píxel central es el mismo con aura y sin ella— sale más
   firme, no más débil.

   La segunda fuente, a 180° y a menos de la mitad de fuerza, es lo que
   convierte el barrido en una VUELTA: con una sola, en cada instante hay
   un lado encendido y tres apagados, y el ojo la lee como un objeto que se
   mueve encima del botón. Con la contraria puesta hay un EJE de luz que
   gira, que es la misma razón por la que el default lleva tres y no una.
   Dos y no tres porque a 38px la tercera vuelve a empastar: 120° en este
   diámetro son 20px entre corazones y las faldas se encuentran; 180° son
   los 38 completos.

   No aplica a `pulse`, que clava sus fuentes a mano. */
.r-beam[data-beam-on][data-beam-tight]:not([data-beam="pulse"])::after {
  --bx1: calc(50% + cos(var(--beam-angle))          * 72%);
  --by1: calc(50% + sin(var(--beam-angle))          * 72%);
  --bx2: calc(50% + cos(var(--beam-angle) + 180deg) * 72%);
  --by2: calc(50% + sin(var(--beam-angle) + 180deg) * 72%);
}

@keyframes r-beam-breathe {
  0%, 100% { opacity: calc(var(--beam-o) * 0.42); }
  50%      { opacity: var(--beam-o); }
}
@keyframes r-beam-orbit {
  to { --beam-angle: 1turn; }
}

/* El vidrio es oscuro en los DOS temas, así que ahí el aura no necesita
   ganancia — la misma excepción que ya hacen estas familias con
   --accent-ink unas líneas arriba.

   Aquí había además un caso especial para .r-liquid, que gastaba sus dos
   pseudoelementos y obligaba al aura a pedirle prestado el ::after y a
   mudarle el brillo especular. Con esa familia fuera del sistema, el caso
   especial se fue con ella: .r-frost no usa pseudos y el ::after está
   libre. Se cumplió lo que se esperaba — la pieza se simplificó sola. */
.r-frost, .r-panel, .r-glass {
  --beam-gain: 1;
}

/* ---- 32b · .r-caret — el cursor del stream ----
   El cuadrito que parpadea mientras el modelo escribe. Es el mismo dibujo
   que el punto de .r-status--live —7px, radio 1px, la misma @keyframes—
   con otro color y otro periodo; tenerlo dos veces era tener dos
   definiciones de "vivo".

   Va en --accent-ink y no en --accent crudo por la regla de siempre: el
   ácido es tinta solo sobre oscuro. En cockpit sale ácido; sobre papel
   sale tinta, que es exactamente lo que se espera de un cursor. */
.r-caret {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 1px;
  background: var(--accent-ink);
  vertical-align: baseline;
  animation: r-pulse var(--d-pulse-fast) var(--ease) infinite;
}

/* ---- 32c · .r-stream — la seña de vida con su cola cruda ----
   Mientras el modelo arma algo que NO se va a leer de corrido —una
   propuesta, un plan, un examen— lo que se muestra no es el texto: es que
   sigue habiendo texto. La cola va en monoespacio, recortada, y se
   desvanece por arriba: el fundido dice "esto sigue" sin una leyenda que
   lo explique.

   Es literalmente el mismo bloque en dos módulos (.lv-live en LEVER y
   .gw-prop en Growth, declaración por declaración iguales salvo el alto).
   El canto punteado a la izquierda es la firma: dice "provisional". */
.r-stream {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-3) 0 var(--s-3) var(--s-4);
  border-left: var(--stroke-2) dashed var(--hairline-strong);
}
.r-stream__h {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
}
.r-stream__raw {
  margin: 0;
  min-height: var(--s-5);
  max-height: var(--s-10);
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: var(--t-body-sm);
  color: var(--ink-3);
  overflow-wrap: anywhere;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 45%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 45%);
}
.r-stream--tall .r-stream__raw { max-height: var(--s-12); }

/* ---- 32d · .r-engine — el estado del motor ----
   "No hay modelo", "no conecta", "se cayó a medio stream". Seis
   superficies lo decían con seis clases y seis frases distintas.

   Aquí SÍ van palabras completas, y no es una excepción a la regla de no
   poner texto: es la regla de la claridad. Un estado de error y una
   acción irreversible se dicen con palabras — y este estado además tiene
   una salida, que va al final del renglón. */
.r-engine {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  margin: 0;
  max-width: var(--measure);
  color: var(--ink-2);
  font-size: var(--t-body-sm);
}
.r-engine > .r-status { flex: 0 0 auto; }
.r-engine__t { margin: 0; }
/* La salida —"Ajustes", "Reintentar"— se va al final del renglón. */
.r-engine__a { margin-left: auto; flex: 0 0 auto; }

/* ---- 32e · .r-run — el mismo botón corre y detiene ----
   Cinco superficies, tres marcados distintos: uno mutaba el botón de
   enviar, otro tenía un botón aparte con icono, y los dos de Growth
   ponían la palabra sola. Un gesto, un componente.

   El contenido lo escribe ron.js desde los data-*, así que el icono y la
   etiqueta accesible no se pueden contradecir — mismo criterio que
   .r-confirm y .r-capture. Si el botón además lleva .r-beam, ron.js le
   prende el resplandor mientras corre: la señal de trabajo es la misma en
   todo el OS. */
.r-run { position: relative; }
.r-run[data-run="busy"] { --beam-strength: 0.85; }
.r-run__t { white-space: nowrap; }

/* ---- 32f · .r-seeing — qué tiene enfrente el agente ----

   «viendo · Workspace». Existe PORQUE hay un modelo detrás —es el chip de
   contexto que CLAUDE.md ya nombraba en esta familia— así que vive aquí y
   no en la página que lo consume: en cuanto dos superficies escriben su
   propia versión, una dice "viendo" y otra "contexto: " y el OS deja de
   sonar a un solo programa.

   Es la única etiqueta que se gana su lugar en pantalla, y se lo gana por
   la excepción de la claridad: sin ella no sabes qué sabe el agente, y eso
   no se puede deducir mirando. Dos palabras, 11px, en la tinta de lo
   secundario; el detalle —qué registro exactamente— vive en el `title`.

   TRES COSAS QUE LA PIEZA DECIDE, y no el consumidor:

   · CUÁNDO NO SE DICE NADA. Sin `data-seeing` la pieza no ocupa caja
     (`display: none`). No es un caso borde: es el estado normal del OS sin
     panel abierto, donde lo único que el agente tiene enfrente es la
     conversación que ya estás viendo, y decirlo es el clutter de lo obvio.
     Así el consumidor pone y quita UN atributo en vez de acordarse de
     esconderla.
   · LA REDACCIÓN. El «viendo · » lo escribe ron.js; el consumidor da el
     nombre del módulo. Mismo criterio que .r-run y .r-confirm: una cadena,
     tres lugares (texto, `title`, etiqueta accesible), imposible que se
     contradigan.
   · QUE APAREZCA SIN SALTAR. Sube 2px al entrar, con las curvas del
     sistema, porque aparece en el mismo gesto en que el panel entra.

   El icono va DELANTE y es el del módulo: a esa distancia el dibujo se
   reconoce antes que la palabra, que es justo lo que se quiere de una
   etiqueta que no debe robar atención. */
.r-seeing {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  font-size: var(--t-micro);
  color: var(--ink-3);
  white-space: nowrap;
  min-width: 0;
  animation: r-seeing-in var(--d-base) var(--ease) both;
}
/* Sin el atributo no hay nada que decir, y entonces no hay caja. */
.r-seeing:not([data-seeing]) { display: none; }
.r-seeing__t {
  overflow: hidden;
  text-overflow: ellipsis;
}
@keyframes r-seeing-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: none; }
}

/* ---- Movimiento reducido: quieto, NO borrado ----
   La regla global de arriba acelera todo a 0.01ms, y para una animación
   `infinite` eso no es quietud: es un ciclo corriendo a ciegas que deja
   la opacidad donde caiga. Estas tres se apagan de verdad y se quedan en
   su valor pleno, porque la señal de "está trabajando" tiene que seguir
   leyéndose sin movimiento. */
@media (prefers-reduced-motion: reduce) {
  .r-beam[data-beam-on]::after,
  .r-caret,
  .r-status--live::before {
    animation: none !important;
    opacity: var(--beam-o, 1);
  }
}

/* ============================================================
   33 · EL CHASIS AI-FIRST
   El panel flotante, los orbes y el campo de color detrás.

   Suben al sistema y no se quedan en el shell por dos razones,
   y la segunda es la que de verdad manda:

   · Los SEIS módulos y Ajustes abren en el mismo panel. Siete
     consumidores es la definición de pieza del sistema.
   · Las tres son UNA geometría, no tres piezas que se juntan. La
     reserva del chasis sale del orbe; el panel se detiene en la
     reserva; el campo de color se dibuja contra la caja del panel.
     Repartidas entre tres archivos, mover el orbe deja el panel
     encimado y el color fuera de sitio, y el error no se ve hasta
     que alguien abre el OS en otra pantalla.

   ── EL CONTRATO CON EL CONSUMIDOR, en cuatro renglones ──────

   1. El HOST es el elemento que contiene a los tres y va
      `position: relative`. Ahí es donde el sistema escribe su
      estado (`data-flyout-open`, `data-flyout-resizing`) y el
      ancho de hoy (`--flyout-w`), y de ahí lo leen los tres.
   2. `.r-flyout` es HIJO DIRECTO del host (así lo encuentra
      ron.js sin que nadie declare nada).
   3. Cerrado es el default: sin `data-flyout-open` en el host el
      panel no se ve y no captura el ratón.
   4. El centro no es del sistema —cada shell arma su
      conversación— pero su geometría sí está dicha aquí, para que
      no se invente dos veces:

        · en reposo
            padding-inline: var(--flyout-pad)
                            calc(var(--flyout-rail) + var(--flyout-air));
        · con el panel abierto, bajo `[data-flyout-open]`
            padding-inline-end:
              calc(var(--flyout-rail) + var(--flyout-w) + var(--flyout-air));

      Y una advertencia que este bloque se ganó: NO se abre un
      comentario dentro de otro. Los de CSS no anidan, así que el
      cierre de adentro cierra el de afuera, el resto del párrafo se
      vuelve CSS inválido y el parser, al recuperarse, se come la
      REGLA SIGUIENTE. Aquí se comió el @property de abajo, y el
      único síntoma visible fue un valor que ron.js leía como texto
      en vez de píxeles — con el respaldo tapando el hueco.

      La transición va en `padding` y no en `width` a propósito: así
      el contenido se remide y el texto refluye MIENTRAS el panel
      entra, en vez de reacomodarse de golpe al final.
   ============================================================ */

/* ron.js necesita LEER --flyout-chrome como número, y un custom property
   sin registrar devuelve a getComputedStyle el texto `calc(…)` tal cual, no
   px. Registrarlo como <length> es lo que lo vuelve legible desde el script
   sin copiar la suma a mano en dos idiomas. Es el mismo recurso que ya usa
   --beam-angle en § 32a, por el mismo motivo: que el valor exista computado
   y no solo sustituido. */
@property --flyout-chrome {
  syntax: "<length>";
  inherits: true;
  initial-value: 120px;
}

/* ---- 33a · .r-flyout — el panel flotante ----

   Un módulo abre AQUÍ, nunca a pantalla completa, y el centro sigue vivo
   a su izquierda. Eso es lo que hace que no sea `--z-panel`: no toma la
   pantalla, no hay velo, se puede seguir escribiendo en el prompt con el
   panel abierto. Vive en --z-flyout, DEBAJO del chasis, y el porqué está
   escrito en la escala de capas de tokens.css.

   FLOTA, y eso es geometría, no adjetivo: sus cuatro esquinas se despegan
   —arriba, abajo y a la derecha por --flyout-gap— y su canto derecho se
   detiene en --flyout-rail, o sea ANTES de la columna de orbes. Nunca los
   toca ni pasa por debajo.

   ES PAPEL TRANSLÚCIDO DEL TEMA (--surface-frost), la misma familia que
   .r-card, y no vidrio oscuro fijo (.r-frost). La pregunta se cerró
   midiéndola en la maqueta: el vidrio oscuro se ve bien en cockpit y en
   tema claro convierte al panel en una losa negra al lado de un centro de
   papel — el OS deja de leerse como un solo programa. Las dos son mate; la
   familia brillante se borró del sistema (§ 22).

   Y por eso necesita .r-iris detrás (§ 33c): papel translúcido sobre un
   lienzo plano no es vidrio, es un rectángulo gris. Ese era el síntoma que
   hacía elegir papel opaco — no era el vidrio, era el vacío.

   LA SOMBRA es la excepción que el propio sistema declara ("se reserva para
   lo que de verdad flota"): esto flota, con contenido pasando por debajo, y
   sin ella en tema claro el papel se confunde con el fondo del centro.

   El ancho lo escribe ron.js en el host. El respaldo de tokens.css existe
   para que el panel tenga caja aunque el script no haya corrido: una pieza
   que sin JS mide 0 se ve como un bug del CSS. */
.r-flyout {
  position: absolute;
  top: var(--flyout-gap);
  bottom: var(--flyout-gap);
  right: var(--flyout-rail);
  width: var(--flyout-w);
  z-index: var(--z-flyout);
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* El cuerpo pone el suyo: la cabecera y el contenido no llevan el mismo. */
  padding: 0;
  overflow: hidden;
  border-radius: var(--r-lg);
  /* EL GRADO ES UNA PERILLA, no un segundo componente: el papel del tema es el
     default y --flyout-frost lo sustituye si alguien encendió el grado delgado
     (ver el bloque de abajo). Con la perilla apagada esta declaración es
     idéntica a la de siempre.

     Y las capas interiores viajan con el papel, porque son el mismo vidrio: un
     pozo opaco dentro de un panel delgado se lee como un parche pegado encima,
     no como el fondo de un campo. El fallback va a los alias --theme-* de
     tokens.css y NO a --surface-sunk: un custom property que se cite a sí mismo
     en su propio fallback es un ciclo, y un ciclo deja la propiedad SIN VALOR
     —o sea todos los campos del panel transparentes— en vez de avisar.

     --surface se queda fuera del remapeo A PROPÓSITO: es el piso del campo que
     se está editando (.r-input:focus). Un campo en el que estás escribiendo
     gana con piso opaco, y es la única capa de adentro que lo gana. */
  background: var(--flyout-frost, var(--surface-frost));
  --surface-sunk: var(--flyout-sunk,  var(--theme-surface-sunk));
  --surface-2:    var(--flyout-raise, var(--theme-surface-2));
  /* CERRADO NO PINTA. backdrop-filter y la sombra se prenden SOLO al
     abrir: en WebKit un filtro de fondo con opacity 0 / visibility
     hidden sigue compositando un canto de 1 px — la raya suelta a la
     derecha de Casa, entre el chrome y los orbes. El hairline de
     `.r-flyout__top` era el que se leía. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
  /* CERRADO ES EL DEFAULT, y es el lado seguro del error: un panel que
     aparece sin que nadie lo pida molesta más que uno que no aparece.
     `visibility` va con retardo al cerrar para que el contenido no
     desaparezca antes de que la caja termine de salir, y en 0s al abrir. */
  opacity: 0;
  transform: translateX(var(--s-5));
  pointer-events: none;
  visibility: hidden;
  /* El rebote corto es del sistema y este es exactamente su caso: algo que
     se desplaza y ATERRIZA. La opacidad nunca rebota. */
  transition: transform var(--d-slow) var(--ease-spring),
              opacity var(--d-base) var(--ease),
              visibility 0s linear var(--d-slow);
}
[data-flyout-open] .r-flyout {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  visibility: visible;
  backdrop-filter: blur(var(--frost-blur)) saturate(120%);
  -webkit-backdrop-filter: blur(var(--frost-blur)) saturate(120%);
  box-shadow: var(--e-3);
  transition: transform var(--d-slow) var(--ease-spring),
              opacity var(--d-base) var(--ease),
              visibility 0s;
}
/* Mientras se arrastra el canto NADA interpola: el panel tiene que seguir
   al dedo, no llegar 380ms tarde. Va en el host porque el centro del shell
   —que no es del sistema— tiene que enterarse del mismo estado. */
[data-flyout-resizing] .r-flyout,
[data-flyout-resizing] .r-iris { transition: none; }

/* ---- El grado DELGADO del vidrio mate: una perilla en el host ----

   La MISMA familia, un grado más delgada, sobre un campo con más color. No es
   la familia brillante y no hay vuelta a ella: el reflejo especular se borró
   del sistema (§ 22) por no tener consumidores, y esto es lo contrario —el
   pedido salió de un módulo que lo va a usar—.

   Sube al sistema y no se queda en Relaciones por el criterio de CLAUDE.md
   —sube lo que más de una superficie va a necesitar—: la pieza que adelgaza es
   .r-flyout, y por ese panel abren SIETE módulos. Lo que se queda del lado del
   módulo es la DECISIÓN de encenderlo, que es exactamente lo que debe ser un
   piloto.

   Los cuatro valores viven en tokens.css con su medición; aquí solo se dice
   cuándo entran. Se encienden juntos porque el vidrio es el papel Y sus pozos.

   CÓMO SE EXTIENDE A OTRO MÓDULO — dos puertas, y las dos son cortas:

     · Si el shell es dueño del host: un atributo, y nada más.
         <div class="os-shell" data-flyout-glass="thin">
       (o escribirlo mientras el módulo esté abierto, si se quiere por módulo).
     · Si la decisión vive en el CSS del módulo —que no puede tocar el host—:
       esta misma regla con su propio selector sobre `data-flyout-open`, que es
       el estado que ron.js ya escribe ahí. Es lo que hace `app/relations.css`.

   Lo que NO se usó, y vale dicho para que nadie lo «arregle»: una consulta de
   estilo (`@container style(--flyout-glass: thin)`) dejaría la puerta del
   módulo en UNA declaración. Se probó y funciona en Chromium, pero la app de
   escritorio es WKWebView y no hubo forma de comprobarlo ahí; si no estuviera
   soportada el panel se quedaría con el papel de siempre —falla segura, sí,
   pero INVISIBLE—, y una pieza cuyo único trabajo es verse no puede fallar
   callada. Con var() y su fallback no hay nada que soportar. */
[data-flyout-glass="thin"] {
  --flyout-frost: var(--surface-frost-thin);
  --flyout-veil:  var(--iris-veil-thin);
  --flyout-sunk:  var(--surface-sunk-frost);
  --flyout-raise: var(--surface-raise-frost);
}

/* El nombre del módulo se dice UNA vez, y este es el lugar: el orbe dice
   dónde picaste, la cabecera dice qué se abrió, y la vista de adentro NO lo
   repite. Es lo que reemplaza a la barra superior, que existía nada más
   para decirlo. */
.r-flyout__top {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-4) var(--s-3) var(--s-5);
  border-bottom: var(--stroke) solid var(--hairline);
}
.r-flyout__t {
  margin: 0;
  min-width: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-light);
  letter-spacing: var(--tr-tight);
  font-size: var(--t-h3);
  line-height: var(--lh-snug);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.r-flyout__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--s-4) var(--s-5) var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}
/* Cambiar de módulo: el contenido sale y entra, la caja SE QUEDA. Se mueve
   el contenido porque lo que cambió es el contenido; animar la caja diría
   que se abrió otro panel, y no se abrió: es el mismo. */
.r-flyout__body {
  transition: opacity var(--d-fast) var(--ease),
              transform var(--d-fast) var(--ease);
}
.r-flyout__body[data-flyout-swap] {
  opacity: 0;
  transform: translateY(var(--s-2));
}
/* Las figuras que se dibujan de su caja CEDEN ALTO antes que hacer
   scrollear el panel, y ceder alto no las deforma: la telaraña conserva su
   viewBox cuadrado y se centra en una caja más baja, y el grafo saca su
   radio del lado corto. Sin la cota, un panel de 705 se pasaba 111px
   (medido) y estrenaba una barra de scroll en un OS donde scrollear es la
   excepción. Se afina por consumidor con --flyout-fig, no editando esto. */
.r-flyout__body .r-radar,
.r-flyout__body .r-graph { max-height: var(--flyout-fig, 360px); }

/* El canto que redimensiona. En reposo es INVISIBLE y al acercarse aparece
   la manija: lo secundario se revela. Lo inyecta ron.js —con su etiqueta
   accesible y su `title` escritos una sola vez— así que el consumidor no
   lo escribe ni puede olvidarlo.

   Los 12px de ancho son objetivo de ratón, no dibujo: la manija visible
   son 1.5×32. Un canto de 1px se caza con el cursor, no se arrastra. */
.r-flyout__grip {
  position: absolute;
  inset-block: 0;
  left: 0;
  width: 12px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: ew-resize;
  touch-action: none;
  z-index: 2;
}
.r-flyout__grip::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 50%;
  translate: 0 -50%;
  width: var(--stroke-2);
  height: 32px;
  border-radius: var(--r-pill);
  background: var(--hairline-strong);
  opacity: 0;
  transition: opacity var(--d-fast) var(--ease);
}
.r-flyout__grip:hover::before,
.r-flyout__grip:focus-visible::before,
[data-flyout-resizing] .r-flyout__grip::before { opacity: 1; }

/* ---- 33b · .r-orbs / .r-orb — el chasis de módulos ----

   Siete botones SUELTOS al canto derecho, no una cápsula con iconos
   pegados. No es .r-rail con otra piel: son dos mecánicas distintas y por
   eso son dos piezas.

     · .r-rail es una CÁPSULA: un fondo, un relleno y un radio compartidos,
       con los ítems adentro. Abrirse al hover es abrir la cápsula entera y
       los ocho ítems crecen juntos, porque `stretch` los empareja.
     · .r-orb es su PROPIA caja. Al revelarse el nombre crece SOLO ese orbe
       y los vecinos no se mueven un píxel — no hay cápsula que remedir.

   Un modificador tendría que apagar el fondo, el relleno, el radio, el
   `stretch` y el marcador de la cápsula: eso no es usar la pieza, es
   pelearse con ella.

   EL MARCADOR SÍ SOBREVIVE, y vive en la COLUMNA, no entre cajas sueltas:
   `#osOrbs` es la pista vertical y `.r-orbs__mark` es la misma pastilla
   de `.r-tabs` (surface + sombra, `--d-base` + `--ease-spring`). Se mide
   el círculo del icono, no el botón entero: el nombre al hover crece a
   la izquierda y los vecinos no se mueven. Casa no tiene orbe — sin
   `aria-current` la pastilla se esconde.

   Tampoco se construyen sobre .r-icon-btn: esa pieza es `inline-grid` de
   ancho FIJO (36/46/56) y lo que define al orbe es crecer hacia la
   izquierda con una segunda columna.

   El fondo es el papel translúcido DEL TEMA y no el rgba oscuro fijo de la
   cápsula: siete manchas oscuras sobre el tapiz en tema claro se leen como
   siete agujeros. Con el papel del tema, chasis y panel son la misma
   familia — que es lo que hace que el OS se vea como un solo programa. */
.r-orbs {
  position: absolute;
  top: 50%;
  right: var(--orb-inset);
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  /* Al canto: el orbe crece hacia la IZQUIERDA porque su caja está anclada
     a la derecha, no porque nadie mueva a nadie. */
  align-items: flex-end;
  gap: var(--orb-gap);
  /* El chasis es la pieza de más afuera, así que es la capa de más arriba
     de las tres — por encima del panel (--z-flyout) y por debajo del velo
     (--z-scrim), que sí tiene que poder cubrirlo. */
  z-index: var(--z-chrome);
}

.r-orb {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: var(--orb-size);
  min-width: var(--orb-size);
  /* Explícito: el relleno que el navegador le pone a un <button> por
     omisión le sumaría ancho, y el orbe tiene que medir su icono y nada
     más mientras el nombre está guardado. */
  padding: 0;
  border-radius: var(--r-pill);
  background: var(--surface-frost);
  border: var(--stroke) solid var(--hairline-strong);
  backdrop-filter: blur(var(--frost-blur)) saturate(115%);
  -webkit-backdrop-filter: blur(var(--frost-blur)) saturate(115%);
  color: var(--ink-2);
  /* Flota igual que el panel, con un escalón menos: es chasis, no una hoja
     encima del contenido. */
  box-shadow: var(--e-2);
  transition: background var(--d-fast) var(--ease),
              color var(--d-fast) var(--ease),
              border-color var(--d-fast) var(--ease);
}
.r-orb:hover { color: var(--ink); border-color: var(--ink-3); }

/* Pastilla deslizante: la misma de .r-tabs, en vertical. Se mide el
   círculo del icono. Sin ron.js no hay marcador y el activo sigue
   leyéndose con el invert de siempre. */
.r-orbs__mark {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: var(--e-1);
  pointer-events: none;
  z-index: 0;
}
.r-orbs[data-mark-ready] .r-orbs__mark {
  transition: transform var(--d-base) var(--ease-spring),
              width var(--d-base) var(--ease),
              height var(--d-base) var(--ease);
}
/* Invertido = elegido. Respaldo sin ron.js. --surface-invert / --ink-invert
   se voltean con el tema solos y § 23 los remapea dentro de una superficie
   de color, así que el orbe activo no puede salir blanco sobre blanco. */
.r-orb[aria-current="page"] {
  background: var(--surface-invert);
  color: var(--ink-invert);
  border-color: transparent;
}

/* Con pastilla, el invert se apaga: la marca es la que dice el activo.
   Va DESPUÉS del invert y gana por especificidad (:has + current). */
.r-orbs:has(.r-orbs__mark) .r-orb { position: relative; z-index: 1; }
.r-orbs:has(.r-orbs__mark) .r-orb[aria-current="page"] {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink-3);
  box-shadow: none;
}

/* La caja del icono mide siempre lo mismo, y eso es lo que hace que al
   revelarse el nombre el dibujo NO se mueva: crece la otra columna.

   El borde se RESTA, y no es cosmética: `box-sizing: border-box` mete el
   hairline dentro de los 44 del alto, pero el ancho lo empuja la base de
   este hijo — así que con 44 aquí el orbe medía 46×44 y dejaba de ser un
   círculo (medido; la maqueta traía el mismo desnivel). De paso recupera
   los 16px de aire contra el panel: la reserva del chasis cuenta 44, no 46. */
.r-orb__i {
  --orb-inner: calc(var(--orb-size) - 2 * var(--stroke));
  flex: 0 0 var(--orb-inner);
  width: var(--orb-inner);
  height: var(--orb-inner);
  display: grid;
  place-items: center;
}

/* El nombre lo INYECTA ron.js desde el `aria-label` del botón — un botón de
   solo icono lo necesita de todos modos, así que el dato no se escribe dos
   veces y no se pueden contradecir. `order: -1` es lo que lo pone del lado
   correcto sin depender de dónde lo haya insertado el script.

   Y el ATAJO sale del otro lado del mismo `title` («Workspace · ⌘1»):
   ron.js lo traduce a la sintaxis de `aria-keyshortcuts` (`Meta+1`), que no
   es el glifo bonito. Lo que no puede traducir lo deja FUERA en vez de
   dejarlo a medias — un atributo vacío promete un dato y no entrega
   ninguno. Ver `keyShortcut` en ron.js. */
.r-orb__n {
  order: -1;
  max-width: 0;
  opacity: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--t-body-sm);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-tight);
  /* No captura el ratón: el clic es del botón, y así el nombre no se queda
     a medio revelar por un hover que en realidad era suyo.
     Mark-family: `--d-base` + `--ease-spring`, la misma curva de la
     pastilla. Sin resorte el nombre llegaba plano y la marca rebotaba. */
  pointer-events: none;
  transition: max-width var(--d-base) var(--ease-spring),
              opacity var(--d-base) var(--ease),
              padding var(--d-base) var(--ease-spring);
}
.r-orb:hover .r-orb__n,
.r-orb:focus-visible .r-orb__n {
  max-width: var(--orb-name);
  opacity: 1;
  padding-inline: var(--s-4) var(--s-1);
}

/* ---- Y aquí se decide sobre qué NO se abre ----
   Con un panel abierto el nombre NO se revela al hover. La razón es la
   premisa del rediseño: el panel es lo que estás mirando y sobre lo que le
   vas a preguntar al agente, así que el chasis no le pinta encima al dato.
   Sin panel se abre sobre la conversación, que es donde sobra espacio.

   El nombre no desaparece: vive en `title` y en la etiqueta accesible, que
   es donde CLAUDE.md manda la explicación.

   EL TECLADO ES LA EXCEPCIÓN, y de las de verdad: quien llega con Tab no
   tiene `title` que leer ni forma de saber en qué botón está. Por eso el
   :focus-visible de arriba sigue abriendo y esta regla lo respeta con
   `:not(:focus-visible)` en vez de apagar los dos. */
[data-flyout-open] .r-orb:hover:not(:focus-visible) .r-orb__n {
  max-width: 0;
  opacity: 0;
  padding-inline: 0;
}

/* ---- Táctil: no se abre nada, y es decisión, no olvido ----
   El sistema resuelve .r-rail sin ratón dejando los nombres PUESTOS
   (§ 13). Con orbes sueltos eso son siete pastillas permanentes encima del
   canto del panel. Aquí no hace falta, porque el rediseño ya dice el nombre
   en otro lado: con panel abierto lo dice la cabecera —el módulo se nombra
   una vez— y sin panel el costo de averiguar es un toque que abre y otro
   que cierra. Siete etiquetas permanentes sobre el contenido cuestan más. */
@media (hover: none) {
  .r-orb:hover:not(:focus-visible) .r-orb__n {
    max-width: 0;
    opacity: 0;
    padding-inline: 0;
  }
}

/* Lo que no es un módulo se separa con AIRE, no con una línea: un hairline
   entre dos elementos sueltos no divide nada —no hay cápsula que partir— y
   se lee como una raya suelta en el fondo. Es la contraparte de
   .r-rail__i--action, que sí puede usar la línea porque tiene cápsula. */
.r-orb--apart { margin-top: var(--orb-apart); }

/* ---- 33c · .r-iris — el campo de color que el vidrio recoge ----

   EL HALLAZGO QUE LO HIZO NECESARIO, medido: las manchas de .r-wall se
   anclan a las esquinas del VIEWPORT. En un monitor de 3440 eso deja el
   color entero en los cantos de la pantalla y lejos de cualquier caja —
   apagar el tapiz ahí es casi imperceptible—. Y sin color detrás, el papel
   translúcido del panel es una mancha gris.

   Esta pieza es la misma receta del tapiz (--wall-blooms + el velo del
   lienzo) ANCLADA A UNA CAJA en vez de al viewport. No es un mecanismo
   nuevo: es exactamente lo que ya hacen las miniaturas del selector de
   fondo, subido a componente.

   Los tonos se resuelven en el elemento que lleva `data-wall`, así que el
   consumidor elige el clima ahí y no hereda el de :root. Y por si el tapiz
   está apagado —`data-wall="none"` en :root deja --wall-a…d sin valor y el
   gradiente entero inválido—, § 27 le declara los tonos de la casa con
   :where(), o sea con especificidad cero: un `data-wall` propio siempre
   gana. Aquí el color no es decoración de escritorio, es lo que vuelve
   vidrio al vidrio, así que la pieza no puede quedarse sin él.

   Va sin z-index propio y ANTES que todo en el marcado: así el texto del
   centro y el panel pintan por encima sin que nadie negocie una capa. */
.r-iris {
  position: absolute;
  inset: 0;
  border-radius: var(--r-2xl);
  pointer-events: none;
  overflow: hidden;
  background-color: var(--canvas);
  background-image: var(--wall-blooms);
  background-repeat: no-repeat;
  --wall-veil: var(--iris-veil);
  transition: opacity var(--d-slow) var(--ease);
}
/* El velo va ENCIMA de las manchas y lleva el color del lienzo del tema:
   el mismo tono se aclara en claro y se apaga en cockpit. Idéntico a
   .r-wall::after, y es a propósito — un segundo velo con otra receta sería
   un segundo lenguaje de fondo. */
.r-iris::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--canvas);
  opacity: var(--wall-veil);
}

/* Pareado al panel: la aureola asimétrica de tokens.css, dicha en cuatro
   líneas que leen las MISMAS variables que .r-flyout. Por eso no se puede
   desincronizar con el panel al arrastrarlo. */
/* Pareado al panel TAMBIÉN en el grado: si el papel adelgaza, el velo del campo
   baja con él. Los dos son la misma perilla (ver el bloque del grado en § 33a) y
   por eso el default se dice aquí con el fallback, no con una segunda regla. */
.r-iris--flyout {
  --wall-veil: var(--flyout-veil, var(--iris-veil));
  inset: auto;
  top:    calc(var(--flyout-gap)  - var(--iris-halo));
  bottom: calc(var(--flyout-gap)  - var(--iris-halo));
  right:  calc(var(--flyout-rail) - var(--iris-halo));
  width:  calc(var(--flyout-w) + var(--iris-halo) + var(--iris-halo-near));
  opacity: 0;
  visibility: hidden;
}
[data-flyout-open] .r-iris--flyout { opacity: 1; visibility: visible; }

/* ---- 33d · .r-fab — el alta del módulo, en la esquina del panel ----

   Un botón redondo con el `plus` del set, pegado a la esquina INFERIOR DERECHA
   del panel, que da de alta la entidad del módulo abierto. Es lo que reemplazó
   a los disparadores de alta repartidos por la barra de cada módulo: cinco
   módulos tenían el suyo en cinco lugares distintos —la barra de Workspace, la
   de Relaciones, el pie de dos secciones de Growth, la cabecera de
   Intelligence— y "dónde se agrega algo" era una pregunta por módulo.

   POR QUÉ ES DEL SISTEMA Y NO DE CADA MÓDULO. La regla de CLAUDE.md es que
   sube lo que más de una superficie necesita: son CINCO módulos, y la caja
   contra la que se posiciona —.r-flyout— es del sistema. Un módulo no puede
   ponerlo desde su propio CSS ni aunque quiera: su raíz lleva
   `container-type: inline-size`, que trae contención de layout y por lo tanto
   VUELVE A SU RAÍZ el bloque contenedor de cualquier absolute de adentro. Un
   botón declarado ahí no se ancla al panel, se ancla al contenido y SE VA CON
   EL SCROLL. Los seis módulos con container lo tienen (wk, gw, intel, lever,
   rel, fin), así que no es un caso raro: es todos.

   Por eso vive DENTRO de .r-flyout y lo monta ron.js (`RON.flyout.fab`): el
   panel es su bloque contenedor, no scrollea con el cuerpo y su `overflow:
   hidden` lo recorta como a todo lo demás.

   Y por eso va DESPUÉS del cuerpo en el marcado: así el tabulador lo alcanza al
   FINAL, cuando ya recorrió la ficha o la lista. Un botón de alta que se
   tabulara primero le robaría el orden al contenido, y es lo contrario de lo
   que hace el orbe.

   TINTA, NO ACENTO, y no es timidez: el no-negociable dice un acento por vista
   y las cinco vistas ya lo gastaron en su métrica de matriz de puntos
   (.r-lcd--accent). Un botón ácido sería el SEGUNDO. Lo que carga la jerarquía
   es la pareja invertida que el sistema ya usa para la acción principal
   —--surface-invert / --ink-invert, la misma de .r-btn--solid y de la pastilla
   de .r-seg—: negra sobre papel, blanca en cockpit, y se voltea con el tema sin
   una sola regla por superficie. Medido contra los cuatro fondos reales del
   panel (papel y vidrio delgado × claro y cockpit) el icono no baja de 14:1.

   LA SOMBRA es la misma excepción que el panel se ganó y por el mismo motivo,
   dicho en el propio sistema: "se reserva para lo que de verdad flota". Esto
   flota, con la lista pasando por debajo. Es --e-2, el escalón MÍNIMO que
   levanta: con --e-1 el círculo se pega al papel y el contenido parece pasarle
   por encima; --e-3 es del panel entero y aquí se lee como un globo. El
   hairline no hace el trabajo esta vez —un borde de --hairline sobre relleno
   invertido es invisible— y por eso la regla de "hairline antes que sombra" no
   aplica: no hay hairline que la sustituya.

   DOS RELLENOS, UNA CAJA. El hueco es un slot y admite dos cosas, según lo que
   el alta necesite:

     · un botón (.r-fab__b) cuando el alta no pide texto — Intelligence crea la
       nota vacía y abre el editor, Finanzas lleva al renglón de captura;
     · el .r-capture del sistema cuando el alta ES un nombre — una tarea, una
       habilidad, una persona. Se reusa entero: la validación, Enter/Escape, el
       foco que vuelve al disparador y el uno-abierto-a-la-vez ya están ahí, y
       su panel sale en la capa superior, así que la esquina del panel no lo
       recorta. Lo único que cambia es la talla, y la escribe el slot.

   El `.r-capture__t` viene con .r-icon-btn--ghost puesto por ron.js. Vestirlo
   desde aquí es el sistema hablándose a sí mismo, no un override de consumidor:
   por eso el skin se declara una vez para los dos rellenos, en una lista de
   selectores, y no hay una copia por relleno que se pueda desincronizar. */
.r-fab {
  position: absolute;
  right: var(--fab-gap);
  bottom: var(--fab-gap);
  width: var(--fab-size);
  height: var(--fab-size);
  /* Del contexto de apilamiento del panel, no de la escala de capas: es el
     z-index INTERNO de una pieza, vecino del 2 de la manija. */
  z-index: 3;
  /* APAGADO ES EL DEFAULT, igual que el panel: una sección que no da de alta
     nada —Nubes, Programas, el costo del OS— no tiene que apagar nada, tiene
     que no prenderlo. */
  opacity: 0;
  scale: 0.9;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--d-fast) var(--ease),
              scale var(--d-base) var(--ease-spring),
              visibility 0s linear var(--d-base);
}
.r-fab[data-fab-on] {
  opacity: 1;
  scale: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--d-fast) var(--ease),
              scale var(--d-base) var(--ease-spring),
              visibility 0s;
}
/* El .r-capture ocupa el hueco entero: su caja de flujo es fija (28×28) y aquí
   la manda el slot. Su disparador ya es width/height 100%, así que crece con
   ella sin tocar nada más. */
.r-fab > .r-capture {
  width: 100%;
  height: 100%;
}
.r-fab__b,
.r-fab > .r-capture > .r-capture__t {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  padding: 0;
  --ico: 20px;
  border: var(--stroke) solid transparent;
  border-radius: var(--r-pill);
  background: var(--surface-invert);
  color: var(--ink-invert);
  box-shadow: var(--e-2);
  transition: box-shadow var(--d-fast) var(--ease),
              transform var(--d-fast) var(--ease);
}
/* Al hover sube un escalón de sombra en vez de cambiar de color: el relleno ya
   es el extremo de la escala y no tiene a dónde ir. */
.r-fab__b:hover,
.r-fab > .r-capture > .r-capture__t:hover {
  box-shadow: var(--e-3);
  color: var(--ink-invert);
}
.r-fab__b:active,
.r-fab > .r-capture > .r-capture__t:active { transform: scale(0.94); }

/* ---- La reserva de abajo, y el bug de layout que costó encontrarla ----

   El último renglón de una lista scrolleada hasta el fondo no puede quedar
   debajo del botón, y en el OS las listas largas son la regla. La reserva es
   un relleno en el cuerpo del panel, `--fab-reserve`, y solo se paga cuando
   hay botón — ron.js escribe el atributo.

   PERO EL RELLENO SOLO NO ALCANZA, y esto es lo que hay que no volver a
   descubrir: el cuerpo es un flex column y la vista de un módulo es
   `flex: 1 1 auto; min-height: 0` (os.css), así que cuando el contenido no
   cabe la vista SE ENCOGE y su contenido le sale por abajo. El fondo del
   scroll lo define entonces la caja de un descendiente DESBORDADO, y a un
   desbordamiento no se le vuelve a sumar el relleno final del contenedor.
   Medido en Intelligence con nueve notas, antes del arreglo: `paddingBottom`
   computaba 70px y `scrollHeight` era idéntico con y sin la regla, con el
   último renglón terminando exactamente en el canto del panel. Un `::after`
   como espaciador falla por lo mismo — es otro elemento de la línea de flex,
   no del desbordamiento.

   Así que el cuerpo declara la otra mitad: EN ESTE CUERPO NADA SE ESTRUJA. Un
   hijo que cede saca su contenido del flujo del scroll, y de ahí en adelante
   el cuerpo ya no manda en su propio alto. Con `flex-shrink: 0` la vista crece
   con su contenido, se queda EN el flujo, y el relleno vuelve a ser lo último
   del scroll. `flex-grow` no se toca, así que una vista corta sigue estirándose
   a todo el panel y las segundas páginas —la ficha, la nota, la hoja— siguen
   midiendo exactamente el panel y scrolleando por dentro.

   Las dos declaraciones van bajo `[data-flyout-fab]` y eso hace dos trabajos:
   scopa el cambio de layout a cuando de verdad hay un botón que esquivar, y le
   da a la regla la especificidad que necesita para ganarle al `flex: 1 1 auto`
   de `.os-view`, que vive en el CSS del chasis y se carga después de este
   archivo. Con el selector corto (`.r-flyout__body > *`) empatan en
   especificidad y gana el chasis por orden de carga: la regla se ve en el
   inspector y `flex-shrink` sigue computando 1. Ya pasó — es media hora. */
.r-flyout[data-flyout-fab] .r-flyout__body { padding-bottom: var(--fab-reserve); }

/* ---- 33d bis · .r-fabsafe — el relleno cuando el scroll es de OTRO ----
   El relleno de arriba vive en `.r-flyout__body`, y eso alcanza mientras el
   cuerpo sea quien scrollea. En cuanto una lista de adentro tiene su PROPIO
   `overflow-y: auto`, el relleno del cuerpo queda FUERA de ese scroller: la
   lista llega hasta su último píxel y su último renglón se mete debajo del
   botón.

   Y no es cosmético, que es la misma lección de `pruebas/ui/test_traslape.py`:
   el `.r-fab` mide `--fab-size` y está encima (`z-index: 3`), así que **se come
   el clic**. Playwright lo dijo con esas palabras — «`<div class="r-fab">`
   intercepts pointer events» — y con un dedo humano pasa igual pero sin
   diagnóstico: se pica el último renglón y se abre el alta.

   Medido el 2026-08-27, y ya estaba mordiendo en producción: `.fin-accs` tenía
   scroller propio desde antes de esta noche.

   CSS NO PUEDE empujar un relleno hasta un descendiente arbitrario, así que
   esto es una clase que el consumidor pone — y una clase que hay que recordar
   es una clase que se olvida (`.fin-accs` la prueba). Por eso el guardián no es
   esta regla: es `test_traslape.py`, que recorre los módulos con alta y afirma
   que ningún scroller de adentro deja su último renglón bajo el botón. La regla
   es el arreglo; la prueba es lo que lo mantiene. */
.r-fabsafe { padding-bottom: var(--fab-reserve); }

/* ---- 33e · [data-confiable="no"] — lo que escribió alguien más ----
   El OS pinta, en varias superficies, texto que **no escribió Robbie**: lo que
   un agente extrajo de una página web, lo que dijo un modelo, el resumen de una
   junta. Del lado de Python eso viaja marcado —`acp/normaliza.py`,
   `navegador/extrae.py` y `agentes/transcripto.py` fuerzan `confiable: False`,
   y ninguno de los tres deja subirlo— y `.r-stream` ya lo dice con su canto
   punteado.

   Lo que faltaba, y lo encontró el review del 2026-08-27, es que los nodos
   FUERA de `.r-stream` llevaban el atributo y **cero diferencia visual**: la
   marca existía y nadie la miraba. Un atributo que viaja en cada evento y que
   ningún estilo consulta es peor que no tenerlo, porque invita a que alguien
   construya encima creyendo que significa algo.

   La firma es la misma que `.r-stream` ya usa —canto punteado— para que sea UNA
   sola señal en todo el OS y no una por superficie. Deliberadamente NO es color:
   los semánticos son señal de estado (esto pide atención, esto falló) y
   «lo escribió alguien más» no es un estado, es una procedencia.

   El caso que más importa es la pregunta de un agente: es la única superficie
   que pide una DECISIÓN, y su texto pudo salir de una página web. */
[data-confiable="no"] {
  border-left: var(--stroke-2) dashed var(--hairline-strong);
  padding-left: var(--s-3);
}
.r-flyout[data-flyout-fab] .r-flyout__body > * { flex-shrink: 0; }

/* Movimiento reducido: la regla global acelera las DURACIONES a 0.01ms y
   no toca los RETARDOS, así que la visibilidad del panel se quedaría
   esperando --d-slow para desaparecer — o sea, un panel cerrado que sigue
   ahí medio segundo. Se dice explícito. Lo mismo con el botón de alta, que
   copia el patrón del panel y por lo tanto también su retardo. */
@media (prefers-reduced-motion: reduce) {
  .r-flyout { transition-delay: 0s !important; }
  .r-fab { transition-delay: 0s !important; }
}

/* ============================================================
   34 · COMPANION — la figura viva con la que se le habla al agente

   Robbie pidió un orbe de luz a la izquierda del prompt y lo tuvo cuatro
   días. Lo que se pidió después es otra cosa y por eso esta sección cambió
   de nombre: no un botón que se enciende, sino **una criatura que está
   viva** — que siempre se mueve un poquito, que cambia de forma según lo
   que está pasando, y que es la cara del OS.

   ── LÁPIDA: `.r-voice`, EL ORBE DE VOZ (2026-08-22 → 2026-08-26) ────────

   Aquí vivía `.r-voice`: un círculo con hairline, un icono de micrófono
   adentro, cuatro estados (`idle · hear · think · talk`) y un ARO que
   crecía con `--voice-level` para decir «te oigo». Sus piezas eran
   `.r-voice__o` (el orbe), `__ring` (el aro), `__i` (el icono), `__s` (el
   paso) y `__m` (el silenciador).

   Se fue porque el aro era la única cosa que la pieza sabía decir con el
   cuerpo, y todo lo demás lo decía con luz de color: cuatro estados, tres
   colores de aura. Eso ponía un objeto de color en pantalla todo el día en
   un OS monocromo, y dejaba sin lugar dos estados que hacían falta
   (`exec`, `needs`). Una figura que MORFEA dice la diferencia con la
   FORMA, así que el color se puede reservar — y reservado, el color vuelve
   a ser información. Ver el bloque del ácido en `tokens.css`.

   Lo que se conserva de aquella pieza, porque estaba bien:
     · la geometría del hermano: la talla sale de `--orb-size`;
     · el paso (`__s`), con la mecánica del nombre de `.r-orb`;
     · el silenciador (`__m`), visible siempre que la criatura se ve;
     · sin atributo, la pieza NO EXISTE.

   Lo que se fue con el orbe: el aro (`__ring`), `--voice-level`,
   `--voice-ring` y `--voice-follow`. El cuerpo es hoy lo que sigue tu voz.

   Y LO QUE SE FUE UNAS HORAS DESPUÉS: **el aura, entera.** La primera versión
   de esta pieza todavía llevaba `.r-beam` y le elegía cinco colores por
   estado, o sea que cometía a medias el error por el que el orbe murió. Robbie
   lo cortó el mismo día. La lápida y el argumento completo están en § 34d, y
   es la subsección que hay que leer antes de proponer devolverla.

   ── DE DÓNDE SALE LA TÉCNICA, Y DE DÓNDE SALE LA CRIATURA ───────────────

   La TÉCNICA —una sola forma rellena que morfea entre estados declarados,
   más formas independientes para los ojos, con el motor como función pura
   del tiempo— está inspirada en **bloub**, de Jeremy Prat (MIT,
   <https://github.com/jeremy-prt/bloub>). La atribución completa y el
   detalle de qué es prestado y qué no vive junto al motor, en
   `ron.js § 34`.

   La GEOMETRÍA es propia y se deriva del vocabulario de este sistema, no
   de la de aquel proyecto. La criatura es un **cuadrado de cantos
   generosos**, y eso no es una preferencia: es el punto de estado del
   sistema —que es **cuadrado, no redondo**, y es un no-negociable de
   `CLAUDE.md`— crecido hasta poder tener ojos. Sus ojos son dos cuadrados
   pequeños del mismo canto, y **parpadean colapsando a un hairline**, que
   es la primitiva con la que este sistema separa todo. Una bola con ojos
   habría sido de cualquier producto; esto es de éste.

   ── POR QUÉ SIGUE SIENDO DEL SISTEMA Y NO DE HOME ───────────────────────

   Por lo mismo que el orbe: son DOS superficies desde el primer día y las
   dos ya existen —el centro (`index.html`, `.os-composer`) y la pastilla
   de ⌥Space (`app/pastilla.html`), que es otra ventana con su propia caja
   de prompt— y las dos cargan `ron.js`. El criterio de `CLAUDE.md` —«sube
   lo que más de una superficie necesita»— se cumple sin imaginar nada.

   Y hay una razón nueva y más fuerte: el companion no es sólo la voz. Es
   la cara del OS, y `exec` («estoy ejecutando una acción») y `needs`
   («necesito que veas esto») son hechos que conoce cualquier módulo, no
   sólo el motor de audio. Una cara por módulo serían siete caras.

   ── LA ANATOMÍA ─────────────────────────────────────────────────────────

     <div class="r-companion" data-comp="idle">           ← el grupo: estado
       <button class="r-companion__b" …>                  ← el botón, SIN aura
         <svg class="r-companion__f" …>                   ← la figura
           <path class="r-companion__body">               ← el cuerpo
           <path class="r-companion__eye"> ×2             ← los ojos
         </svg>
         <span class="r-companion__s"></span>             ← el paso
       </button>
       <button class="r-companion__m r-icon-btn …">       ← el silenciador
     </div>

   El estado vive en el GRUPO y no en el botón, y de las dos razones que tenía
   el orbe queda una —pero es suficiente y no cambió: un botón no puede
   contener otro botón, así que el silenciador tiene que ser HERMANO y el
   estado tiene que vivir en algo que los contenga a los dos. (La otra era que
   `--beam-ink` se declarara una vez y bajara por la cascada al portador del
   aura; se fue con el aura, § 34d.)

   Los `path` los escribe `ron.js` cuadro por cuadro. El texto —el `title`,
   la etiqueta accesible, la palabra del paso— sale de UNA sola tabla de
   estados, mismo criterio que `.r-run`, `.r-confirm` y `.r-seeing`.

   ── EL CONTRATO ─────────────────────────────────────────────────────────

     .r-companion              la clase, sobre el grupo
     (sin data-comp)           INERTE: la pieza no existe. Es el estado
                               `off` — ver abajo.
     data-comp="idle"          reposo. Respira y parpadea: está vivo
     data-comp="hear"          escuchando — el cuerpo sigue tu volumen
     data-comp="think"         transcribiendo o esperando al modelo
     data-comp="talk"          el agente está hablando
     data-comp="exec"          ejecutando una acción del catálogo
     data-comp="needs"         requiere tu atención — EL ÚNICO ÁCIDO
     data-comp-step="…"        una palabra que dice en qué paso va; la
                               revela al lado de la figura, con la misma
                               mecánica del nombre de `.r-orb`
     data-comp-muted           el agente no habla en voz alta (junta)
     data-comp-body="…"        cuál cuerpo. Personalización por usuario;
                               el catálogo lo declara `ron.js § 34`
     data-comp-rest="…"        cuál expresión de reposo. Igual
     data-comp-solo            la criatura NO está en un renglón de
                               controles: está sola y en grande, y lo que la
                               acompaña —el paso, el silenciador— se sale del
                               flujo para no moverla. Ver § 34f
     --comp-size               la talla. Default 88px, la de la cara del OS
     --comp-size-min           el piso cuando el hueco no alcanza, y es
                               `--orb-size`. Lo elige el consumidor, no la
                               pieza: quién tiene hueco es cosa del layout

   El nivel del micrófono NO es un atributo ni una custom property, y es la
   única diferencia de contrato con el orbe que se fue: se entrega por
   `RON.companion.level(nodo, 0–1)`. El porqué está en `ron.js § 34`, junto
   al código que lo consume — en resumen, es un dato que llega sesenta
   veces por segundo y hoy lo consume JavaScript, así que pasarlo por el
   DOM sería escribir un atributo y leerlo con `getComputedStyle` en cada
   cuadro para nada.

   ── SIN `data-comp` LA PIEZA NO EXISTE, Y ES EL ESTADO `off` ────────────

   `CLAUDE.md`: «nada se prende solo». `off` —la voz no está disponible—
   NO tiene valor de atributo propio: es la AUSENCIA del atributo. Es a
   propósito y es la misma decisión que tomó `.r-voice`: con un
   `data-comp="off"` habría dos formas de decir «no hay companion» y una
   podría quedarse pegada. Sin motor de transcripción o sin permiso de
   micrófono, el compositor se ve EXACTAMENTE como sin esta pieza, y la
   forma de garantizarlo es `display: none` y no `opacity: 0` —que deja un
   objetivo de ratón invisible pegado al prompt—. El atributo lo escribe
   `app/voz.js` sólo después de confirmar la capacidad.
   ============================================================ */

/* ---- 34a · El grupo, el botón y la figura ----

   La talla sale de `--comp-size`, que es `--orb-size`: si algún día el
   chasis engorda sus botones, ésta engorda con ellos y el prompt no se
   queda con dos círculos de distinto tamaño.

   El ancho del botón es `min-width` y no `width` para que la palabra del
   paso pueda crecer la caja HACIA LA DERECHA —la figura vive a la
   izquierda del prompt, así que ese es el lado donde hay renglón— sin que
   la figura se mueva: su caja mide siempre lo mismo, igual que
   `.r-orb__i`.

   SIN BORDE, y es un cambio deliberado respecto al orbe. Aquel era un
   círculo de hairline con un icono adentro: el marco era lo que decía
   «esto es un botón». La figura no lo necesita —ella misma es el objeto—
   y un aro alrededor de una criatura la mete en una jaula. Lo que
   sustituye al marco como señal de que se pica es el cuerpo, que se
   oscurece al hover, y el anillo de foco del sistema para el teclado. */
.r-companion {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  min-width: 0;
}
/* Sin estado declarado no hay pieza. Ver el encabezado. */
.r-companion:not([data-comp]) { display: none; }

.r-companion__b {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  flex: 0 0 auto;
  height: var(--comp-size);
  min-width: var(--comp-size);
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  /* El aura se dibuja FUERA de la caja y no se puede recortar: nada de
     `overflow: hidden` aquí, ni en el grupo. */
}
.r-companion__b:active { transform: scale(0.94); }

/* La caja de la figura. Talla fija y no `100%`: el botón CRECE cuando hay
   palabra de paso, y con un porcentaje la criatura se estiraría con él. */
.r-companion__f {
  flex: 0 0 var(--comp-size);
  width: var(--comp-size);
  height: var(--comp-size);
  display: block;
  /* La criatura se sale de su caja al respirar y al dar el brinco de
     `needs`. El `viewBox` reserva aire, pero `overflow: visible` es lo que
     garantiza que un pico no se recorte a media animación. */
  overflow: visible;
  /* Encima del aura: el aura es luz de fondo, la criatura es el objeto. */
  position: relative;
  z-index: 1;
}

/* ---- 34b · El color: tinta siempre, ácido en un solo estado ----

   `--comp-body` en reposo y `--comp-body-on` cuando está ocupada: es
   exactamente la transición que el orbe hacía con su icono (`--ink-2` →
   `--ink`), y se conserva porque estaba bien — «ocupado» se lee más
   presente, no de otro color.

   El ÁCIDO es de `needs` y de nada más. Con eso el color es información y
   el no-negociable de «un solo acento por vista» sobrevive a una pieza que
   está en pantalla todo el día. El porqué largo está en `tokens.css`.

   La transición es de `fill` y dura `--d-base`: el CAMBIO DE FORMA lo
   interpola el motor en JavaScript (es geometría, no una propiedad
   animable), pero el cambio de color es una propiedad y le toca al CSS.
   Repartirlo así es lo que hace que en `prefers-reduced-motion` el estado
   siga cambiando de color aunque el motor deje de correr. */
.r-companion__body {
  fill: var(--comp-body);
  transition: fill var(--d-base) var(--ease);
}
.r-companion__eye {
  fill: var(--comp-eye);
  transition: fill var(--d-base) var(--ease);
}

/* Hover: la señal de que se pica, ahora que no hay marco. */
.r-companion__b:hover .r-companion__body { fill: var(--comp-body-on); }

.r-companion[data-comp="hear"]  .r-companion__body,
.r-companion[data-comp="think"] .r-companion__body,
.r-companion[data-comp="talk"]  .r-companion__body,
.r-companion[data-comp="exec"]  .r-companion__body {
  fill: var(--comp-body-on);
}

/* AL FINAL a propósito: `needs` empata en especificidad con el hover y con
   los cuatro de arriba, así que el orden de origen es lo que lo hace ganar.
   Y tiene que ganar: si el ácido se apagara al pasar el ratón por encima, la
   única señal de que algo requiere atención desaparecería justo cuando
   alguien fue a atenderla. */
.r-companion[data-comp="needs"] .r-companion__body { fill: var(--comp-alert); }
/* La regla de `CLAUDE.md` sobre fondo claro fijo: el ácido es claro en los
   DOS temas, así que los ojos en `--comp-eye` (que sigue al tema) saldrían
   casi blancos sobre casi blanco en claro. `--fixed-ink` es el token que
   existe justo para esto. */
.r-companion[data-comp="needs"] .r-companion__eye { fill: var(--comp-alert-eye); }

/* ---- 34c · El paso, con la mecánica del nombre del orbe ----

   Se conserva tal cual del orbe de voz, incluida la razón: entre que dejas
   de hablar y que el agente contesta pueden pasar segundos, y son DOS
   esperas distintas —transcribir el audio y esperar el primer token— de
   las que no se ve ninguna. Sin decirlo, lo que uno concluye es que no lo
   oyó, y vuelve a hablar encima.

   Esto ES la excepción de la claridad de `CLAUDE.md`, no una etiqueta de
   más: un estado ambiguo se dice con palabras. Lo que NO se hace es
   inventar una pieza para decirlo — se reusa la mecánica del nombre de
   `.r-orb`: `max-width` de 0 a un techo con las curvas del sistema, y el
   texto lo escribe ron.js.

   Sin `order`, que es el espejo del `order: -1` de `.r-orb__n`: aquel crece
   hacia la izquierda porque su caja está anclada al canto derecho de la
   ventana; éste crece hacia la derecha porque ahí está el prompt. */
.r-companion__s {
  max-width: 0;
  opacity: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--t-micro);
  color: var(--ink-3);
  letter-spacing: var(--tr-tight);
  pointer-events: none;
  position: relative;
  z-index: 1;
  transition: max-width var(--d-base) var(--ease),
              opacity var(--d-fast) var(--ease),
              padding var(--d-base) var(--ease);
}
/* La condición es QUE HAYA PALABRA, dicha con `:not(:empty)` y no con un
   atributo. Es la diferencia entre una verdad y dos: `data-comp-step` lo pone
   el consumidor cuando quiere forzar un paso, pero el paso también sale de la
   tabla de estados de `ron.js` (`think` → «pensando»), y con la regla colgada
   del atributo esos pasos se escribían en el DOM y no se veían. Preguntar por
   el texto no se puede desincronizar del texto. */
.r-companion__s:not(:empty) {
  max-width: 12ch;
  opacity: 1;
  padding-inline: var(--s-1) var(--s-3);
}

/* ---- 34d · LA CRIATURA NO LLEVA AURA. NINGUNA, EN NINGÚN ESTADO ----

   Robbie, 2026-08-26, viendo el showcase: «no pongas auras alrededor del
   compañón. Que el compañón no tenga ninguna aura alrededor.»

   Esta subsección no declara una sola regla, y por eso vale la pena que
   exista: es el lugar donde alguien va a venir a preguntar «¿y por qué esta
   pieza no usa `.r-beam` si tiene un modelo detrás?». La respuesta es que la
   forma ya lo dice.

   ── LÁPIDA: EL AURA POR ESTADO (nació y murió el 2026-08-26) ────────────

   Aquí vivían cuatro reglas. `.r-beam` iba puesto en el botón, `ron.js` le
   prendía `data-beam-on` en los cuatro estados ocupados, y cada uno elegía
   color: `--teal-500` escuchando, `--rose-300` ejecutando, `--violet-500`
   hablando, y `data-beam-busy` en pensando —que heredaba el `--ok` y el ritmo
   acelerado del sistema—. `--comp-strength` valía .62 y era la fuerza de ese
   resplandor. Reposo y `needs` ya iban sin aura.

   Se fueron todas, y el argumento es el de esta pieza llevado hasta el final.
   La lápida del orbe de voz —arriba, en el encabezado de § 34— dice que aquel
   murió porque «el aro era lo único que decía con el cuerpo y todo lo demás lo
   decía con color: cuatro estados, tres colores de aura». Y la primera versión
   del companion dejó CINCO: los cuatro de arriba más el ácido de `needs`.

   Con la forma diciendo los siete estados, el aura es **redundancia de color
   en la pieza que está en pantalla más tiempo que ninguna.** Y `needs` ya
   estaba resuelto con el criterio correcto —«el cuerpo entero ya es la señal»—
   aplicado a un solo estado. Extenderlo a los otros seis es la misma frase.

   ── LO QUE DICE CADA ESTADO SIN COLOR ───────────────────────────────────

   No es una lista de consuelo: es el contrato de la pieza, porque si un estado
   no se distingue por su forma, esto no se arregla devolviéndole el aura — se
   arregla en el ciclo (`ron.js § 34`, `COMP_STATES`).

     idle    respira lento y parpadea. Está viva y no está haciendo nada.
     hear    se ESTIRA con tu volumen, y con un piso de 1.05 que se queda
             aunque estés callado: «el micrófono está abierto».
     think   se MECE ±13°, se estrecha hacia `torre` y MIRA ARRIBA. Los tres
             gestos juntos son lo que lo separa de reposo; el que más pesa es
             la inclinación, porque reposo nunca se inclina más de 0.9°.
     talk    PULSA con dos ritmos superpuestos, sílaba y frase.
     exec    la CÁSCARA GIRA y los ojos se quedan fijos y a rendijas.
     needs   ÁCIDO, y brinca hacia ti cada 1.9s con los ojos abiertos.
     off     no existe.

   ── LO QUE **NO** SE FUE, Y NO HAY QUE CONFUNDIRLO ──────────────────────

   El aura de la CAJA DE PROMPT (`.os-composer.r-beam` con `data-beam-on` en el
   marcado) se queda intacta. `CLAUDE.md` es explícito: ahí «vive en reposo,
   porque es su identidad». Son dos elementos HERMANOS desde que la criatura
   salió de la caja, así que quitarle el aura a una no le toca la otra — pero
   en pantalla están a 16px de distancia, y quien vea esto de rápido va a
   creer que son la misma pieza. No lo son.

   Y `.r-beam` sigue en el sistema, entero, con sus dos consumidores de
   siempre: la caja de prompt y `.r-run`. Lo que se fue es un consumidor, no
   la pieza. */

/* ---- 34e · Silenciado: el agente no habla en voz alta ----

   VC1.7 / MD1. Un solo control, la misma bocina (`sound` / `sound-off`).
   Se VE siempre que la criatura se ve — también en reposo, sin silenciar
   y sin hablar. Escondido-hasta-estado era el único camino a descubrirlo,
   y en idle unmuted no había forma de encontrarlo.

   Quiet/secundario en reposo (la tinta de `.r-icon-btn--ghost`). Más
   presente —`--ink`— cuando está silenciado o en sesión de voz
   (hear / think / talk). `off` (sin `data-comp`) sigue sin pieza, así que
   tampoco hay silenciador.

   MD2: silenciado se dice con el icono tachado y la palabra «Silencio»,
   no con un color semántico. No es un error ni una advertencia: es una
   preferencia. La palabra es la excepción de la claridad: un modo que
   cambia el comportamiento y no se ve es un modo que uno no sabe que
   prendió.

   LÁPIDA (2026-08-26 → 2026-09-11): aquí vivían dos reglas de visibilidad
   —sesión de voz, o silenciado— y `display: none` en el resto, incluida
   `exec`/`needs` «porque no hay sesión». Eso dejaba el reposo sin control.
   El `display: none` de `off` se queda: es la pieza que no existe, no un
   estado de audio. */
.r-companion__m { display: none; }
.r-companion[data-comp] .r-companion__m {
  display: inline-grid;
  grid-auto-flow: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-1);
  margin-inline-start: var(--s-1);
  min-width: var(--ctl-sm);
  min-height: var(--ctl-sm);
}
.r-companion[data-comp="hear"] .r-companion__m,
.r-companion[data-comp="think"] .r-companion__m,
.r-companion[data-comp="talk"] .r-companion__m,
.r-companion[data-comp-muted] .r-companion__m {
  color: var(--ink);
}
/* Silenciado ensancha el botón para la palabra. El icono se queda; no se
   inventa una segunda bocina. */
.r-companion[data-comp-muted] .r-companion__m {
  width: auto;
  padding-inline: var(--s-2);
}
.r-companion__m-lab {
  display: none;
  font-size: var(--t-micro);
  letter-spacing: var(--tr-tight);
  white-space: nowrap;
}
.r-companion[data-comp-muted] .r-companion__m-lab { display: inline; }

/* ---- 34f · `data-comp-solo`: la criatura no está en un renglón ----

   Robbie, 2026-08-26: «el companion no tiene que estar dentro de la cajita.
   Tiene que estar afuera y tiene que ser más grande.»

   Fuera de un renglón de controles, las medidas de la pieza siguen valiendo
   pero SU COMPOSICIÓN no: el paso y el silenciador crecían A LO LARGO porque
   vivían en una fila donde había renglón a la derecha. Sola en el canal libre
   de la ventana, cada una de esas dos cajas que aparece MUEVE A LA CRIATURA
   —el grupo está anclado por un canto, así que lo que crece empuja lo demás—
   y una cara que da un brinco de 80px al entrar a «pensando» no se lee como
   un estado, se lee como un bug.

   Así que este modificador declara una sola cosa: **lo que acompaña a la
   criatura sale del flujo.** Los dos con `position: absolute`, o sea que
   aparecer y desaparecer no le cuesta un pixel de movimiento a la figura.

   ── Y LOS DOS SE APILAN HACIA ARRIBA, QUE NO ES LO OBVIO ────────────────

   El paso pide a gritos ir DEBAJO —es un pie de foto— y se escribió así
   primero. Medido en el centro con la ventana en 914×870: el pie caía en
   y=854 y la ventana termina en 870, o sea que la palabra vivía en los 20px
   de relleno de abajo y con dos píxeles más de tipografía la corta
   `.os-shell`, que es el único `overflow: hidden` del OS. La causa es
   estructural y no de esa ventana: **la criatura se apoya en el canto de
   abajo de su superficie**, porque ahí está la caja de prompt a la que
   acompaña. Debajo de ella no hay página.

   Arriba hay toda la conversación. Así que arriba, y en un orden que también
   se decidió midiendo: el paso PEGADO a la figura, porque es su pie de foto,
   y el silenciador encima del paso. Los dos con desplazamientos FIJOS y no
   apilados en un flujo — así, cuando el paso está vacío, el silenciador no
   baja, y cuando aparece, no sube nada.

   Y el silenciador va arriba y no a la izquierda, que fue el primer intento:
   a la izquierda el grupo mide `--comp-size` más el botón, y el consumidor que
   la ancla contra un canto tendría que reservarle ese ancho o dejar que se
   recorte contra el canto de la ventana. Arriba, el grupo mide exactamente
   `--comp-size` de ancho, y entonces la cuenta del consumidor es una sola
   resta. Es la misma clase de decisión que el `min-width` de § 34a: la caja
   del acompañante no puede mover ni medir a la figura.

   POR QUÉ ES DEL SISTEMA Y NO DE `app/voz.css`. Porque el criterio de
   `CLAUDE.md` se cumple: son dos superficies. El centro la pone en el canal de
   la ventana, y la pastilla de ⌥Space —que es una ventana chica con su propia
   caja de prompt— es la otra candidata natural a tenerla sola y en grande. Y
   porque hacerlo desde la app habría pedido `flex-direction` sobre
   `.r-companion__b`, que es exactamente el override de `.r-` que el proyecto
   prohíbe.

   `:where()` en la regla de posicionamiento es deliberado y copia a `.r-beam`:
   el modificador NECESITA un ancestro posicionado para colgar sus dos hijos,
   pero el consumidor que ya se posiciona solo —el centro la ancla con
   `position: absolute` contra su columna— no puede perder su posición por
   ponerse este atributo. Con especificidad cero, cualquier regla del
   consumidor gana sin pelear. */
:where(.r-companion[data-comp-solo]) { position: relative; }

/* EL PASO, ARRIBA Y CENTRADO. La caja se declara completa y lo único que
   cambia con el texto es la OPACIDAD: con `max-width` de 0 a un techo, el
   ancho de la caja variaría y el `translate: -50%` la movería mientras crece,
   que se ve como una palabra que entra de lado.

   `padding-inline: 0` deshace el de § 34c, que ahí separaba la palabra del
   icono en la misma fila. Aquí no hay fila. */
.r-companion[data-comp-solo] .r-companion__s,
.r-companion[data-comp-solo] .r-companion__s:not(:empty) {
  position: absolute;
  bottom: calc(100% + var(--s-1));
  left: 50%;
  translate: -50% 0;
  max-width: 12ch;
  padding-inline: 0;
  text-align: center;
  transition: opacity var(--d-base) var(--ease);
}
.r-companion[data-comp-solo] .r-companion__s { opacity: 0; }
.r-companion[data-comp-solo] .r-companion__s:not(:empty) { opacity: 1; }

/* EL SILENCIADOR, ENCIMA DEL PASO. Sigue las reglas de § 34e —visible
   siempre que la criatura se ve—; lo que cambia es que ya no ocupa caja
   en la fila, así que no mueve nada al aparecer.

   El desplazamiento es FIJO (`--s-6`, 24px) y no «lo que mida el paso»: el
   paso aparece y desaparece con el estado, y un silenciador que siguiera su
   alto subiría y bajaría cada vez que el modelo cambia de espera. Con 24 hay
   lugar para el renglón de 11px del paso y su aire, y cuando el paso está
   vacío queda un hueco que nadie nota porque el canal está vacío. */
.r-companion[data-comp-solo] .r-companion__m {
  position: absolute;
  bottom: calc(100% + var(--s-6));
  left: 50%;
  translate: -50% 0;
  margin-inline-start: 0;
}

/* ---- 34g · Movimiento reducido: la forma se queda, el movimiento se va ----

   Mismo criterio que § 32 y que el orbe: quieto, NO borrado. Aquí el reparto
   lo hace el MOTOR y no esta hoja —`ron.js` deja de pedir cuadros y dibuja
   una sola pose por estado—, así que la criatura sigue siendo la criatura,
   sigue cambiando de forma al cambiar de estado, y deja de respirar.

   Lo que esta hoja sí tiene que decir es que el color siga cambiando: la
   regla global acelera las duraciones a 0.01ms, o sea que la transición de
   `fill` llega de golpe en vez de desaparecer. Eso es correcto y es la razón
   de que el color viva en CSS y la forma en JS: quien pidió movimiento
   reducido no se queda sin saber en qué estado está.

   Sin esto, lo único que se perdería es el `scale` del `:active`, que sí es
   movimiento de gesto y sí se va. */
@media (prefers-reduced-motion: reduce) {
  .r-companion__b:active { transform: none; }
}
