/* =====================================================================
   TIPOGRAFÍA AUTOALOJADA (static/fonts/, ver OFL.txt — SIL Open Font License)
   Big Shoulders Display: variable font — un archivo cubre 700 y 900.
   IBM Plex Sans: variable font — un archivo cubre 400 y 600.
   IBM Plex Mono: instancias estáticas — un archivo por peso.
   ===================================================================== */
@font-face {
  font-family: 'Big Shoulders Display';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/big-shoulders-display-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: 'Big Shoulders Display';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url('fonts/big-shoulders-display-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: 'Big Shoulders Display';
  font-style: normal;
  font-weight: 700 900;
  font-display: swap;
  src: url('fonts/big-shoulders-display-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-024F, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-024F, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-024F, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-024F, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF;
}

/* =====================================================================
   TOKENS — modo oscuro (default) y modo claro
   ===================================================================== */
:root,
:root[data-theme="dark"] {
  /* --- superficies ---
     Casi negro con tinte verdeazulado, no azul marino. El cambio es el que
     hace que el color "prenda": medido sobre docs/guia_sinoptico.png, su
     página va a #01060E y su lienzo a #020E1B, y sobre esa oscuridad los
     mismos ámbar y verde que ya teníamos se leen mucho más vivos. La
     sensación de iluminación de esa referencia NO viene de ningún blur
     -- lo comprobé píxel a píxel: sus tuberías pasan del color pleno al
     fondo en un solo píxel -- sino de esta oscuridad y de la saturación. */
  --bg: #01060E;
  --panel: #031220;
  --panel2: #051826;
  --panel3: #0B2233;
  /* bordes con tinte de marca en vez de blanco puro: sobre superficies tan
     oscuras un borde blanco al 10 % se lee gris sucio */
  --border: rgba(96, 178, 208, 0.14);
  --border-strong: rgba(96, 178, 208, 0.26);
  --grid: rgba(96, 178, 208, 0.08);
  /* filete de 1 px que separa filas DENTRO de una tarjeta. Es un token
     aparte de --border porque no delimita una superficie, organiza una
     lista: va más claro que el borde y no se usa en los contornos. */
  --hairline: #153347;
  /* centro de la vineta del fondo. Antes el degradado radial del body usaba
     --panel2, que al oscurecerse se quedo a 4 puntos del fondo y la vineta
     dejo de existir. Con tono propio la pantalla vuelve a tener un centro mas
     claro que las esquinas, que es profundidad gratis. */
  --vineta: #0B2436;
  /* centro del lavado radial que va DENTRO del svg. La vineta del body
     existia pero el rectangulo del lienzo la tapaba, asi que el plano
     era plano de verdad: el mismo valor en el centro y en las esquinas. */
  --luz-sala: #123A56;

  /* --- texto --- */
  --text: #EAF3F8;
  --text-dim: #7C8CA3;

  /* --- marca Frozz Colombia (del logo: navy + swoosh celeste) --- */
  --brand: #4FC8E8;
  --brand-light: #8FE0F5;
  --brand-navy: #14203E;

  /* --- serie de datos (paleta categórica validada contra daltonismo,
     ver dataviz skill; idéntica en ambos modos) --- */
  --s1: #3987e5;
  --s2: #d95926;
  --s3: #199e70;
  --s4: #c98500;

  /* --- estado --- */
  --ok: #2FCB6B;
  --warn: #FBBF24;
  --critical: #F0473F;

  /* --- tipografía --- */
  --font-display: 'Big Shoulders Display', 'Arial Narrow', sans-serif;
  --font-body: 'IBM Plex Sans', -apple-system, 'Segoe UI', Arial, sans-serif;
  --font-mono: 'IBM Plex Mono', 'Consolas', 'Liberation Mono', monospace;
  --fs-kpi: clamp(1.9rem, 3vw, 3rem);
  --fs-meter: clamp(1.05rem, 1.6vw, 1.5rem);

  /* --- espaciado / radios / sombras (radios chicos y deliberados —
     el chaflán del hero hace el trabajo de "elemento importante",
     no un radio grande) --- */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px;
  --r-sm: 3px; --r-md: 5px; --r-lg: 8px;
  --shadow-glow-ok: 0 0 16px rgba(47, 203, 107, 0.35);
  --shadow-glow-alarm: 0 0 18px rgba(240, 71, 63, 0.45);
  /* movimiento: curvas fuertes (las de CSS se quedan cortas) y la duracion
     de la respuesta al pulsar. Ver el bloque MOVIMIENTO al final. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --dur-pulsar: 160ms;
}

:root[data-theme="light"] {
  /* ORDEN DE PLANOS: papel > instrumento > pared.
     Antes el lienzo del plano (--canvas) era mas oscuro que las tarjetas, y el
     sinoptico -- que es el heroe de la vista -- se leia como deshabilitado. El
     modo claro no es "el oscuro invertido": es la version IMPRESA del mismo
     plano, asi que la hoja va en blanco y todo lo demas se hunde por debajo. */
  --bg: #DCE3EC;
  --panel: #F4F7FA;
  --panel2: #E9EFF6;
  --panel3: #DAE3ED;
  --border: rgba(16, 24, 38, 0.13);
  --border-strong: rgba(16, 24, 38, 0.24);
  --grid: rgba(16, 24, 38, 0.08);
  --hairline: #D8E0EA;
  --vineta: #FFFFFF;

  --text: #101826;
  --text-dim: #57647A;

  /* #4FC8E8 no pasa AA sobre blanco -> se oscurece; se guarda el original
     como --brand-light para rellenos/decoración. En claro, brand-navy pasa
     a ser el acento de más peso (encabezados) para que el modo claro tenga
     identidad propia y no sea solo "invertir" el oscuro. */
  --brand: #0E7C9B;
  --brand-light: #4FC8E8;
  --brand-navy: #14203E;

  --s1: #3987e5;
  --s2: #d95926;
  --s3: #199e70;
  --s4: #c98500;

  /* variantes oscurecidas para cumplir contraste AA de texto sobre blanco;
     el relleno de LEDs/glow queda igual de reconocible con estos tonos */
  --ok: #047857;
  --warn: #B45309;
  --critical: #DC2626;

  --shadow-glow-ok: 0 0 14px rgba(4, 120, 87, 0.25);
  --shadow-glow-alarm: 0 0 16px rgba(220, 38, 38, 0.3);
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
}

body {
  background:
    radial-gradient(1400px 800px at 50% -10%, var(--vineta) 0%, var(--bg) 58%);
  min-height: 100vh;
}

.mono { font-family: var(--font-mono); }
.hidden { display: none !important; }

button, input { font-family: inherit; }
:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* --- iconos (sprite SVG, ver templates/_icons.html) --- */
.icon {
  width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto;
}

/* ===================== SPLASH ===================== */
#splash {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-4);
  background: var(--bg);
  transition: opacity 0.6s ease;
}
#splash.splash-out { opacity: 0; pointer-events: none; }
.splash-card {
  background: #FFFFFF;
  border-radius: var(--r-lg);
  padding: 36px 56px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
  animation: splashIn 0.7s cubic-bezier(.2, .8, .3, 1) both;
}
.splash-logo { display: block; height: 140px; padding-block: 12px; width: auto; }
.splash-tagline {
  font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.06em;
  color: var(--text-dim); animation: splashFadeUp 0.8s 0.25s both;
}
.splash-loader {
  width: 160px; height: 3px; border-radius: 3px; background: var(--panel3); overflow: hidden;
  animation: splashFadeUp 0.8s 0.35s both;
}
.splash-loader::after {
  content: ''; display: block; height: 100%; width: 40%; border-radius: 3px;
  background: linear-gradient(90deg, var(--brand), var(--brand-light));
  animation: splashLoad 1.4s ease-in-out infinite;
}
@keyframes splashIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
@keyframes splashFadeUp { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@keyframes splashLoad { 0% { transform: translateX(-100%); } 100% { transform: translateX(350%); } }
@media (prefers-reduced-motion: reduce) {
  .splash-card, .splash-tagline, .splash-loader { animation: none; }
  .splash-loader::after { animation: none; width: 100%; }
}

/* ===================== HEADER + TABS ===================== */
/* Cabecera y pestanas van dentro de un unico contenedor pegajoso. Antes cada
   una era sticky por su cuenta y #nav-tabs usaba top:72px, un numero que nunca
   coincidia con la altura real de la cabecera (con el padding, el logo de 56px
   y el borde son ~77px). Al angostar la ventana la cabecera ademas envuelve a
   dos lineas y crecia mas, tapando la barra de pestanas; por debajo de 560px
   el logo baja a 40px y entonces sobraba hueco. Envolviendolas desaparece la
   dependencia de adivinar esa altura. */
#cabecera { position: sticky; top: 0; z-index: 50; }
#topbar {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 10px var(--sp-4);
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  flex-wrap: wrap;
}
/* Un solo archivo, a color, en los dos temas: el PNG ya trae fondo
   transparente, asi que no hace falta ninguna tarjeta detras. Sobre la
   cabecera oscura el azul de marca queda en 2,68:1 -- se ve algo apagado,
   y es una decision tomada a proposito para respetar el color. Si Frozz
   entrega una version oficial para fondo oscuro, basta con cambiar el
   archivo y anadir aqui el par claro/oscuro. */
/* Es un SVG sacado del PDF oficial (docs/Logo_20Frozz_20Color-R.pdf), nitido
   a cualquier tamano. Va recortado al dibujo, sin el margen que traia el PNG
   anterior: el acolchado de 5 px devuelve ese margen para que el logo se vea
   del mismo tamano que antes y la cabecera conserve su alto. */
.logo { height: 56px; padding-block: 5px; width: auto; flex: 0 0 auto; }
#titulo-wrap { flex: 1 1 auto; min-width: 0; }
#titulo-principal {
  font-family: var(--font-display); font-size: 1.5rem; font-weight: 900; letter-spacing: 0.005em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.1;
}
#subtitulo {
  font-family: var(--font-body); font-size: 0.68rem; font-weight: 600;
  color: var(--brand); letter-spacing: 0.05em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#ts-text {
  font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-dim); white-space: nowrap;
}

/* Boton de /sistema: mismo tratamiento visual que el de tema, porque son
   los dos accesos globales que viven en la cabecera. */
/* Pie de un control: el rango que admite el equipo, el modo de una salida.
   Informacion que ayuda a decidir el valor, no un resultado. */
.ctrl-hint {
  font-size: 0.68rem; color: var(--text-dim); letter-spacing: 0.01em;
  margin-top: 4px; min-height: 1em;
}

#btn-sistema {
  flex: 0 0 auto; width: 36px; height: 36px; display: grid; place-items: center;
  background: var(--panel2); border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--text-dim); text-decoration: none;
  transition: background 0.15s, color 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
#btn-sistema:hover { background: var(--panel3); color: var(--text); }
#btn-sistema .icon { width: 18px; height: 18px; }

/* Ventana de tiempo. Es un desplegable y no una fila de campos porque la
   cabecera ya va justa de ancho y el rango horario se toca de vez en
   cuando, mientras que saber en que ventana estas se mira siempre: por eso
   el resumen queda a la vista y los campos debajo. */
#barra-ventana { position: relative; flex: 0 0 auto; }
#btn-ventana {
  appearance: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  height: 36px; padding: 0 10px;
  background: var(--panel2); border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--text-dim); font-family: inherit; font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.02em; white-space: nowrap;
  transition: background 0.15s, color 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
#btn-ventana:hover { background: var(--panel3); color: var(--text); }
#btn-ventana .icon { width: 16px; height: 16px; }
#btn-ventana[aria-expanded="true"] { background: var(--panel3); color: var(--text); border-color: var(--brand); }
/* En rango historico deja de estar "en vivo": el color lo dice sin leer. */
#btn-ventana.es-rango { color: var(--brand); border-color: var(--brand); }

#ventana-panel {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 70;
  display: flex; flex-direction: column; gap: var(--sp-2);
  min-width: 230px; padding: var(--sp-3);
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
}
#ventana-panel.hidden { display: none; }

#theme-toggle, #orient-toggle {
  appearance: none; cursor: pointer; flex: 0 0 auto;
  width: 36px; height: 36px; display: grid; place-items: center;
  background: var(--panel2); border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--text-dim); transition: background 0.15s, color 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
#theme-toggle:hover, #orient-toggle:hover { background: var(--panel3); color: var(--text); }
#theme-toggle .icon, #orient-toggle .icon { width: 18px; height: 18px; }
/* el icono dice como se ve AHORA */
#orient-toggle .icon-orient-h { display: none; }
:root[data-orient="horizontal"] #orient-toggle .icon-orient-v { display: none; }
:root[data-orient="horizontal"] #orient-toggle .icon-orient-h { display: inline-block; }
/* ACCESO REMOTO DE SOLO LECTURA (puerto PORT_WEB_REMOTO de app.py). El
   servidor ya rechaza toda escritura; aqui se quitan los controles para que
   nadie pulse un boton que no va a hacer nada. .det-cabecera es flex con
   wrap, asi que el panel de estado se queda con la fila entera. */
:root[data-solo-lectura] #det-controlbar,
:root[data-solo-lectura] .controlbar,
:root[data-solo-lectura] .bar-msg { display: none !important; }
/* Ocupa el hueco del boton del teclado y se viste como el de la ventana de
   tiempo: es informacion, no un control, por eso no cambia al pasar encima. */
#marca-solo-lectura {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px;
  height: 36px; padding: 0 10px;
  background: var(--panel2); border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--brand); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em;
  white-space: nowrap; cursor: default;
}
#marca-solo-lectura .icon { width: 16px; height: 16px; }
/* en vertical la pastilla se come el hueco de la hora y de ella quedaba un
   digito suelto asomando: mejor sin hora (la dice el reloj de las graficas) */
:root[data-solo-lectura][data-orient="vertical"] #ts-text { display: none; }

#theme-toggle .icon-moon { display: none; }
:root[data-theme="light"] #theme-toggle .icon-sun { display: none; }
:root[data-theme="light"] #theme-toggle .icon-moon { display: inline-block; }

#nav-tabs {
  position: relative; /* contiene a #tab-indicador */
  display: flex; gap: 2px; padding: 0 var(--sp-3);
  background: var(--panel); border-bottom: 1px solid var(--border);
  overflow-x: auto;
}
.nav-tab {
  appearance: none; background: transparent; border: none; cursor: pointer;
  font-family: var(--font-display); color: var(--text-dim); font-weight: 700;
  font-size: 0.95rem; letter-spacing: 0.02em;
  padding: 10px 20px; border-bottom: 3px solid transparent; white-space: nowrap;
  transition: color 0.15s, border-color 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
.nav-tab:hover { color: var(--text); }
/* Cada pestana lleva dos etiquetas: la larga para pantalla ancha y una
   corta que la reemplaza en movil. Con seis pestanas repartiendose la
   barra inferior, "MULTIPOWER" no entra ni bajando la fuente. */
.tab-corta { display: none; }
.nav-tab.active { color: var(--brand); border-bottom-color: var(--brand); }

/* ===================== LAYOUT GENERAL ===================== */
/* La cabecera va a sangre; el contenido tambien. Un contenedor mas
   estrecho que la barra dejaba una franja muerta en el borde derecho que
   a 4 m se lee como pagina rota. */
.view { padding: var(--sp-4) var(--sp-4) 48px; margin: 0; text-align: left; }

.graph-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-3); min-width: 0; }
.graph-box {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: inset 0 1px 0 rgba(180, 236, 247, 0.07);
  padding: var(--sp-3) var(--sp-3) var(--sp-1);
  min-width: 0; /* permite que el item de grid se encoja aunque el <canvas> de adentro tenga su propio ancho intrínseco */
}
.graph-title {
  font-family: var(--font-body); font-weight: 600; font-size: 0.72rem; color: var(--text-dim);
  letter-spacing: 0.03em; margin-bottom: var(--sp-2);
}
.graph-canvas-wrap { position: relative; height: 210px; min-width: 0; }
.graph-canvas-wrap canvas { max-width: 100%; }
/* sin lecturas no se dibuja una rejilla con una escala inventada por el
   autoescalado: se dice que no hay nada */
.graph-canvas-wrap.is-empty canvas { visibility: hidden; }
.graph-canvas-wrap.is-empty::after {
  content: 'SIN LECTURAS'; position: absolute; inset: 0;
  display: grid; place-items: center;
  font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.08em; color: var(--text-dim);
}

.section-title {
  font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; color: var(--text);
  letter-spacing: 0.01em; border-bottom: 2px solid var(--border);
  padding-bottom: var(--sp-2); margin: var(--sp-5) 0 var(--sp-3);
}
.section-title:first-child { margin-top: 0; }

/* ===================== DASHBOARD ===================== */
/* Aviso del panel interno /sistema (esa página sí anuncia "equipo normal") */
.alarm-banner {
  display: flex; align-items: center; gap: var(--sp-3);
  border-radius: var(--r-md); padding: 12px var(--sp-4); margin-bottom: var(--sp-3);
  font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; cursor: default;
}
.alarm-banner .icon { width: 22px; height: 22px; flex: 0 0 auto; }
.alarm-banner.state-alarm { cursor: pointer; animation: alarmPulse 1.8s ease-in-out infinite; }

/* Franja de aviso de la planta, al estilo de un tablero y no de una alerta
   web: bloque de estado sólido a la izquierda, sin translúcidos ni esquinas
   blandas. No tiene variante "todo bien": un HMI no anuncia la normalidad,
   y afirmarla mientras media pantalla está en SIN DATO es peor que callar. */
.alarm-bar {
  display: flex; align-items: stretch;
  border-radius: 2px; margin-bottom: var(--sp-3); overflow: hidden;
  font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; cursor: default;
}
.alarm-bar.is-hidden { display: none; }
.alarm-bar > span:last-child {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 10px var(--sp-4); flex: 1 1 auto;
}
.alarm-banner-mark {
  flex: 0 0 auto; width: 40px; display: grid; place-items: center;
  font-family: var(--font-mono); font-size: 1.2rem; font-weight: 600;
  color: var(--bg);
}
.alarm-bar.state-alarm { cursor: pointer; animation: alarmPulse 1.8s ease-in-out infinite; }
.alarm-banner-detail {
  font-family: var(--font-body); font-weight: 500; color: var(--text-dim);
  margin-left: var(--sp-2); font-size: 0.78rem;
}
/* El latido de la alarma iba animando un box-shadow de 16 px de desenfoque, o
   sea repintando una sombra difuminada en cada fotograma, justo cuando mas
   falta hace que la interfaz responda. Un cambio de color de fondo es un
   relleno plano y se ve igual de urgente. */
@keyframes alarmPulse {
  0%, 100% { background-color: var(--banner-alarm-bg); }
  50% { background-color: var(--banner-alarm-bg2); }
}
@media (prefers-reduced-motion: reduce) { .alarm-banner.state-alarm, .alarm-bar.state-alarm { animation: none; } }

/* --- tiras KPI (las usa templates/sistema.html) --- */
.sis-kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-3); margin-bottom: var(--sp-3); }
@media (max-width: 980px) { .sis-kpi-grid { grid-template-columns: repeat(2, 1fr); } }
.kpi-strip {
  flex: 1 1 0; background: var(--panel); border: 1px solid var(--border);
  border-top: 3px solid var(--mc, var(--brand)); border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3); display: flex; flex-direction: column; justify-content: center;
}
.kpi-label {
  font-family: var(--font-body); font-weight: 600; font-size: 0.68rem;
  color: var(--text-dim); letter-spacing: 0.03em;
}
.kpi-value {
  font-family: var(--font-mono); font-size: var(--fs-kpi); font-weight: 600;
  color: var(--mc, var(--brand)); margin: 2px 0;
  font-variant-numeric: tabular-nums; line-height: 1.05;
}
.kpi-strip .kpi-value { font-size: clamp(1.3rem, 2.2vw, 1.9rem); }
.kpi-sub { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-dim); }

/* ===================== LEDS ===================== */
.led {
  width: 12px; height: 12px; border-radius: 50%; background: var(--border-strong);
  display: inline-block; flex: 0 0 auto;
}
.led-ok { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
.led-alarm { background: var(--critical); box-shadow: 0 0 8px var(--critical); }
.led-warn { background: var(--warn); box-shadow: 0 0 8px var(--warn); }
.led-off { background: var(--border-strong); }
/* parpadeo por opacity: es lo barato en una Raspberry (no repinta sombras) */
.led-blink { animation: ledBlink 1.1s steps(1, end) infinite; }
@keyframes ledBlink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.2; } }
@media (prefers-reduced-motion: reduce) { .led-blink { animation: none; } }

/* ===================== METERS (RCK / VX / MULTIPOWER) ===================== */
.meters-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-3); margin-bottom: var(--sp-3); }
.mp-meters-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--sp-2); margin-bottom: var(--sp-2); }
.meter-box {
  background: var(--panel); border: 1px solid var(--border); border-top: 3px solid var(--mc, var(--brand));
  border-radius: var(--r-sm); padding: var(--sp-3) var(--sp-2); text-align: left;
}
.meter-label { font-family: var(--font-body); font-weight: 600; font-size: 0.64rem; color: var(--text-dim); margin-bottom: var(--sp-1); letter-spacing: 0.02em; }
.meter-value { font-family: var(--font-mono); font-size: var(--fs-meter); font-weight: 600; color: var(--mc, var(--brand)); font-variant-numeric: tabular-nums; }
.meter-unit { font-family: var(--font-mono); font-size: 0.66rem; color: var(--mc, var(--brand)); opacity: 0.75; margin-top: 2px; }

/* ===================== TARJETAS DE LECTURAS (vistas de detalle) =====================
   El mismo lenguaje que las tarjetas del resumen: una tarjeta por SECCION, no
   una caja por lectura; etiqueta en versalitas encima y numero grande con la
   unidad al lado. Las filas son .side-row/.side-val, las mismas del resumen,
   asi que el estado vacio (is-vacio) tambien es el mismo. */
.lect-secciones {
  display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-3);
}
/* Cada tarjeta pide ancho en proporcion a sus lecturas (--n). Una seccion de
   una sola lectura ("Lazo de agua") ya no se estira a 1800 px con el numero
   perdido en una esquina: comparte la fila con su vecina. */
.lect-panel {
  flex: var(--n, 1) 1 calc(var(--n, 1) * 200px); min-width: 0;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4) var(--sp-2);
  box-shadow: inset 0 1px 0 rgba(180, 236, 247, 0.05);
}
.lect-titulo {
  margin: 0 0 var(--sp-1);
  font-family: var(--font-display); font-weight: 700; font-size: 1.05rem;
  color: var(--text); letter-spacing: 0.01em;
}
.lect-grid {
  /* auto-fit: las lecturas se reparten TODO el ancho de su tarjeta, sin
     pistas vacias a la derecha */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  column-gap: var(--sp-4);
}
.lect-grid .side-row { border-top: 1px solid var(--hairline); padding: var(--sp-2) 0; }

/* Controles y estado LADO A LADO. La barra de controles mide lo que sus
   botones; el hueco que antes quedaba a su derecha lo ocupa el estado del
   equipo, con sus alarmas a la vista sin bajar. */
.det-cabecera {
  display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: stretch;
  margin-bottom: var(--sp-3);
}
.det-cabecera > #det-controlbar,
.det-cabecera > .controlbar {
  /* los cuatro controles del RCK caben en una fila de ~600 px; sin tope la
     barra tomaba el ancho de TODO su contenido en una linea (incluido el
     desplegable de mantenimiento) y dejaba un hueco a la derecha */
  flex: 0 1 auto; max-width: 640px; margin-bottom: 0; align-content: flex-start;
}
.det-cabecera > .status-panel { flex: 1 1 360px; margin-bottom: 0; }
/* el mensaje del ultimo envio ya no reserva 160 px: aparece debajo de los
   controles cuando hay algo que decir */
.det-cabecera .bar-msg { flex: 1 1 100%; }
.det-cabecera .bar-msg:empty { display: none; }

/* ===================== ESTADO / ALARMAS (RCK y MULTIPOWER) ===================== */
.status-panel {
  background: var(--panel); border: 1px solid var(--border); border-top: 3px solid var(--brand);
  border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-3);
}
.status-title {
  font-family: var(--font-body); font-weight: 600; font-size: 0.74rem; color: var(--text-dim);
  border-bottom: 1px solid var(--border); padding-bottom: var(--sp-2); margin-bottom: var(--sp-2);
  letter-spacing: 0.02em;
}
.status-leds { display: flex; gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-2); }
.led-row { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-body); font-size: 0.82rem; }
.status-line { display: flex; justify-content: space-between; border-top: 1px solid var(--border); padding-top: var(--sp-2); margin-bottom: var(--sp-2); font-family: var(--font-body); font-size: 0.85rem; }
.status-value { font-family: var(--font-mono); font-weight: 600; font-variant-numeric: tabular-nums; }
.status-alarmas-title {
  font-family: var(--font-body); font-weight: 600; font-size: 0.7rem; color: var(--text-dim);
  border-top: 1px solid var(--border); padding-top: var(--sp-2); letter-spacing: 0.02em;
}
.status-alarmas { font-family: var(--font-mono); color: var(--ok); font-size: 0.82rem; white-space: pre-line; margin: var(--sp-1) 0 var(--sp-2); }
.status-comm {
  display: flex; align-items: center; gap: 6px; justify-content: center;
  font-family: var(--font-body); font-weight: 600; font-size: 0.78rem; color: var(--text-dim);
}
.status-comm .icon { width: 15px; height: 15px; }

/* ===================== BARRA DE CONTROL (solo vista RCK) ===================== */
/* Barra de controles de escritura. La usan las cuatro pestañas que tienen
   algo que escribir. Tiene que ser un contenedor flex: .ctrl-group es un flex
   en columna y, suelto, se estira a todo el ancho y arrastra al boton con el. */
#det-controlbar, .controlbar {
  background: var(--panel); border: 1px solid var(--border); border-top: 3px solid var(--brand);
  border-radius: var(--r-md); margin-bottom: var(--sp-3);
  padding: var(--sp-4); display: flex; gap: var(--sp-4); flex-wrap: wrap; align-items: flex-end;
}
.ctrl-group { display: flex; flex-direction: column; gap: var(--sp-1); }
.ctrl-label { font-family: var(--font-body); font-weight: 600; font-size: 0.62rem; color: var(--text-dim); text-align: center; letter-spacing: 0.02em; }
.ctrl-inline { display: flex; gap: 6px; }
.btn-wide { min-width: 112px; padding: 12px 8px; border-radius: var(--r-sm); font-family: var(--font-body); font-weight: 600; font-size: 0.78rem; cursor: pointer; line-height: 1.25; }
.btn-sm { padding: 10px 14px; border-radius: var(--r-sm); font-family: var(--font-body); font-weight: 600; font-size: 0.74rem; cursor: pointer; line-height: 1.2; }
.btn-state { background: var(--panel2); border: 1.6px solid var(--ok); color: var(--text); }
.btn-yellow-outline { background: var(--panel2); border: 1.6px solid var(--warn); color: var(--warn); }
.btn-accent-outline { background: var(--panel2); border: 1.6px solid var(--brand); color: var(--brand); }
.btn-cyan-outline { background: var(--panel2); border: 1.6px solid var(--brand); color: var(--brand); }
.btn-green-outline { background: var(--panel2); border: 1.6px solid var(--ok); color: var(--ok); }
.btn-dim-outline { background: var(--panel2); border: 1.6px solid var(--text-dim); color: var(--text); }
.btn-wide:hover, .btn-sm:hover { background: var(--panel3); }
.btn-active-yellow { background: rgba(251, 191, 36, 0.2) !important; color: var(--warn) !important; font-weight: 700; }
.input-box { width: 72px; padding: 10px; border-radius: var(--r-sm); border: 1.5px solid var(--brand); background: var(--panel2); color: var(--brand); font-family: var(--font-mono); font-size: 0.95rem; text-align: center; }
.input-box-sm { width: 64px; padding: 10px; border-radius: var(--r-sm); border: 1.5px solid var(--text-dim); background: var(--panel2); color: var(--text); font-family: var(--font-mono); font-size: 0.9rem; text-align: center; }
.bar-msg { font-family: var(--font-body); font-size: 0.72rem; color: var(--text-dim); flex: 1 1 160px; align-self: center; }
.view-msg { display: flex; align-items: center; gap: 6px; font-family: var(--font-body); font-weight: 600; font-size: 0.76rem; color: var(--ok); align-self: center; }
.live-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: 0 0 auto; background: currentColor; }

#ctrl-advanced { flex-basis: 100%; }
#ctrl-advanced summary {
  cursor: pointer; display: flex; align-items: center; gap: 6px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.7rem; color: var(--text-dim);
  padding: var(--sp-2) 0; list-style: none;
}
#ctrl-advanced summary::-webkit-details-marker { display: none; }
.summary-chevron { transition: transform 0.15s ease; }
#ctrl-advanced[open] summary .summary-chevron { transform: rotate(90deg); }
#ctrl-advanced-body { display: flex; gap: var(--sp-4); flex-wrap: wrap; align-items: flex-end; padding-top: var(--sp-2); }

/* ===================== SLOT: LOGO DEL EVENTO ===================== */
#event-logo-slot {
  position: fixed; left: var(--sp-4); bottom: var(--sp-4); z-index: 40;
  max-height: 52px; max-width: 160px; opacity: 0.92; pointer-events: none;
}

/* ===================== RESPONSIVE ===================== */
@media (min-width: 1600px) {
  :root { --fs-kpi: clamp(2.2rem, 2.4vw, 3.4rem); }
}

@media (max-width: 980px) {
  .meters-grid-3 { grid-template-columns: repeat(2, 1fr); }
  .graph-grid-2 { grid-template-columns: 1fr; }
  #titulo-principal { font-size: 1.2rem; }
  .nav-tab { padding: 10px 14px; font-size: 0.85rem; }
}

@media (max-width: 560px) {
  .view { padding: var(--sp-3) var(--sp-2) 84px; }
  /* la barra de pestanas es fija abajo: el contenido necesita ese hueco o
     se le monta encima y tapa la ultima tarjeta */
  #view-dashboard { padding-bottom: 84px; }
  /* Sin nowrap la cabecera se partia en dos filas al añadir los botones de
     ventana y de sistema: el titulo empujaba y el ultimo boton bajaba solo. */
  #topbar { padding: 8px var(--sp-2); gap: 6px; flex-wrap: nowrap; }
  /* Medido a 360 px: el titulo pedia 151 px y tenia 127, porque logo (85) +
     tres botones (108) + huecos (24) llenaban justo los 344 utiles. Se le
     ceden esos 24 px bajando el cuerpo del titulo y encogiendo botones y
     separaciones, que es de donde menos se echa en falta. */
  #titulo-principal { font-size: 1rem; }
  .logo { height: 32px; padding-block: 3px; }
  #theme-toggle, #orient-toggle, #btn-sistema, #btn-ventana, #btn-teclado { width: 32px; height: 32px; }
  #btn-ventana { padding: 0; }
  #ventana-resumen { display: none; }
  /* en el telefono solo el ojo: el texto no cabe junto a los demas botones */
  #marca-solo-lectura { width: 32px; height: 32px; padding: 0; justify-content: center; }
  #marca-solo-lectura .marca-texto { display: none; }
  #btn-ventana { padding: 0 9px; }
  #ventana-panel { right: auto; left: 0; }
  #ts-text { display: none; }
  .mp-meters-grid { grid-template-columns: repeat(2, 1fr); }
  #det-controlbar { justify-content: center; }
  .btn-wide, .btn-sm { min-width: 90px; }
  #event-logo-slot { display: none; } /* choca con la barra de pestañas fija abajo */

  #nav-tabs {
    position: fixed; top: auto; bottom: 0; left: 0; right: 0; z-index: 60;
    justify-content: space-around; padding: 0; border-top: 1px solid var(--border); border-bottom: none;
    box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.4);
  }
  .nav-tab { flex: 1 1 0; min-width: 0; text-align: center; padding: 12px 2px; border-bottom: none; border-top: 3px solid transparent; font-size: 0.7rem; }
  .nav-tab.active { border-top-color: var(--brand); border-bottom-color: transparent; }
}

/* =====================================================================
   SINOPTICO DEL PROCESO (vista RESUMEN)

   El diagrama es SVG en linea, asi que resuelve var(--...) por si solo y
   el tema claro/oscuro lo maneja la misma tabla de tokens de arriba.

   Presupuesto de animacion (esto corre en una Raspberry Pi): solo
   stroke-dashoffset y opacity, y el flujo se anima unicamente en los
   tramos que de verdad tienen caudal -- si un compresor esta parado su
   descarga se queda quieta -- y a baja frecuencia. Nada de filtros, blur
   ni sombras dentro del SVG.
   ===================================================================== */

/* --- tokens propios del plano ---------------------------------------
   Un fluido, un color. El agua tibia se queda en --s2 para que coincida
   con la serie "Entrada" de la grafica de tendencia; por eso el lado de
   alta del refrigerante paso a rojo ladrillo (gas caliente) y verde
   (linea de liquido), que ademas es la convencion de refrigeracion. */
:root,
:root[data-theme="dark"] {
  --canvas: rgb(2, 14, 27);
  --sv-dim: rgb(151, 166, 188);
  /* La pared deja de ser un gris azulado visible y pasa a rozar el lienzo:
     en la referencia el tubo es una banda maciza de color, sin contorno.
     Se conserva la capa porque en tema claro sí hace falta un borde. */
  --pipe-wall: rgb(9, 28, 45);
  --pipe-core: rgb(42, 56, 84);
  --pipe-hot: rgb(214, 90, 78);
  --pipe-liq: rgb(38, 180, 128);
  --pipe-suc: rgb(79, 200, 232);
  --pipe-cold: rgb(57, 135, 229);
  /* ambar: el naranja anterior era casi el mismo rojo de la descarga y
     sus tramos corren en la misma mitad de pantalla */
  --pipe-warm: rgb(222, 166, 62);
  /* gris de "aqui se mide, pero el sensor todavia no esta conectado". Es un
     token por tema y no un opacity: un opacity<1 sobre un grupo obliga al
     navegador a un bufer fuera de pantalla en cada repintado, y son diez
     grupos repintandose cada segundo en un Raspberry Pi. */
  /* Medido: rgb(96,110,132) daba 3,76:1 sobre el lienzo, por debajo del
     minimo AA de 4,5:1 -- y es el color de casi todo el texto del plano
     mientras no lleguen los registros. #8794A8 da 6,32:1 y sigue
     leyendose como atenuado. */
  --sv-faint: #8794A8;
  /* TONO CLARO DE CADA FLUIDO, para el pulso que recorre el tubo.
     Es el detalle que más acerca el dibujo a la referencia: allí el tramo
     animado del tubo ámbar va a #E9D5A8, un ámbar pálido, NO a blanco. Un
     pulso blanco se lee como un reflejo pegado encima; uno del propio color
     se lee como luz por dentro del tubo. */
  /* vértice iluminado de los degradados de equipo. En la referencia el
     evaporador va de #071928 a #040F1B: un bloque no es un color plano,
     tiene una cara que recibe luz. Un degradado no obliga al navegador a
     pintar en un búfer aparte; un filtro sí, y eso es lo que no cabe en la
     Raspberry. */
  --eq-lift: #0C2A3E;
  --pipe-hot-lit: #F0B0A8;
  --pipe-liq-lit: #8FE6C6;
  --pipe-suc-lit: #B4ECF7;
  --pipe-cold-lit: #A8CDF6;
  --pipe-warm-lit: #F2DCA8;
  --pipe-pulse: rgba(255, 255, 255, 0.92);
  --banner-ok-bg: rgb(14, 40, 28);
  --banner-warn-bg: rgb(46, 37, 12);
  --banner-alarm-bg: rgb(56, 20, 20);
  /* --critical sobre el fondo de la propia banda se queda en 4,44:1, a un
     pelo del minimo, y el texto va a 16,8 px (por debajo del umbral de
     "texto grande"). Token propio: 5,89:1. --critical global no se toca,
     que funciona bien en LEDs y en el plano. */
  --banner-alarm-fg: #FF6B61;
  --banner-alarm-bg2: rgb(92, 26, 26);
}

:root[data-theme="light"] {
  /* el papel: la superficie MAS clara de la pantalla. Ver el comentario del
     orden de planos mas arriba. */
  --canvas: #FFFFFF;
  --sv-dim: rgb(64, 78, 99);
  /* Igual que en oscuro, la pared roza el lienzo en vez de ser un gris
     azulado visible: a 17 px bajo un nucleo de 12 dejaba 2,5 px de funda gris
     a cada lado de CADA tubo, la misma para los cinco fluidos, y eso aplanaba
     la paleta entera. */
  --pipe-wall: rgb(196, 206, 218);
  --pipe-core: rgb(197, 206, 220);
  /* en claro los trazos se oscurecen y saturan, no se aclaran */
  --pipe-hot: rgb(173, 45, 38);
  --pipe-liq: rgb(11, 116, 80);
  /* La succion valia rgb(11,116,146) y el glicol frio rgb(27,90,190). Medido
     con el validador de la skill dataviz, la succion quedaba a deltaE 10.6 de
     la linea de liquido en vision NORMAL -- por debajo de 15, o sea dificiles
     de distinguir incluso con vision de color completa -- y ademas leia como
     gris (croma 0.096, por debajo del minimo). Se reabren los dos azules: asi
     la paleta clara de seis tuberias pasa todos los chequeos. */
  /* rgb(21,150,210) daba 2,62:1 sobre el lienzo claro y era el unico tubo
     que "flotaba": WCAG 1.4.11 pide 3:1 a un elemento grafico que porta
     informacion, y un tubo de fluido lo es. #0E85BE da 3,23:1 y mantiene
     deltaE 20,7 contra el glicol frio, muy por encima del suelo de 15. */
  --pipe-suc: #0E85BE;
  --pipe-cold: rgb(26, 79, 175);
  --pipe-warm: rgb(158, 106, 12);
  /* rgb(132,142,158) daba 2,61:1 sobre el lienzo claro. #57647A da 4,72:1
     y es el mismo hex que --text-dim de este tema: el gris atenuado del
     plano y el del texto pasan a ser el mismo gris, que es lo correcto. */
  --sv-faint: #57647A;
  --eq-lift: #EDF2F8;
  --pipe-hot-lit: #D69691;
  --pipe-liq-lit: #7FB9A4;
  --pipe-suc-lit: #86C8E8;
  --pipe-cold-lit: #8CA7D7;
  --pipe-warm-lit: #CEB485;
  --pipe-pulse: rgba(255, 255, 255, 0.95);
  --banner-ok-bg: rgb(213, 238, 226);
  --banner-warn-bg: rgb(250, 235, 206);
  --banner-alarm-bg: rgb(252, 221, 219);
  /* en claro --critical daba 3,79:1 sobre la banda rosa; este da 5,47:1 */
  --banner-alarm-fg: #B31217;
  --banner-alarm-bg2: rgb(250, 196, 193);
}

/* ===================== BANDA DE ESTADO =====================
   Color plano de banda completa: el rgba() anterior dejaba ver el
   degradado radial del body y parecia un banner a medio cargar. */
.alarm-banner.state-ok { background: var(--banner-ok-bg); border: 1px solid rgba(47, 203, 107, 0.4); color: var(--ok); }
.alarm-banner.state-off { background: var(--banner-warn-bg); border: 1px solid rgba(251, 191, 36, 0.4); color: var(--warn); }
.alarm-banner.state-alarm { background: var(--banner-alarm-bg); border: 1px solid rgba(240, 71, 63, 0.5); color: var(--banner-alarm-fg); }

.alarm-bar.state-off { background: var(--banner-warn-bg); color: var(--warn); }
.alarm-bar.state-off .alarm-banner-mark { background: var(--warn); }
.alarm-bar.state-alarm { background: var(--banner-alarm-bg); color: var(--banner-alarm-fg); }
.alarm-bar.state-alarm .alarm-banner-mark { background: var(--critical); }

/* ===================== ESCENARIO DE LA VISTA RESUMEN =====================
   En pantalla de kiosco nadie hace scroll: el resumen entra completo en el
   viewport y lo que sobra se recorta.
   Esta regla base es la de pantalla angosta: el plano arriba y las tres
   tarjetas (VX, Multipower, tendencia) en una banda al pie. La grafica se lleva
   algo mas del doble que cada tarjeta de datos, porque las dos tarjetas son
   listas de tres lecturas y no ganan nada con mas ancho. El sinoptico es quien
   flexiona (minmax(0,1fr)): si aparece la barra de alarma, le cede alto en vez
   de empujar la banda fuera de pantalla.
   En el kiosco apaisado manda otra reparticion, al final del archivo. */
#view-dashboard {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--sp-3);
  align-content: start;
}
#dash-alarm-banner { margin-bottom: 0; }
/* BANDA CENTRAL EN TRES COLUMNAS.
   Las tarjetas de datos flanquean el plano y el plano queda de héroe en el
   medio, como en la guía de referencia. La tendencia NO vive aquí: baja a
   todo el ancho al final de la vista, que es donde gana espacio.
   En pantalla angosta esto degrada solo a una columna apilada. */
.dash-medio {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-3);
  min-height: 0;
}
/* Las dos tarjetas se van a los EXTREMOS de su columna en vez de apilarse
   arriba dejando media columna vacia. Siguen midiendo lo que mide su
   contenido -- no se estiran -- pero el hueco queda en medio y la columna se
   lee llena. */
.dash-col {
  display: flex; flex-direction: column; justify-content: space-between;
  gap: var(--sp-3); min-width: 0;
}
.graph-ribbon { margin-top: var(--sp-3); }

/* Lecturas en texto: en un teléfono el plano cabe pero no se lee, así que
   allí se sustituye por los números. En escritorio no existe. */
#dash-movil { display: none; }
.mv-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
.mv-cell { background: var(--panel); padding: 9px var(--sp-3); display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mv-cell-hero { grid-column: 1 / -1; }
.mv-k { font-family: var(--font-body); font-size: 0.68rem; font-weight: 600; color: var(--text-dim); }
.mv-v { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.mv-v b { font-size: 1.32rem; font-weight: 600; color: var(--text); }
.mv-cell-hero .mv-v b { font-size: 2.4rem; }

@media (min-width: 1000px) and (min-height: 640px) {
  html, body { height: 100dvh; overflow: hidden; }
  body { display: flex; flex-direction: column; }
  .view { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  #view-dashboard { overflow: hidden; padding-bottom: var(--sp-4); }
  #topbar { position: static; }
  #nav-tabs { position: relative; } /* sin sticky, pero sigue conteniendo a #tab-indicador */
}

/* ===================== PANEL DEL SINOPTICO ===================== */
.syn-panel {
  display: flex; flex-direction: column; min-height: 0;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  overflow: hidden;
  /* filete de luz cenital, no sombra: sobre casi negro una sombra difuminada
     no puede ser mas oscura que el fondo y no separa nada. Un inset de 0 px de
     desenfoque es un relleno de un pixel. */
  box-shadow: inset 0 1px 0 rgba(180, 236, 247, 0.07);
}
.syn-head {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--border);
}
.syn-title {
  font-family: var(--font-display); font-weight: 900; font-size: 1.32rem;
  letter-spacing: 0.005em; margin: 0; flex: 0 0 auto; line-height: 1.15;
}
.syn-status { display: flex; gap: var(--sp-4); flex-wrap: wrap; flex: 1 1 auto; }
.syn-st {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font-body); font-size: 0.78rem; font-weight: 600; color: var(--text-dim);
}
.syn-val {
  font-family: var(--font-mono); font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.04em; color: var(--text-dim);
}
.syn-val.is-on { color: var(--ok); }
.syn-val.is-warn { color: var(--warn); }
.syn-val.is-alarm { color: var(--critical); }
.syn-link {
  appearance: none; cursor: pointer; flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 6px;
  background: transparent; border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  color: var(--brand); font-family: var(--font-body); font-weight: 600; font-size: 0.78rem;
  padding: 7px 12px; transition: background 0.15s, border-color 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
.syn-link:hover { background: var(--panel2); border-color: var(--brand); }
.syn-link .icon { width: 14px; height: 14px; }

.syn-canvas {
  flex: 1 1 auto; min-height: 0; display: flex;
  background: var(--canvas);
  padding: var(--sp-2);
}
.syn-canvas .sv { display: block; width: 100%; height: 100%; }

/* leyenda de líneas: cinco códigos de color que el visitante tenía que
   deducir solo. Va en HTML para que no encoja con el dibujo. */
/* La leyenda se queda al PIE del panel. La probe en la cabecera, como la guia
   de referencia, y sale peor: con el titulo, los dos chips de estado y el boton
   de detalle ya ocupados, envuelve a una segunda linea y la cabecera pasa de 40
   a 81 px. El dibujo perdia 17 px de alto para ganar cero. */
.syn-legend {
  flex: 0 0 auto; list-style: none; margin: 0;
  padding: 3px var(--sp-4) 4px;
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--border);
  font-family: var(--font-body); font-size: 0.72rem; font-weight: 600; color: var(--text-dim);
}
.syn-legend li { display: inline-flex; align-items: center; gap: 7px; }
.lg { width: 20px; height: 5px; border-radius: 1px; flex: 0 0 auto; }
.lg-hot { background: var(--pipe-hot); }
.lg-liq { background: var(--pipe-liq); }
.lg-suc { background: var(--pipe-suc); }
.lg-cold { background: var(--pipe-cold); }
.lg-warm { background: var(--pipe-warm); }
/* La cascada no anade colores a la leyenda: anade una REGLA. Son dos muestras,
   una azul y una ambar, las dos con el centro abierto, porque en el plano no
   hay ninguna tuberia gris: las huecas llevan los mismos dos colores del agua.
   Una sola muestra neutra mandaba al visitante a buscar un tubo gris que no
   existe. */
.lg-hueco-cold {
  background: linear-gradient(to bottom,
    var(--pipe-cold) 0 1.5px, var(--pipe-core) 1.5px 3.5px, var(--pipe-cold) 3.5px 5px);
}
.lg-hueco-warm {
  background: linear-gradient(to bottom,
    var(--pipe-warm) 0 1.5px, var(--pipe-core) 1.5px 3.5px, var(--pipe-warm) 3.5px 5px);
}

/* --- superficies del dibujo: la tarjeta, el equipo y el plano son tres
   valores distintos, no tres blancos --- */
.sv-block { fill: url(#g-zona); stroke: var(--border-strong); stroke-width: 1.5; }
/* el lazo de agua es el corazon del plano: va en un plano propio, un paso
   por encima de las dos zonas de refrigerante */
.sv-block-w { fill: url(#g-zona-w); }
.sv-machine { fill: url(#g-eq); stroke: var(--text); stroke-width: 2.4; }
/* el compresor lleva el símbolo del plano del cliente, no la diagonal de
   una bomba genérica: con la diagonal, compresor y bomba quedaban como dos
   círculos casi iguales en la misma pantalla */
.sv-machine-mark { fill: none; stroke: var(--text); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.sv-hx { fill: none; stroke: var(--sv-dim); stroke-width: 1.2; opacity: 0.55; }
.sv-fan { fill: none; stroke: var(--sv-dim); stroke-width: 1.6; }
.sv-fan circle { fill: var(--panel3); }
/* VENTILADOR EN MARCHA: giran las aspas, no el aro. Solo con evidencia --
   rele FAN del controlador del cuarto, o % del condensador en el circuito 1 --
   porque un transform en SVG no sube a capa propia y Chromium repinta el
   ventilador en cada fotograma. 7 como maximo, a 1,6 s la vuelta: se lee como
   "en marcha" sin pedirle a la Raspberry mas de lo necesario. */
.sv-fans.is-spinning .sv-fan-aspas {
  transform-box: fill-box; transform-origin: center;
  animation: svFan 1.6s linear infinite;
}
.sv-fans.is-spinning .sv-fan { stroke: var(--text); }
@keyframes svFan { to { transform: rotate(360deg); } }

/* la EEV 1 solo se lee a traves del VX-1225: sin el, se retira del plano */
.sv-sin-vx .sv-solo-vx { display: none; }
.sv-pump { fill: var(--panel3); }
.sv-pump-vane { fill: var(--sv-dim); stroke: none; }
/* bocas de intercambiador: al grosor de la línea y del color del fluido
   que entra. Blancas y grandes parecían tiradores de un editor. */
.sv-node circle { stroke: none; }
.n-hot  { fill: var(--pipe-hot); }
.n-liq  { fill: var(--pipe-liq); }
.n-suc  { fill: var(--pipe-suc); }
.n-cold { fill: var(--pipe-cold); }
.n-warm { fill: var(--pipe-warm); }

/* --- capa de luz: un rectangulo y tres elipses, lo primero que se pinta.
   En tema claro la luz se invierte (el papel se queda blanco en el centro y
   se hunden las esquinas) y las lamparas de equipo se apagan: una hoja
   impresa no tiene equipos que brillen. --- */
.sv-luz { fill: url(#g-luz); }
.sv-eqluz { fill: url(#g-eqluz); }
:root[data-theme="light"] .sv-luz { fill: url(#g-luz-claro); }
:root[data-theme="light"] .sv-eqluz { display: none; }

/* el cuarto frio es obra civil, no un equipo: muro doble con trama de
   aislamiento y la puerta insinuada, no un punteado de CSS */
.sv-room-wall { fill: var(--panel3); stroke: var(--sv-dim); stroke-width: 1.8; }
.sv-room-in { fill: var(--canvas); stroke: var(--sv-dim); stroke-width: 1.2; }
.sv-room-hatch { fill: none; stroke: var(--sv-dim); stroke-width: 1; opacity: 0.65; }
.sv-room-door { fill: none; stroke: var(--canvas); stroke-width: 5; stroke-linecap: butt; }
.sv-room-jamb { fill: none; stroke: var(--sv-dim); stroke-width: 1.8; stroke-linecap: round; }
.sv-fins { fill: none; stroke: var(--sv-dim); stroke-width: 1.2; opacity: 0.6; }
/* la bobina del solenoide, como caja sobre el vástago: suelta y del grosor
   del tubo parecía una rotura de la tubería */
.sv-line-acc .sv-acc-coil { fill: var(--sv-dim); stroke: none; }

/* --- intercambiadores de placas: el simbolo de la caja con aspas. Son los
   dos unicos puntos donde los dos circuitos se tocan, asi que se dibujan
   igual y con el mismo peso a ambos lados del lazo de agua. --- */
.sv-hx-body { fill: url(#g-hx); stroke: var(--text); stroke-width: 2.4; }
.sv-hx-plates { fill: none; stroke: var(--sv-dim); stroke-width: 1.3; opacity: 0.7; }

/* --- accesorios de la linea de liquido (filtro secador, solenoide,
   mirilla): hardware sin lectura propia, por eso van en neutro --- */
.sv-line-acc { stroke: var(--sv-dim); stroke-width: 2; stroke-linejoin: round; }
.sv-line-acc path { fill: none; }
.sv-line-acc rect, .sv-line-acc circle { fill: var(--panel2); }
.sv-line-acc .sv-acc-solid { fill: var(--sv-dim); stroke: none; }

/* --- tuberías: 3 capas (pared / núcleo / pulso).
   El núcleo lleva SIEMPRE el color del fluido: el color de una tubería es
   parte del esquema de la planta, no una lectura del controlador, así que
   se queda aunque el equipo no responda. Lo único que se apaga sin
   comunicación es el pulso. El pulso es corto y lento a propósito: la
   versión anterior, con bloques largos en todas las líneas a la vez,
   convertía la pantalla en una cinta LED. El grosor distingue circuitos:
   el agua es la tubería gruesa, el refrigerante la fina. --- */
/* CAPA 0 -- HALO. El resplandor se finge apilando el MISMO recorrido mas
   ancho y translucido por debajo de la pared. Es la unica tecnica de
   "iluminacion" que cabe aqui: la opacidad va en el TRAZO, nunca sobre un <g>
   con varios hijos, porque un grupo con opacidad si obliga al navegador a
   componer en un bufer aparte en cada repintado.
   Da un escalon y no una caida suave -- a 4 m no se distingue, a 40 cm si. Si
   alguna vez molesta, una segunda capa (34 @ 0.07) lo disuelve, pero eso hay
   que medirlo en la Raspberry antes: cada capa suma trazados y Chrome desactiva
   la rasterizacion por GPU cuando hay demasiados trazados no convexos. */
.pb { fill: none; stroke-width: 26; opacity: 0.14; stroke-linecap: round; stroke-linejoin: round; }
.pb-w { stroke-width: 40; opacity: 0.11; }
.b-hot  { stroke: var(--pipe-hot); }
.b-liq  { stroke: var(--pipe-liq); }
.b-suc  { stroke: var(--pipe-suc); }
.b-cold { stroke: var(--pipe-cold); }
.b-warm { stroke: var(--pipe-warm); }
/* Sobre el papel blanco del tema claro un halo saturado se lee como una
   sangria de tinta, no como luz. Se baja hasta que solo insinua el recorrido. */
:root[data-theme="light"] .pb { opacity: 0.09; }
:root[data-theme="light"] .pb-w { opacity: 0.08; }

.pw { fill: none; stroke: var(--pipe-wall); stroke-width: 17; stroke-linecap: round; stroke-linejoin: round; }
.pc { fill: none; stroke: var(--pipe-core); stroke-width: 12; stroke-linecap: round; stroke-linejoin: round; }
/* FILO DE LUZ. Una linea mas fina y clara por el eje del tubo: convierte la
   banda maciza en un cilindro con un brillo especular encima. Reutiliza las
   clases l-* del pulso, que ya llevan el tono claro de cada fluido.
   Va CENTRADA y no desplazada a proposito: un desplazamiento hacia arriba solo
   da volumen a los tramos horizontales y deja los verticales iluminados de
   costado. Aqui el dibujo es una vista en planta con codos, asi que la unica
   solucion coherente es una luz que no dependa de la orientacion.
   No se pone en los tramos huecos de la cascada: ahi el centro del tubo esta
   abierto a proposito, y un filo por el eje lo volveria a cerrar. */
.pl { fill: none; stroke-width: 4; opacity: 0.32; stroke-linecap: round; stroke-linejoin: round; }
.pl-w { stroke-width: 7; opacity: 0.28; }

.pf {
  fill: none; stroke: var(--pipe-pulse); stroke-width: 5; stroke-linecap: round;
  stroke-dasharray: 14 86; stroke-dashoffset: 0;
  opacity: 0;
}
.pf.is-flowing { opacity: 0.9; animation: svFlow 3.4s linear infinite; }
/* El pulso lleva el tono CLARO de su propio fluido. --pipe-pulse (blanco) se
   queda como respaldo para cualquier tramo que no declare fluido. */
.l-hot  { stroke: var(--pipe-hot-lit); }
.l-liq  { stroke: var(--pipe-liq-lit); }
.l-suc  { stroke: var(--pipe-suc-lit); }
.l-cold { stroke: var(--pipe-cold-lit); }
.l-warm { stroke: var(--pipe-warm-lit); }
/* FIRMA DE GUION POR FLUIDO. Segundo canal, ademas del color, para quien no
   separa el rojo del verde: en deuteranopia -- ~6 % de los hombres, y los
   tecnicos de refrigeracion lo son casi todos -- la descarga y la linea de
   liquido caen a deltaE 12,9, y es justo el par rojo-verde de la convencion.
   No se recolorea (romperia la convencion y la validacion de la paleta): se le
   da a cada fluido una cadencia distinta, que ademas se replica en la leyenda.
   Coste de pintado: cero, es un parametro del trazo que ya se pinta.
   El PERIODO se conserva -- 100 en tubo fino, 136 en tubo de agua -- para que
   svFlow y svFlowW sigan valiendo para los cinco. */
.pf.l-hot  { stroke-dasharray: 10 90; }
.pf.l-liq  { stroke-dasharray: 14 86; }
.pf.l-suc  { stroke-dasharray: 22 78; }
.pf.l-cold { stroke-dasharray: 6 44 6 44; }
.pf.l-warm { stroke-dasharray: 30 70; }
/* los dos fluidos de agua, en tubo grueso: van DESPUES porque comparten
   especificidad con las reglas de arriba y tienen que ganar por orden */
.pf-w.l-cold { stroke-dasharray: 8 60 8 60; }
.pf-w.l-warm { stroke-dasharray: 40 96; }
.pw-w { stroke-width: 28; }
.pc-w { stroke-width: 22; }
.pf-w { stroke-width: 8; stroke-dasharray: 20 116; }
.pf-w.is-flowing { animation: svFlowW 4.6s linear infinite; }

/* --- cuarta capa, solo para el agua pura de la cascada: abre el nucleo por
   el centro y deja el tubo en dos filos del color del fluido.
     glicol     tubo macizo
     agua pura  tubo hueco
   Es la senal que separa los dos lazos de agua, y va en el DIBUJO y no en el
   tono a proposito. La cascada lleva agua fria y agua de retorno igual que el
   chiller, asi que comparte sus dos colores; medido con el validador de la
   skill dataviz, un sexto tono para la cascada quedaba a deltaE 9.4 del azul
   del glicol (el minimo para vision normal es 15) y cualquier septimo caia
   todavia mas bajo. El tono ya no da para mas fluidos: el grosor y este hueco
   si. Se pinta con --pipe-core, el mismo interior neutro que ya usan los
   tramos sin fluido asignado, para que el hueco se vea igual cruce por encima
   del lienzo o de un recuadro de zona. */
.ph   { fill: none; stroke: var(--pipe-core); stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; }
.ph-w { stroke-width: 8; }

@keyframes svFlow  { to { stroke-dashoffset: -100; } }
@keyframes svFlowW { to { stroke-dashoffset: -136; } }

.f-hot  { stroke: var(--pipe-hot); }
.f-liq  { stroke: var(--pipe-liq); }
.f-suc  { stroke: var(--pipe-suc); }
.f-warm { stroke: var(--pipe-warm); }
.f-cold { stroke: var(--pipe-cold); }
.a-hot { fill: var(--pipe-hot); }
.a-liq { fill: var(--pipe-liq); }
.a-suc { fill: var(--pipe-suc); }
.a-warm { fill: var(--pipe-warm); }
.a-cold { fill: var(--pipe-cold); }
.sv-arrow { opacity: 0.9; }

/* --- válvula de expansión electrónica: pajarita + vástago + motor. Sin el
   actuador la pajarita sola es una válvula manual, y un técnico no
   entendería que muestre "Apertura 47 %". --- */
.sv-valve-body { fill: var(--panel3); stroke: var(--pipe-suc); stroke-width: 2.4; stroke-linejoin: round; }
.sv-valve-stem { fill: none; stroke: var(--pipe-suc); stroke-width: 2.2; }
.sv-valve-motor { fill: var(--panel2); stroke: var(--pipe-suc); stroke-width: 2.2; }
.sv-valve-m { font-family: var(--font-mono); font-size: 17px; font-weight: 600; fill: var(--pipe-suc); }

/* la valvula del cuarto frio NO la gobierna el VX: lleva bulbo en vez de
   motor y va en neutro, porque de ella no llega ninguna lectura */
.sv-valve-tce .sv-valve-body { stroke: var(--sv-dim); fill: var(--panel2); }
.sv-valve-tce .sv-valve-stem { stroke: var(--sv-dim); }
.sv-valve-bulb { fill: var(--panel2); stroke: var(--sv-dim); stroke-width: 1.8; }
.sv-valve-tce .sv-valve-m { font-size: 9px; fill: var(--sv-dim); }

/* --- tipografía dentro del SVG (unidades del viewBox) --- */
/* ESCALA DEL PLANO -- CINCO PASOS, en unidades del viewBox.
   Antes habia catorce tamanos entre 9 y 104 px, que no es jerarquia sino
   ruido. Con 1 unidad = 0,881 px en el kiosco de 1920x1080, y el criterio de
   altura de mayuscula >= distancia/200 en una pantalla de 55", cada paso tiene
   un alcance:
       104  heroe                      ~8 m
        52  lectura principal          ~4 m
        30  lectura secundaria         ~2,3 m
        24  rotulo de equipo y zona    ~1,9 m
        17  nota y unidad              ~1,3 m
   No se lee todo a cuatro metros -- es imposible con esta densidad sin borrar
   informacion -- pero hay un CONTRATO DE DISTANCIA: de lejos el estado de la
   planta, de cerca los rotulos. Antes no lo habia y entre el nivel 2 y el 3
   habia un precipicio de 4,1 m a 1,6 m.
   Si tocas un tamano, vuelve a pasar el comprobador de choques de la maqueta:
   es lo primero que se rompe al crecer el texto. */
.sv-zone    { font-family: var(--font-body); font-size: 24px; font-weight: 600; fill: var(--sv-dim); letter-spacing: 0.02em; }
.sv-chip    { font-family: var(--font-mono); font-size: 17px; font-weight: 600; fill: var(--warn); letter-spacing: 0.08em; }
.sv-tag     { font-family: var(--font-body); font-size: 17px; font-weight: 600; fill: var(--sv-dim); }
/* .sv-name es la ETIQUETA de una lectura ("cuarto", "setpoint", "T de
   liquido"), no el nombre de un equipo: va en el nivel de nota, no en el de
   rotulo. Ese es el contrato de distancia -- el numero se lee de lejos, su
   nombre de cerca -- y ademas a 24 px "T de liquido" se comia su propio valor.
   El nombre de equipo, que si es rotulo, vive en .sv-eq-name a 24. */
.sv-name    { font-family: var(--font-body); font-size: 17px; font-weight: 600; fill: var(--sv-dim); }
.sv-val     { font-family: var(--font-mono); font-size: 30px; font-weight: 600; fill: var(--text); font-variant-numeric: tabular-nums; }
.sv-unit    { font-family: var(--font-mono); font-size: 17px; fill: var(--sv-dim); }
.sv-eq-name { font-family: var(--font-display); font-size: 24px; font-weight: 700; fill: var(--text); }
/* El % de un compresor es la apertura de un ACTUADOR, igual que el % de una
   valvula: va al 24 de .sv-vbar-num. Temperaturas y presiones a 30, porcentajes
   de actuador a 24. A 52 competia con el retorno de glicol y se comia los
   simbolos de su circuito. */
.sv-eq-big  { font-family: var(--font-mono); font-size: 24px; font-weight: 600; fill: var(--text); font-variant-numeric: tabular-nums; }
.sv-eq-unit { font-size: 17px; fill: var(--sv-dim); }
.sv-lead    { font-family: var(--font-body); font-size: 24px; font-weight: 600; fill: var(--sv-dim); }
.sv-lead { fill: var(--pipe-warm); }

/* El héroe va al doble de altura que la siguiente lectura Y con el mayor
   contraste de la pantalla: el color se lo lleva su rótulo, que es lo que
   lo ata a la tubería azul. Antes el número importante iba en azul (menos
   contraste) y el secundario en blanco, contradiciendo al tamaño.
   El tracking cerrado junta el punto decimal: suelto, "7 . 3" se leía
   como dos cifras sueltas. */
.sv-hero {
  font-family: var(--font-mono); font-size: 104px; font-weight: 600;
  fill: var(--text); letter-spacing: -0.06em; font-variant-numeric: tabular-nums;
  /* EL NUMERO INTERRUMPE LA TUBERIA. El trazo se pinta por debajo del relleno
     (paint-order) en el color del LIENZO, asi que el numero abre un hueco en lo
     que tenga detras en vez de montarse encima. Con un valor real el heroe mide
     310 unidades y cruza dos bajantes: sin esto se leia como un choque.
     Cuesta lo mismo que pintar el texto -- un drop-shadow costaria un bufer
     fuera de pantalla en cada repintado, y esto se repinta una vez por segundo
     -- y funciona igual en los dos temas, al contrario que el halo de color que
     habia antes, que estaba apagado en claro. */
  paint-order: stroke fill;
  stroke: var(--canvas); stroke-width: 16; stroke-opacity: 1;
  stroke-linejoin: round;
}
/* el recorte vale para los dos temas: ya no hace falta un caso aparte */
.sv-hero-unit { font-size: 24px; fill: var(--sv-dim); letter-spacing: 0; }
/* El retorno es una temperatura de agua igual que la salida: va en --text,
   no atenuado. Antes acumulaba desventaja por tamano (2:1) y por contraste
   a la vez, y la pareja no se leia como pareja. */
.sv-big     { font-family: var(--font-mono); font-size: 52px; font-weight: 600; fill: var(--text); letter-spacing: -0.04em; font-variant-numeric: tabular-nums; }
.sv-big-unit{ font-size: 24px; fill: var(--sv-dim); letter-spacing: 0; }
/* Salida y retorno de glicol son la pareja del lazo y van AL MISMO TAMANO.
   Con el heroe al doble (104 contra 52) la pareja se leia despareja, y el
   dueno lo pidio igualado. Solo se tocan estas dos cifras: .sv-hero sigue
   siendo el numero grande de las vistas de detalle. */
#sv-hero-t, #sv-tent-t { font-size: 56px; letter-spacing: -0.05em; }
#sv-hero-t.is-empty, #sv-tent-t.is-empty { font-size: 36px; letter-spacing: 0.04em; }

/* EL VACIO ES EL ESTADO PRINCIPAL y se disena como tal.
   A 34 px la palabra dejaba un agujero bajo su propio rotulo y la composicion
   se venia abajo precisamente en el estado en el que la pantalla pasa la mayor
   parte del tiempo. Va UN ESCALON por debajo de su numero (52 el heroe, 30 el
   retorno) -- dentro de la escala, no en tamanos propios -- y cambia
   de fuente: va en la CONDENSADA DE ROTULO y en gris, no en la monoespaciada
   de las cifras. Asi el hueco conserva el peso de la composicion sin imitar a
   un numero, y nadie confunde una ausencia con una lectura. */
.sv-hero.is-empty {
  font-family: var(--font-display); font-size: 52px; font-weight: 700;
  fill: var(--sv-faint); letter-spacing: 0.04em; stroke-opacity: 0;
}
.sv-big.is-empty {
  font-family: var(--font-display); font-size: 30px; font-weight: 700;
  fill: var(--sv-faint); letter-spacing: 0.04em;
}
.is-empty .sv-hero-unit, .is-empty .sv-big-unit { display: none; }

/* --- estado de equipo (classList.toggle sobre el <g>; nunca className,
   que en SVG es un SVGAnimatedString de solo lectura) --- */
.sv-eq.is-on   .sv-machine { stroke-width: 4; fill: var(--eq-lift); }
.sv-eq.is-on   .sv-machine-mark { stroke-width: 3.2; }
/* mantenimiento SI lleva color: es una anomalia, y para eso esta el ambar */
.sv-eq.is-warn .sv-machine { stroke: var(--warn); stroke-width: 4; }
.sv-eq.is-warn .sv-machine-mark { stroke: var(--warn); }
.sv-eq.is-off .sv-machine, .sv-eq.is-off .sv-machine-mark { stroke: var(--sv-dim); }

/* --- sin comunicación: el plano SIGUE SIENDO EL PLANO. Las tuberías
   conservan su color y las flechas su sentido, porque eso describe la
   instalación y no depende de que el RCK conteste; lo que se apaga es el
   estado de las máquinas, el pulso del flujo y todos los números. --- */
.sv-off .sv-machine, .sv-off .sv-machine-mark, .sv-off .sv-hx-body { stroke: var(--sv-dim); }
.sv-off .sv-eq-big, .sv-off .sv-eq-name { fill: var(--sv-dim); }
.sv-off .sv-hero { fill: var(--sv-dim); }
.sv-off .sv-valve-body, .sv-off .sv-valve-stem, .sv-off .sv-valve-motor { stroke: var(--sv-dim); }
.sv-off #sv-vx .sv-valve-m { fill: var(--sv-dim); }

/* el VX es un equipo aparte: si se cae él, solo se apaga él */
.sv-vx-off #sv-vx { opacity: 0.45; }

/* el circuito 2 y la bomba pueden no estar cableados todavía. El dibujo
   los sigue contando -- la planta es la que es -- pero se marca que de ahí
   no llega ningún dato, en vez de fingir ceros o de esconderlos. */
#sv-c2-chip { display: none; }
.sv-c2-off:not(.sv-off) #sv-c2-chip { display: block; }

/* --- agua pura de la cascada: no es un fluido nuevo en la leyenda de color,
   es el mismo agua fria y el mismo agua de retorno dibujados con tubo hueco.
   Lo que dice "este es OTRO lazo" es el hueco, no el tono. --- */

/* --- toma de medida ---
   Un punto del color del fluido con un anillo del color de la superficie. El
   anillo es lo que lo mantiene legible justo donde cae encima de la tuberia,
   que es donde tiene que estar: la etiqueta ya no flota suelta, senala el
   punto exacto de la instalacion donde se lee ese valor.
   Va dibujado en linea y NO con <use>: el contenido que clona <use> vive en un
   shadow tree y estos selectores no lo alcanzarian. */
.sv-tap-ring { fill: none; stroke: var(--panel); stroke-width: 4; }
.sv-tap-dot  { stroke: none; }
.t-cold .sv-tap-dot { fill: var(--pipe-cold); }
.t-warm .sv-tap-dot { fill: var(--pipe-warm); }
.t-hot  .sv-tap-dot { fill: var(--pipe-hot); }
.t-liq  .sv-tap-dot { fill: var(--pipe-liq); }
.t-suc  .sv-tap-dot { fill: var(--pipe-suc); }
.t-room .sv-tap-dot { fill: var(--sv-dim); }

/* linea guia: solida, nunca punteada -- una guia discontinua se lee como
   senal de control. Arranca en la circunferencia del punto, no en su centro,
   y nunca cruza una tuberia: si la cruzaria, se mueve la etiqueta. */
.sv-lead-line { fill: none; stroke: var(--sv-dim); stroke-width: 2; opacity: 0.75; }

/* El rotulo de una lectura NO lleva el color del fluido: esa identidad la
   carga el punto de medida que tiene al lado. Con el color en el texto, el
   rotulo del glicol de salida salia en ambar y contradecia a su tuberia. */
.sv-lead { fill: var(--sv-dim); }

/* --- empalme real de tuberia ---
   Dos circulos concentricos que reproducen la seccion del tubo, asi el
   empalme parece moldeado y de paso tapa las medialunas que deja
   stroke-linecap:round donde arranca una derivacion.
   CONTRATO DEL PLANO: circulo = conectado · arco = solo se cruzan. */
.sv-joint-wall { fill: var(--pipe-wall); stroke: none; }
.sv-joint-core { stroke: none; }
.j-cold .sv-joint-core { fill: var(--pipe-cold); }
.j-warm .sv-joint-core { fill: var(--pipe-warm); }

/* --- barra de apertura de valvula ---
   La pista es el mismo tono del relleno a menor opacidad, no un gris neutro:
   asi el estado se lee a lo largo de toda la barra y ademas ata cada barra al
   color de la tuberia que gobierna. El relleno no lleva rx: con esquinas
   redondas completas un 3 % se dibuja como una pastilla deforme. */
.sv-vbar-track { fill: currentColor; opacity: 0.22; }
.sv-vbar-fill  { fill: currentColor; }
.sv-vbar-tick  { fill: none; stroke: var(--canvas); stroke-width: 2; opacity: 0.5; }
.sv-vbar-num   { font-family: var(--font-mono); font-size: 24px; font-weight: 600; fill: var(--text); font-variant-numeric: tabular-nums; }
.v-cold { color: var(--pipe-cold); }
.v-liq  { color: var(--pipe-liq); }

/* --- valvulas con lectura ---
   Las dos reportan % de apertura y las dos llevan actuador "M". Lo que las
   distingue es el fluido que gobiernan: la EEV expande refrigerante, la 2V
   dosifica glicol. Por eso el actuador va en el color de su linea. */
.sv-valve-2v  .sv-valve-body { stroke: var(--pipe-cold); fill: var(--panel3); }
.sv-valve-2v  .sv-valve-stem { stroke: var(--pipe-cold); }
.sv-valve-2v  .sv-valve-motor{ stroke: var(--pipe-cold); fill: var(--panel2); }
.sv-valve-2v  .sv-valve-m    { fill: var(--pipe-cold); }
/* actuador redondo: es como se dibuja un actuador modulante de agua, y separa
   las 2V de las EEV por FORMA y no solo por color, porque el color ya lo tiene
   asignado el fluido. Faltaba aqui y solo existia en la maqueta, asi que en
   produccion estos circulos salian sin estilo. */
.sv-valve-2v  .sv-valve-act  { stroke: var(--pipe-cold); stroke-width: 2.4; fill: var(--panel2); }
.sv-valve-eev .sv-valve-body { stroke: var(--pipe-liq); fill: var(--panel3); }
.sv-valve-eev .sv-valve-stem { stroke: var(--pipe-liq); }
.sv-valve-eev .sv-valve-motor{ stroke: var(--pipe-liq); fill: var(--panel2); }
.sv-valve-eev .sv-valve-m    { fill: var(--pipe-liq); }

/* --- punto de medida previsto pero todavia sin fuente de datos ---
   El punto HUECO es lo que distingue "aqui se mide pero aun no llega" de
   "aqui no se mide". Y una barra vacia se dibuja en contorno discontinuo, no
   en 0 %: un 0 % se leeria como "valvula cerrada", que es peor que sin dato.
   La clase es ESTATICA en el marcado -- describe la instalacion, no una
   lectura -- asi que app.js no la conmuta. Conectar una fuente es borrar la
   clase de ese grupo y anadir una linea setNum(). */
/* Un hueco ocupa lo MISMO que su dato: lo distingue el tono, no el cuerpo.
   Con cuerpo propio (26 y 20) la temperatura del cuarto B salia mas pequena
   que su propio setpoint, justo debajo. */
.sv-sin-fuente .sv-val,
.sv-sin-fuente .sv-vbar-num   { fill: var(--sv-faint); }
/* el estado "sin fuente" no puede ser MAS grande que el normal: al subir la
   escala estos tres se quedaron en 20 px cuando su version normal bajo a 17 */
.sv-sin-fuente .sv-name,
.sv-sin-fuente .sv-tag        { font-size: 17px; fill: var(--sv-faint); }
.sv-sin-fuente .sv-unit       { fill: var(--sv-faint); }
.sv-sin-fuente .sv-vbar-fill  { display: none; }
.sv-sin-fuente .sv-vbar-tick  { display: none; }
.sv-sin-fuente .sv-vbar-track { fill: none; stroke: var(--sv-faint); stroke-width: 1.5; stroke-dasharray: 4 4; opacity: 1; }
.sv-sin-fuente .sv-tap-dot    { fill: none; stroke: var(--sv-faint); stroke-width: 2; }
.sv-sin-fuente .sv-lead-line  { stroke: var(--sv-faint); opacity: 0.5; }

/* --- serpentin de glicol dentro del evaporador del cuarto A: es lo que lo
   distingue del de expansion directa sin tener que leer el rotulo --- */
.sv-coil { fill: none; stroke: var(--pipe-cold); stroke-width: 3.2; stroke-linecap: round; opacity: 0.85; }

/* --- cascada: la lamina de agua ---
   Es el unico elemento que se mueve por si solo en toda la pantalla; el resto
   se comporta como instrumento. Solo stroke-dashoffset, como el pulso de las
   tuberias: nada de filtros ni sombras, que esto corre en un Raspberry. */
.sv-fall {
  fill: none; stroke: var(--pipe-cold); stroke-width: 4; stroke-linecap: round;
  /* El agua que cae es lo UNICO que se pinta a plena saturacion en todo el
     plano. Es el elemento que da nombre a la rama, el unico que literalmente
     se mueve en la planta, y lo que explica sin leer nada por que hay una ida
     fria y un retorno templado. Al 0.55 competia con veinte tubos y se perdia
     arriba al centro. */
  opacity: 0.88; stroke-dasharray: 30 46;
}
/* dos grosores mezclados: una cortina de agua no cae en hilos iguales */
.sv-fall-fino { stroke-width: 2.6; opacity: 0.66; }
/* la lamina cae solo con evidencia, igual que el pulso de las tuberias: sin
   lectura de la valvula de su ramal se queda quieta antes que mentir */
.sv-fall.is-flowing { animation: svFall 1.05s linear infinite; }
/* El agua llega fria a la canaleta de arriba y cae; lo que se junta abajo en
   el estanque ya viene templado y es lo que vuelve al intercambiador. Por eso
   la lamina va en el azul del agua fria y el estanque en el ambar del retorno:
   la cascada cuenta ella sola por que hay ida y retorno. */
.sv-fall-sheet  { fill: var(--pipe-cold); opacity: 0.18; }
.sv-fall-trough { fill: var(--panel3); stroke: var(--sv-dim); stroke-width: 1.8; }
/* El cuenco iba relleno de --panel3, que es azul grisaceo, y encima el agua
   ambar a 0.3: la mezcla daba un caqui sucio que no se parecia ni al agua ni
   al ambar del retorno. El cuenco pasa al color del lienzo para que el ambar
   no se mezcle con azul. */
.sv-fall-basin  { fill: var(--canvas); stroke: var(--sv-dim); stroke-width: 1.8; }
/* la lamina del canal de arriba: el agua fria que espera antes de caer */
.sv-fall-canal  { fill: var(--pipe-cold); opacity: 0.5; }
/* el estanque va mas suave que la lamina: es una superficie quieta, y al 0.35
   el ambar pesaba tanto que se leia como un bloque macizo en vez de agua. Por
   debajo de 0.3 se apaga demasiado en el tema oscuro y deja de leerse ambar. */
.sv-fall-water  { fill: var(--pipe-warm); opacity: 0.62; }
/* filo de luz en la superficie del agua: la diferencia entre una barra de
   color y una superficie de agua vista desde arriba */
.sv-fall-brillo { fill: none; stroke: var(--pipe-warm-lit); stroke-width: 2;
                  stroke-linecap: round; opacity: 0.55; }
@keyframes svFall { to { stroke-dashoffset: -76; } }

/* El tamano del heroe y del retorno esta en su declaracion de mas arriba.
   Antes habia aqui un refuerzo a 132/84 px porque el lienzo vertical se
   dibujaba muy pequeno; con el lienzo apaisado la escala sube a 0,85 px por
   unidad y ese refuerzo sobra. */

/* NADA SE MUEVE SI NADIE MIRA. Hay cinco bucles infinite corriendo; cuando el
   kiosco apaga la salida HDMI o la ventana pierde visibilidad, los @keyframes
   de CSS SIGUEN componiendo. En una feria de tres dias eso es procesador y
   temperatura por nada. app.js pone la clase con visibilitychange. */
:root.esta-oculto .pf,
:root.esta-oculto .pf-w,
:root.esta-oculto .sv-fall,
:root.esta-oculto .sv-fan-aspas,
:root.esta-oculto .led-blink,
:root.esta-oculto .alarm-bar { animation-play-state: paused; }

/* CON ALARMA, LO DEMAS SE QUEDA QUIETO. Antes competian cuatro periodos
   distintos a la vez y el que mas importa -- la franja -- era el que menos
   destacaba, porque todo lo demas ya se movia. Los guiones siguen DIBUJADOS,
   asi que no se pierde la informacion de caudal: solo dejan de correr. De paso
   la Raspberry compone menos justo cuando mas falta hace que responda. */
body.hay-alarma .pf.is-flowing,
body.hay-alarma .pf-w.is-flowing,
body.hay-alarma .sv-fall.is-flowing,
body.hay-alarma .sv-fans.is-spinning .sv-fan-aspas { animation-play-state: paused; }

/* Cada bucle con NOMBRE PROPIO: antes .pf-w dependia de que su regla fuera
   de antes en el archivo, y reordenar el CSS lo habria dejado corriendo. */
@media (prefers-reduced-motion: reduce) {
  .pf.is-flowing,
  .pf-w.is-flowing,
  .sv-fall.is-flowing,
  .sv-fans.is-spinning .sv-fan-aspas { animation: none; }
}

/* ===================== RIEL DERECHO Y CINTA DE TENDENCIA ===================== */
/* la cinta de tendencia vive en el riel y se estira con el: aporta lo
   unico que el plano no puede dar, que es el tiempo */
.graph-ribbon {
  flex: 1 1 auto; min-height: 120px;
  display: flex; flex-direction: column;
  padding-bottom: var(--sp-2);
}
.graph-ribbon .graph-canvas-wrap { flex: 1 1 auto; height: auto; min-height: 90px; }

/* tarjetas hermanas, maquetadas igual y sin filete de color propio: tres
   colores distintos sin razón semántica solo pedían que se los buscara */
.side-panel {
  display: flex; flex-direction: column; gap: var(--sp-2);
  flex: 0 0 auto; min-height: 0;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4);
  text-align: left; cursor: pointer; color: inherit; font-family: inherit;
  transition: background 0.15s, border-color 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
.side-panel:hover { background: var(--panel2); border-color: var(--border-strong); }
.side-panel.is-off { opacity: 0.55; }
/* --- CABECERA DE TARJETA ---
   Chip de icono, antetitulo, nombre y LED de estado. Es el mobiliario que
   sostiene una tarjeta cuando sus tres lecturas dicen SIN DATO: sin el, la
   tarjeta es un rectangulo con texto y por eso la pantalla se veia sin
   terminar. Va calcado de la guia de referencia. --- */
.side-head {
  display: grid;
  /* chip | identidad | LED. Sin chevron: la tarjeta entera ya es el boton y
     ese icono se comia los 23 px que le faltaban al antetitulo para caber en
     una columna de 220. La guia de referencia tampoco lo lleva. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--sp-2);
}
.side-chip {
  width: 26px; height: 26px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel2); border: 1px solid var(--border-strong);
  color: var(--brand); flex: 0 0 auto;
}
.side-chip .icon { width: 15px; height: 15px; }
.side-ident { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.side-eyebrow {
  font-family: var(--font-body); font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim);
  /* envuelve en vez de truncar: a 220 px "Condensacion por agua" no cabe en
     una linea a ningun cuerpo legible, y cortarlo en "por ag..." es peor que
     partirlo. El alto sobra desde que la tarjeta ocupa toda la columna. */
  white-space: normal; line-height: 1.2;
}
.side-name {
  font-family: var(--font-display); font-weight: 700; font-size: 1.2rem;
  color: var(--text); line-height: 1.05;
}
/* el LED de la cabecera es mas pequeno que el de la vista de detalle: aqui
   acompana a un nombre, no encabeza una lista de estados */
.side-head .led { width: 9px; height: 9px; }
/* POR QUE esta vacia. Una sola declaracion por tarjeta: repetir SIN DATO en
   cada una de las catorce filas vaciaba la palabra de significado y hacia que
   la pantalla pareciera rota en vez de desconectada. Solo aparece cuando la
   fuente no responde; en cuanto llegan lecturas se va sola. */
.side-estado {
  font-family: var(--font-body); font-size: 0.68rem; font-weight: 600;
  color: var(--sv-faint); letter-spacing: 0.02em;
  display: flex; align-items: center; gap: 6px;
}
.side-estado.is-hidden { display: none; }
.side-estado .icon { width: 12px; height: 12px; flex: 0 0 auto; }

/* Escuadra y filete: los dos remates que separan una tarjeta de un
   rectangulo. Van en pseudoelementos, sin marcado extra, y no se animan. */
.side-panel, .lect-panel { position: relative; overflow: hidden; }
.side-panel::before, .lect-panel::before {
  content: ''; position: absolute; top: -1px; left: -1px;
  width: 15px; height: 15px;
  border-top: 2px solid var(--brand); border-left: 2px solid var(--brand);
  border-top-left-radius: var(--r-md);
  opacity: 0.6; pointer-events: none;
}
.side-panel::after, .lect-panel::after {
  content: ''; position: absolute; top: 0; left: 15px; right: 0; height: 1px;
  background: linear-gradient(90deg, var(--brand), transparent 70%);
  opacity: 0.3; pointer-events: none;
}
.side-panel.is-off::before, .side-panel.is-off::after { opacity: 0.18; }
/* la tarjeta entera es el botón: no necesita un "Ver detalle →" de landing */
.side-go { width: 15px; height: 15px; color: var(--text-dim); }
.side-panel:hover .side-go { color: var(--brand); }
/* Las filas se REPARTEN el alto de la tarjeta en vez de agruparse en el
   centro. En el riel de antes la tarjeta medía lo que medía su contenido;
   en columna lateral mide media columna, y con las filas centradas quedaba
   un hueco arriba y otro abajo que parecía un error de maquetación. */
.side-rows {
  display: flex; flex-direction: column;
  /* sin gap: lo que separa una lectura de la siguiente es el filete de la
     propia fila, y el alto se lo reparten las filas (ver .side-row) */
  gap: 0; flex: 1 1 auto; min-height: 0;
}
/* Etiqueta ENCIMA y número debajo, no uno a cada lado. En una columna de
   220 px el par nombre/valor en horizontal deja el número apretado contra
   el borde; apilado, el número puede crecer y leerse desde lejos, que es
   lo que hace la guía de referencia. */
.side-row {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  border-top: 1px solid var(--hairline);
  /* CADA FILA ES UNA BANDA y todas miden igual. Es lo que convierte el alto
     sobrante en estructura en vez de en hueco: con las filas midiendo su
     contenido, estirar la tarjeta solo las alejaba entre si. */
  flex: 1 1 0; min-height: 0; justify-content: center;
  padding: var(--sp-2) 0;
}
.side-row:first-child { border-top: none; }
/* fila de solo valores (cuando el nombre va en su propia linea encima) */
/* fila de titulo dentro de la tarjeta (el nombre de la valvula) */
.side-row-head { border-top: none; padding-bottom: 2px; }
.side-row-head .side-row-name { color: var(--brand); font-weight: 700; }
.side-row-name { font-family: var(--font-body); font-size: 0.78rem; font-weight: 600; color: var(--text-dim); }
.side-row-vals { display: flex; gap: var(--sp-3); }
.side-val {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  font-family: var(--font-mono); font-size: 0.9rem; color: var(--text-dim); white-space: nowrap;
}
.side-val b { font-size: 2.4rem; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1; }
/* la raya del valor ausente no merece el cuerpo del dato: el hueco se marca,
   no se grita. Quien explica el hueco es .side-estado, arriba. */
.side-row.is-vacio .side-val b { font-size: 1.6rem; color: var(--sv-faint); }
.side-row-name {
  color: var(--text-dim); font-size: 0.66rem; letter-spacing: 0.07em; text-transform: uppercase;
}
.side-row-head .side-row-name { font-size: 0.72rem; letter-spacing: 0.04em; }

/* ===================== RESPONSIVE ===================== */
/* tablet APAISADA: el sinoptico pasa a ancho completo y los tres resumenes
   debajo.
   Se conserva el guard "orientation: landscape" aunque el kiosco ya no sea
   vertical: una tableta de pie sigue cumpliendo max-width:1180px, y sin el
   guard este bloque le impone a esa pantalla una caja ancha y baja que, con el
   overflow:hidden de mas arriba, recorta el riel sin avisar. */
@media (max-width: 1180px) and (orientation: landscape) {
  #view-dashboard { grid-template-rows: auto auto auto; }
  /* la grafica pasa a ancho completo debajo de las dos tarjetas */
  .dash-medio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .graph-ribbon { min-height: 200px; }
  /* la proporcion del viewBox, para que el lienzo y el dibujo coincidan y no
     sobre lienzo a los lados ni se recorte por arriba */
  .syn-canvas { aspect-ratio: 2200 / 1000; }
  .syn-head { gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); }
  .syn-title { font-size: 1.15rem; }
  /* en fila los paneles se estiran a la altura de la cinta de tendencia:
     su contenido se pega al titulo en vez de flotar en el centro */
  .side-rows { justify-content: flex-start; flex: 0 1 auto; }
  .syn-legend { padding: 6px var(--sp-3) 7px; gap: 6px var(--sp-3); font-size: 0.68rem; }
}

/* ===== DISPOSICION VERTICAL =====
   La de la feria: una TV Full HD montada de pie, 1080x1920. En un kiosco nadie
   hace scroll, asi que TODO el resumen cabe en una pantalla:
     1. el plano, a todo el ancho y entero (su alto sale de su viewBox);
     2. las tarjetas, en una sola fila debajo;
     3. la tendencia, que se queda con todo el alto que sobre.
   No se redibuja el plano en vertical: con el lienzo apaisado a 1048 px de
   ancho mide ~650 de alto, y deja mas de la mitad de la pantalla para lo demas.
   Las columnas laterales se disuelven (display: contents) y sus tarjetas pasan
   a ser celdas de la rejilla: salgan 3 o 4 (con el VX), se reparten el ancho. */
@media (min-width: 700px) {
  :root[data-orient="vertical"] #view-dashboard {
    display: flex; flex-direction: column; gap: var(--sp-3);
  }
  :root[data-orient="vertical"] .dash-pantalla {
    flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--sp-3);
  }
  /* Tantas columnas como tarjetas: 3, o 4 cuando vuelve el VX. Con auto-fit
     no sirve -- el plano abarca 1 / -1 y eso mantiene viva la pista vacia. */
  :root[data-orient="vertical"] .dash-medio {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-3); align-items: stretch;
  }
  :root[data-orient="vertical"] .dash-medio:has(#panel-vx) {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
  :root[data-orient="vertical"] .dash-col { display: contents; }
  :root[data-orient="vertical"] .syn-panel { grid-column: 1 / -1; order: -1; }
  /* el svg toma el ancho y calcula su alto desde el viewBox */
  :root[data-orient="vertical"] .syn-canvas { display: block; padding: var(--sp-1); }
  :root[data-orient="vertical"] .syn-canvas .sv { width: 100%; height: auto; }
  :root[data-orient="vertical"] .syn-head { padding: 6px var(--sp-4); }
  :root[data-orient="vertical"] .syn-legend { padding: 5px var(--sp-4) 6px; }
  /* la tarjeta mide lo que su contenido: las filas dejan de repartirse un
     alto que aqui no existe (con flex-basis 0 se aplastaban una encima de otra) */
  :root[data-orient="vertical"] .side-rows { justify-content: flex-start; flex: 0 0 auto; }
  :root[data-orient="vertical"] .side-row { flex: 0 0 auto; }
  :root[data-orient="vertical"] .graph-ribbon {
    flex: 1 1 auto; min-height: 220px; margin-top: 0;
  }
  /* se mira desde mas lejos: titulos de grafica y numeros de tarjeta suben.
     El alto que ganan las tarjetas sale de la tendencia, que iba sobrada. */
  :root[data-orient="vertical"] .graph-title { font-size: 0.95rem; }
  /* EL PLANO EN VERTICAL se escala a ~0,65: un rotulo de 17 unidades cae a
     11 px. Rotulos y nombres de lectura suben a 18 (el maximo que el plano
     admite sin choques: a 20 salian cinco), y
     las etiquetas de accesorio (filtro, mirilla, solenoide) se retiran: su
     simbolo ya lo dice, y dejan el aire que necesita el texto mas grande.
     Comprobado con el comprobador de la maqueta en data-orient="vertical". */
  :root[data-orient="vertical"] .sv-tag,
  :root[data-orient="vertical"] .sv-sin-fuente .sv-tag { font-size: 18px; }
  :root[data-orient="vertical"] .sv-name,
  :root[data-orient="vertical"] .sv-sin-fuente .sv-name { font-size: 18px; }
  :root[data-orient="vertical"] .sv-tag-acc { display: none; }
  :root[data-orient="vertical"] .syn-legend { font-size: 0.9rem; }
  :root[data-orient="vertical"] .side-val b { font-size: 3rem; }
  :root[data-orient="vertical"] .side-row-name { font-size: 0.78rem; }
  :root[data-orient="vertical"] #dash-movil { display: none; }

  /* VISTAS DE DETALLE EN VERTICAL. Con las gráficas a 210 px fijos quedaba
     media pantalla vacía debajo; aquí la vista es una columna y la rejilla de
     gráficas se queda con el alto que sobre, repartido a partes iguales.
     :not(.hidden) es imprescindible: esta regla gana en especificidad a
     .hidden y, sin el, mostraría las cuatro vistas a la vez. */
  :root[data-orient="vertical"] .view:not(.hidden):not(#view-dashboard) {
    display: flex; flex-direction: column;
  }
  :root[data-orient="vertical"] .view:not(#view-dashboard) > * { flex-shrink: 0; }
  :root[data-orient="vertical"] .view:not(#view-dashboard) > .graph-grid-2 {
    flex: 1 1 auto; min-height: 0; grid-auto-rows: minmax(200px, 1fr);
    margin-bottom: 0 !important;
  }
  /* Cuatro graficas, SIEMPRE en 2x2. Por debajo de 980 px la regla general
     las apilaba en una columna, y en vertical eso son cuatro franjas que no
     caben: la vista del RCK se salia 400 px por abajo. */
  :root[data-orient="vertical"] .graph-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* con solo dos graficas, una encima de otra: lado a lado en 1080 de ancho
     quedaban columnas de 500x1300 que no se leen como serie temporal */
  :root[data-orient="vertical"] .graph-grid-2:not(:has(> :nth-child(3))) { grid-template-columns: minmax(0, 1fr); }
  :root[data-orient="vertical"] .graph-grid-2 .graph-box { display: flex; flex-direction: column; }
  :root[data-orient="vertical"] .graph-grid-2 .graph-canvas-wrap { flex: 1 1 auto; height: auto; min-height: 160px; }
  :root[data-orient="vertical"] .lect-grid { grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); }

  /* UNA PANTALLA EXACTA TAMBIEN POR DEBAJO DE 1000 px. El reparto a alto
     completo (cabecera, vista que flexiona, pie) solo se activaba desde 1000 px
     de ancho, y la TV del stand trabaja a ~810-900 px CSS porque el navegador
     de la Pi la escala al 125-133 %: por eso las graficas se quedaban arriba
     con un tercio de pantalla negro debajo. En vertical el alto se reparte
     siempre. El resumen scrollea en vez de recortar: en una tableta de pie
     mas bajita el plano, las tarjetas y la tendencia no caben y recortar
     escondería la tendencia entera. */
  /* 100vh primero: si el Chromium de la Pi es anterior a dvh (v108), la
     segunda declaracion se descarta y la pantalla sigue repartida. */
  :root[data-orient="vertical"],
  :root[data-orient="vertical"] body { height: 100vh; height: 100dvh; overflow: hidden; }
  :root[data-orient="vertical"] body { display: flex; flex-direction: column; }
  :root[data-orient="vertical"] .view {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: var(--sp-3);
  }
  :root[data-orient="vertical"] #view-dashboard { overflow-y: auto; }
  :root[data-orient="vertical"] #topbar { position: static; }

  /* PESTANAS A TODO EL ANCHO. Alineadas a la izquierda con 14-20 px de
     relleno fijo sumaban ~860 px y EFICIENCIA quedaba cortada contra el borde.
     Repartidas, el aire sobrante va entre ellas y caben desde ~780 px. */
  :root[data-orient="vertical"] #nav-tabs { padding: 0 var(--sp-2); }
  :root[data-orient="vertical"] .nav-tab {
    flex: 1 1 auto; text-align: center; padding: 12px 6px;
  }

  /* EL NOMBRE DEL SITIO NO SE CORTA. Entre ~860 y ~1017 px de ancho la hora
     y el titulo se repartian la falta de espacio y salia "CHILLER FER...":
     aqui el titulo mide lo que pida (con tope) y la que cede es la hora. */
  :root[data-orient="vertical"] #titulo-wrap { flex: 0 1 auto; min-width: 212px; max-width: 240px; }
  :root[data-orient="vertical"] #ts-text {
    flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  }

  /* LOGO EN PLACA. En el stand el logo se perdia: chico y, sobre la cabecera
     oscura, el azul Frozz queda en 2,7:1 y el gris de "COLOMBIA" desaparece.
     En vertical crece a 72 px de dibujo sobre una placa blanca -- el fondo
     para el que se diseno --, con el filete y la sombra corta del resto de
     tarjetas. Es un SVG, asi que se ve nitido a este tamano. La cabecera
     pasa de 77 a ~108 px de alto; lo pagan las graficas, que van sobradas. */
  :root[data-orient="vertical"] .logo {
    height: 88px; padding: 8px 14px;
    background: #FFFFFF; border-radius: var(--r-sm);
    box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.35);
  }
  /* sobre la cabecera clara la placa se fundiria: la delimita el filete */
  :root[data-orient="vertical"][data-theme="light"] .logo {
    border: 1px solid var(--border);
    box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.12);
  }
}


/* ===== KIOSCO APAISADO =====
   Este es el caso del stand: 1920x1080 en horizontal.
   EL PLANO SE LLEVA TODO EL ANCHO. Antes el riel ocupaba una columna de ~500 px
   a la derecha para que el lienzo cupiera entero en el alto de la pantalla; el
   dueno pidio lo contrario: que el dibujo use todo el espacio hacia los lados
   aunque haya que bajar un poco para verlo completo. El riel vuelve a la banda
   del pie, a lo ancho.
   Por eso la fila del plano pasa a `auto` y no a `minmax(0,1fr)`: con 1fr el
   lienzo se comprime al alto que sobre y el aspect-ratio no sirve de nada. Con
   `auto` el lienzo toma el alto que le pide su proporcion y la vista scrollea,
   que es justo lo que se acepto. */
/* Antes lo decidia el media query (orientation: landscape). Ahora lo decide
   data-orient, que pone el <head>: asi la disposicion apaisada se puede
   FORZAR tambien en una pantalla de pie, y al reves. El segundo media cubre
   justo ese caso (horizontal forzado en una TV girada de 1080 de ancho). */
@media (min-width: 1181px), (orientation: portrait) and (min-width: 1000px) {
  /* Aqui la vista deja de ser rejilla y pasa a flujo normal: son tres bloques
     apilados y nada mas. Con rejilla, la fila del plano se quedaba con el alto
     que sobraba en la pantalla en vez de crecer hasta el alto que pide el
     dibujo, y el riel se montaba encima. En flujo normal cada bloque toma su
     propio alto y el que scrollea es #view-dashboard, que ES el .view: por eso
     lleva overflow-y:auto y no overflow:visible, que lo dejaria sin barra y el
     <body> (overflow:hidden) se comeria lo que sobra. */
  :root[data-orient="horizontal"] #view-dashboard {
    display: block;
    overflow-y: auto;
  }
  :root[data-orient="horizontal"] #dash-alarm-banner { margin-bottom: var(--sp-3); }
  /* Tres columnas: tarjetas · plano · tarjetas. Las laterales van a ancho
     FIJO y el centro flexiona, para que el plano se quede con todo lo que
     sobre. Las dos tarjetas de cada columna se reparten el alto (flex:1). */
  :root[data-orient="horizontal"] .dash-medio {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr) 220px;
    align-items: stretch;
  }
  /* Las tarjetas LLENAN su columna. El intento anterior de estirarlas se
     revirtió porque dejaba tres lecturas flotando en media pantalla vacía y la
     segunda tarjeta se salía por abajo; las dos cosas están resueltas ahora:
     el alto extra se lo reparten las filas (.side-rows va a space-between) y
     lo ocupa contenido -- números a 2.4rem -- en vez de aire, y el flex-basis
     de 0 con min-height:0 impide que ninguna desborde.
     Sin esto, a 1920x1080 sobraban unos 450 px muertos en cada columna. */
  :root[data-orient="horizontal"] .dash-col > .side-panel { flex: 1 1 0; min-height: 0; }
  :root[data-orient="horizontal"] .dash-col { align-content: stretch; }
  /* UNA PANTALLA EXACTA para banner + banda central. El plano deja de imponer
     su alto y pasa a ajustarse al que haya: si aparece la franja de alarma, el
     dibujo se encoge unos pixeles en vez de empujar la leyenda fuera de vista.
     Como la proporcion del viewBox (1,62) ya casi coincide con la de la caja
     (1,60), lo que se pierde a los lados es alrededor del 1 %. */
  :root[data-orient="horizontal"] .dash-pantalla {
    height: 100%;
    display: grid; grid-template-rows: auto minmax(0, 1fr); gap: var(--sp-3);
  }
  :root[data-orient="horizontal"] .dash-medio { min-height: 0; height: 100%; }
  :root[data-orient="horizontal"] .syn-panel { height: 100%; display: flex; flex-direction: column; overflow: hidden; }
  :root[data-orient="horizontal"] .syn-canvas { display: flex; flex: 1 1 auto; min-height: 0; }
  :root[data-orient="horizontal"] .syn-canvas .sv { width: 100%; height: 100%; }

  /* La proporcion la lleva el PROPIO SVG, no la caja.
     Con height:100% el svg no tiene alto propio, el .syn-canvas se queda sin
     contenido que medir y el flex lo aplasta a lo que sobre; un aspect-ratio
     sobre la caja tampoco gana, porque para entonces el flex ya le fijo el
     alto. Con width:100% y height:auto el svg se dimensiona solo a partir de
     su viewBox.
     Ademas el .syn-canvas deja de ser contenedor flex: en fila y con
     align-items:stretch estiraba el svg a lo alto y luego lo encogia a su
     ancho intrinseco, dejandolo estrecho y centrado en la tarjeta. Como
     bloque, el svg toma el ancho completo y calcula su alto.
     Y la tarjeta deja de recortar, porque ahora es mas alta que la ventana a
     proposito y es #view-dashboard quien scrollea. */
  /* El cromo de la tarjeta se adelgaza aqui y solo aqui: cada pixel que se
     le quita a la cabecera, a la leyenda y al acolchado es un pixel mas de
     dibujo, y el plano va justo de alto para caber sin bajar. */
  :root[data-orient="horizontal"] .syn-head { padding: 4px var(--sp-4); }
  :root[data-orient="horizontal"] .syn-legend { padding: 3px var(--sp-4) 4px; }
  :root[data-orient="horizontal"] .syn-canvas { padding: var(--sp-1); }
}

/* movil: el plano se escala completo por viewBox aunque quede pequeno
   (cortarlo o dejarlo con scroll horizontal se lee como pagina rota),
   y los resumenes se apilan */
@media (max-width: 760px) {
  /* al volver a columna hay que soltar el flex-basis 0 del modo fila:
     con basis 0 en vertical los paneles se colapsan a una linea */
  .dash-medio { grid-template-columns: minmax(0, 1fr); }
  .graph-ribbon .graph-canvas-wrap { height: 170px; flex: 0 0 auto; }
  .syn-canvas { padding: var(--sp-1); }
  .syn-head { flex-wrap: wrap; gap: var(--sp-2); }
  .syn-status { gap: var(--sp-3); }
  .side-val b { font-size: 1.1rem; }
  /* en un telefono el plano se escala a un tercio y sus textos caen a 4-5
     px: deja de informar. Se retira y en su lugar van los numeros. */
  .syn-canvas, .syn-legend { display: none; }
  #dash-movil { display: block; grid-column: 1; grid-row: 3; }
  
  .syn-panel { border-bottom: none; }
}

@media (max-width: 560px) {
  /* cuatro pestanas tienen que caber sin recortarse en 360 px */
  .nav-tab { padding: 11px 1px; font-size: 0.6rem; letter-spacing: -0.02em; }
  .tab-larga { display: none; }
  .tab-corta { display: inline; }
  #nav-tabs { overflow-x: hidden; }
  .syn-title { font-size: 1.05rem; }
  .syn-link { padding: 6px 10px; font-size: 0.72rem; }
}

/* ===================== FLUJO DE DATOS DEL BUS (/sistema) =====================
   Pagina interna. La tira de marcas muestra el resultado de las ultimas
   transacciones con cada esclavo: continua = sano, rayada = intermitente. */
.bus-lista { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: var(--sp-2); }
.bus-fila { display: flex; flex-direction: column; gap: 6px; }
.bus-cab { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.bus-nombre { font-family: var(--font-display); font-weight: 700; font-size: 1rem; }
.bus-slave { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); }
.bus-tasa {
  margin-left: auto; font-family: var(--font-mono); font-size: 1.1rem;
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.bus-hace { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); min-width: 11ch; text-align: right; }
.bus-tira { display: flex; gap: 1px; height: 14px; }
.bus-tick { flex: 1 1 0; min-width: 1px; background: var(--critical); border-radius: 1px; }
.bus-tick.ok { background: var(--ok); }
.bus-err { font-family: var(--font-mono); font-size: 0.7rem; color: var(--warn); word-break: break-word; }

@media (max-width: 560px) {
  .bus-hace { min-width: 0; }
  .bus-tira { height: 10px; }
}


/* =====================================================================
   BARRA DE SCROLL
   Sobre un fondo casi negro, la barra gris del navegador es el elemento
   mas claro de toda la pantalla y queda pegada a la columna de tarjetas.
   En un stand es lo primero que delata que esto es una pagina web.
   ===================================================================== */
.view::-webkit-scrollbar { width: 10px; height: 10px; }
.view::-webkit-scrollbar-track { background: var(--bg); }
.view::-webkit-scrollbar-thumb {
  background: var(--panel3); border-radius: 5px;
  border: 2px solid var(--bg);
}
.view::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }
.view { scrollbar-width: thin; scrollbar-color: var(--panel3) var(--bg); }

/* --- "sin dato" en las tarjetas ---
   Un guion a 1,9 rem en monoespaciada se lee como texto tachado, no como
   ausencia. El plano ya resolvio esto: "sin dato no se escribe con rayas,
   va la palabra". Las tarjetas siguen la misma regla. --- */
.side-row.is-vacio .side-val b {
  font-size: 1.05rem; color: var(--sv-faint);
  letter-spacing: 0.08em; font-weight: 600;
}
.side-row.is-vacio .side-val { color: transparent; }

/* ===================== SINOPTICO DE DETALLE DE CADA CUARTO =====================
   Hermano del principal (docs/herramientas/genera-sinoptico-cuartos.py): usa
   su mismo vocabulario de clases, y aqui solo va lo que le es propio. A
   diferencia del principal no llena la pantalla -- va entre los controles y
   los medidores -- asi que el lienzo se ajusta al ancho y conserva su
   proporcion, en vez de estirarse a la altura del panel. */
.svc-panel { margin-bottom: var(--sp-4); }
/* bloque, no flex: como hijo flexible de un panel sin altura propia, el SVG
   con height:auto colapsaba a cero en el movil */
.svc-panel .svc-canvas { display: block; flex: 0 0 auto; }
/* .syn-panel.svc-panel delante: la regla apaisada del principal
   (:root[data-orient=horizontal] .syn-canvas .sv, height:100%) pesa mas que
   .svc-canvas .sv y estiraba el SVG a la altura del panel */
.syn-panel.svc-panel .svc-canvas .sv { height: auto; aspect-ratio: 1600 / 600; max-height: 62vh;
  /* el ancho sigue al tope de altura: sin esto, en una pantalla apaisada el
     SVG ocupaba todo el ancho con la altura recortada y el dibujo quedaba
     centrado entre dos franjas vacias */
  width: min(100%, calc(62vh * 1600 / 600)); margin: 0 auto; }
/* degradado propio de cada dibujo: el de #sv-root no se pinta mientras el
   resumen esta oculto, que es justo cuando se ve esta pestaña */
#svc-cd .sv-machine { fill: url(#svc-cd-g-eq); }
#svc-cg .sv-machine { fill: url(#svc-cg-g-eq); }
#svc-cd .sv-eq.is-on .sv-machine, #svc-cg .sv-eq.is-on .sv-machine { fill: var(--eq-lift); }
#svc-vx.sv { aspect-ratio: 1600 / 510; width: min(100%, calc(62vh * 1600 / 510)); }
#svc-vx .sv-hx-body { fill: url(#svc-vx-g-eq); }
#svc-vx .sv-eq.is-on .sv-hx-body { fill: var(--eq-lift); }

/* LED de rele: gris apagado, verde con el rele cerrado. Solo esos dos
   estados -- el controlador no reporta otro. */
.svc-led { fill: var(--panel3); stroke: var(--sv-dim); stroke-width: 2; }
.svc-led.is-on { fill: var(--ok); stroke: var(--ok); }
.svc-proc { font-family: var(--font-mono); font-size: 17px; font-weight: 600; fill: var(--text); }

/* resistencia de deshielo del evaporador de expansion directa: en neutro
   mientras no calienta, en el color de la descarga cuando el rele DEFR cierra */
.svc-resist { fill: none; stroke: var(--sv-dim); stroke-width: 3; stroke-linejoin: round; opacity: 0.7; }
.svc-resist.is-on { stroke: var(--pipe-hot); opacity: 1; }

.svc.sv-off .svc-proc, .svc.sv-off .sv-val { fill: var(--sv-dim); }
/* la vista vertical sube el panel del sinoptico principal al principio del
   resumen (order:-1); en las pestañas de cuarto los controles van primero */
:root[data-orient="vertical"] .syn-panel.svc-panel { order: 0; grid-column: auto; }
/* y en apaisada no heredan el panel a toda altura ni el lienzo flexible del
   principal: el dibujo manda su alto por proporcion, sin franjas vacias */
:root[data-orient="horizontal"] .syn-panel.svc-panel { height: auto; }
:root[data-orient="horizontal"] .syn-panel.svc-panel .svc-canvas { display: block; flex: 0 0 auto; }
:root[data-orient="horizontal"] .syn-panel.svc-panel .svc-canvas .sv { height: auto; }

/* ===================== FLECHAS DE PASO Y TECLADO EN PANTALLA =====================
   El kiosco tapa el teclado virtual del sistema: estas son las dos formas de
   cambiar un valor solo con el dedo. Todo a 44 px como minimo, que es lo que
   pide un objetivo tactil fiable. */
.btn-paso {
  appearance: none; cursor: pointer; flex: 0 0 auto;
  width: 44px; min-height: 44px; padding: 0;
  border: 1.5px solid var(--border-strong); border-radius: var(--r-sm);
  background: var(--panel2); color: var(--text);
  font-family: var(--font-mono); font-size: 1.35rem; font-weight: 600; line-height: 1;
  touch-action: manipulation; user-select: none; -webkit-user-select: none;
  transition: background 0.12s, border-color 0.12s, scale var(--dur-pulsar) var(--ease-out);
}
.btn-paso:hover { border-color: var(--brand); }
.btn-paso:active { background: var(--panel3); border-color: var(--brand); color: var(--brand); }
.input-box, .input-box-sm { min-height: 44px; box-sizing: border-box; }
.ctrl-inline { align-items: stretch; }

#btn-teclado {
  appearance: none; cursor: pointer;
  flex: 0 0 auto; width: 36px; height: 36px; display: grid; place-items: center; padding: 0;
  background: var(--panel2); border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--text-dim);
  transition: background 0.15s, color 0.15s, border-color 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
#btn-teclado:hover { background: var(--panel3); color: var(--text); }
#btn-teclado .icon { width: 18px; height: 18px; }
/* activo se nota: es un modo, no una accion de un solo toque */
#btn-teclado.is-on { color: var(--brand); border-color: var(--brand); background: var(--panel3); }

#teclado {
  position: fixed; z-index: 1000;
  right: 16px; bottom: 16px;
  width: 300px; max-width: calc(100vw - 32px);
  background: var(--panel);
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  padding: 10px;
  touch-action: manipulation; user-select: none; -webkit-user-select: none;
}
#teclado.hidden { display: none; }
.tk-cabeza { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.tk-destino {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--font-body); font-size: 0.78rem; font-weight: 600; color: var(--brand);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tk-rejilla { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
#teclado button {
  appearance: none; cursor: pointer;
  min-height: 54px; padding: 0;
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  background: var(--panel2); color: var(--text);
  font-family: var(--font-mono); font-size: 1.4rem; font-weight: 600;
}
#teclado button:active { background: var(--panel3); border-color: var(--brand); }
#teclado .tk-fn { color: var(--text-dim); font-size: 1.2rem; }
#teclado .tk-ok { color: var(--ok); border-color: var(--ok); font-family: var(--font-body); font-size: 1.05rem; }
#teclado .tk-cerrar { min-height: 36px; width: 36px; flex: 0 0 auto; font-size: 1rem; color: var(--text-dim); }

@media (max-width: 560px) {
  /* en un celular el teclado del sistema si funciona -- el problema es solo
     del kiosco -- y el hueco en la cabecera se lo lleva el titulo */
  #btn-teclado { display: none; }
  /* en el movil la barra de pestañas va fija abajo: el teclado se apoya
     encima de ella y ocupa el ancho, para que las teclas sigan siendo grandes */
  #teclado { left: 8px; right: 8px; width: auto; max-width: none; bottom: 64px; }
}

/* Con el boton del teclado la cabecera ya no cabe entera a 1080 px (la
   pantalla vertical del stand) y el boton de tema saltaba a una segunda
   fila. Por debajo de 1180 se queda en una linea y lo que cede es la marca
   de hora, que se recorta con puntos suspensivos: el titulo y los botones
   son lo que hay que poder tocar. */
@media (max-width: 1180px) {
  #topbar { flex-wrap: nowrap; }
  /* encoge mucho antes que el titulo: flex-shrink pondera por el ancho */
  #ts-text { flex: 0 100 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}

/* Regreso de /sistema al dashboard. Boton grande y con texto, no solo un
   icono: en el kiosco es la unica forma de salir de esta pagina. */
#btn-volver {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 16px;
  background: var(--panel2); border: 1.5px solid var(--brand); border-radius: var(--r-sm);
  color: var(--brand); text-decoration: none;
  font-family: var(--font-body); font-weight: 700; font-size: 0.9rem;
  transition: background 0.15s, scale var(--dur-pulsar) var(--ease-out);
}
#btn-volver:hover { background: var(--panel3); }
#btn-volver .icon { width: 18px; height: 18px; }

/* ===================== PESTAÑA EFICIENCIA =====================
   Pantalla comercial: las cifras en pesos son lo que se mira de lejos, asi
   que van grandes; el kWh que las respalda, debajo y mas pequeño. Lo medido
   y lo estimado se distinguen por color: el ahorro (estimado) va en verde,
   el costo (medido) en el color de texto normal. */
.ef-controlbar { margin-bottom: var(--sp-3); }
.ef-input-tarifa { width: 96px; }
.ef-aviso {
  margin: 0 0 var(--sp-3); padding: 10px var(--sp-4);
  border: 1px solid var(--warn); border-radius: var(--r-sm);
  color: var(--warn); background: var(--panel);
  font-family: var(--font-body); font-weight: 600; font-size: 0.85rem;
}
.ef-aviso.is-hidden { display: none; }
.ef-panel { margin-bottom: var(--sp-3); }
.ef-titulo-nota {
  font-family: var(--font-body); font-weight: 600; font-size: 0.75rem;
  color: var(--text-dim); letter-spacing: 0; margin-left: 8px; text-transform: none;
}
.ef-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-3); }
.ef-card {
  background: var(--panel2); border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: 14px 16px; min-width: 0;
}
.ef-card-titulo {
  font-family: var(--font-body); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.08em; color: var(--text-dim); margin-bottom: 6px;
}
.ef-dinero {
  font-family: var(--font-mono); font-size: 2rem; font-weight: 600; line-height: 1.1;
  color: var(--text); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ef-unidad { font-size: 1rem; color: var(--text-dim); }
.ef-sub { font-family: var(--font-body); font-size: 0.82rem; color: var(--text-dim); margin-top: 4px; }
.ef-sub b { font-family: var(--font-mono); color: var(--text); font-weight: 600; }
.ef-desde { font-family: var(--font-body); font-size: 0.72rem; color: var(--warn); margin-top: 4px; }
.ef-desde:empty { display: none; }

.ef-card-ahorro { border-color: var(--ok); }
.ef-card-ahorro .ef-dinero { color: var(--ok); }

.ef-ambiente {
  display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: center;
  margin-top: var(--sp-3); padding: 14px 16px;
  border: 1px dashed var(--border-strong); border-radius: var(--r-sm);
}
.ef-arboles { display: flex; align-items: center; gap: 14px; flex: 1 1 320px; min-width: 0; }
.ef-icono-arbol { width: 48px; height: 48px; flex: 0 0 auto; color: var(--ok); }
.ef-arboles-num { font-family: var(--font-mono); font-size: 2rem; font-weight: 600; color: var(--ok); line-height: 1.1; }
.ef-co2 { flex: 0 1 auto; }

.ef-panel-r290 .lect-titulo { display: flex; align-items: center; gap: 8px; }
.ef-panel-r290 .lect-titulo .icon { width: 20px; height: 20px; color: var(--ok); }
.ef-r290-texto { font-family: var(--font-body); font-size: 0.95rem; line-height: 1.5; color: var(--text); margin: 0 0 var(--sp-3); max-width: 70ch; }
.ef-gwp { display: grid; gap: 8px; }
.ef-gwp-fila { display: grid; grid-template-columns: minmax(120px, 230px) 1fr 64px; align-items: center; gap: 12px; }
.ef-gwp-nombre { font-family: var(--font-body); font-size: 0.85rem; font-weight: 600; color: var(--text-dim); }
.ef-gwp-pista { height: 14px; background: var(--panel3); border-radius: 3px; overflow: hidden; }
.ef-gwp-barra { display: block; height: 100%; background: var(--text-dim); }
.ef-gwp-num { font-family: var(--font-mono); font-size: 0.95rem; font-weight: 600; text-align: right; color: var(--text); }
.ef-gwp-fila.is-propio .ef-gwp-nombre, .ef-gwp-fila.is-propio .ef-gwp-num { color: var(--ok); }
.ef-gwp-fila.is-propio .ef-gwp-barra { background: var(--ok); }
.ef-gwp-pie { font-family: var(--font-body); font-size: 0.72rem; color: var(--text-dim); margin-top: 8px; }

.ef-graficas { grid-template-columns: 1fr; }
.ef-supuestos {
  font-family: var(--font-body); font-size: 0.75rem; line-height: 1.5; color: var(--text-dim);
  margin: var(--sp-3) 0 var(--sp-4); max-width: 110ch;
}

@media (max-width: 560px) {
  .ef-dinero, .ef-arboles-num { font-size: 1.6rem; }
  .ef-gwp-fila { grid-template-columns: 1fr 56px; }
  .ef-gwp-pista { grid-column: 1 / -1; order: 3; }
}
/* .lect-panel reparte ANCHO con flex-basis (n x 200 px). En vertical la vista
   es una columna flexible y esa misma base pasaba a ser una ALTURA fija que
   recortaba los arboles y las barras de refrigerantes: aqui cada panel mide
   lo que su contenido. */
#view-eficiencia > .ef-panel { flex: 0 0 auto; }
/* las cifras en pesos llegan a 11 caracteres ("$ 2.496.400"): con cuatro
   tarjetas en 1080 px de ancho (pantalla vertical) no caben a 2rem y se
   cortaban con puntos suspensivos. Escalan con el ancho, con techo en 2rem. */
.ef-dinero { font-size: clamp(1.35rem, 2.1vw, 2rem); }

/* =====================================================================
   MOVIMIENTO
   Todo lo que se mueve aqui anima SOLO transform/scale y opacity: la
   Raspberry lo compone en la GPU sin redibujar. Nada de blur, sombras
   animadas ni tamanos. El flujo de las tuberias y los ventiladores ya
   gastan el presupuesto continuo, asi que esto solo corre al tocar algo.
   ===================================================================== */

/* 1. RESPUESTA AL PULSAR. En un kiosco tactil no hay hover: el hundimiento
   es lo que confirma que el dedo entro. Se usa la propiedad `scale` (no
   transform) para no pisar ningun transform que ya tenga el boton. Los que
   ya declaraban transicion llevan la de `scale` sumada a mano; :where() no
   suma especificidad, asi que solo alcanza a los que no tenian ninguna. */
:where(button, .syn-link, #btn-sistema, #btn-volver) { transition: scale var(--dur-pulsar) var(--ease-out); }
button:active:not(:disabled), .syn-link:active, #btn-sistema:active, #btn-volver:active { scale: 0.97; }
/* las tarjetas laterales son grandes: 0.97 en 250 px se lee como un salto */
.side-panel:active:not(:disabled) { scale: 0.985; }

/* 2. INDICADOR DE PESTANA. Una sola barra que viaja de una pestana a la
   otra (translateX + scaleX sobre 1 px de ancho) en vez de un borde que
   aparece y desaparece. app.js la coloca; sin JS quedan los bordes. */
#tab-indicador {
  position: absolute; left: 0; bottom: 0; width: 1px; height: 3px;
  background: var(--brand); transform-origin: 0 0; pointer-events: none;
  opacity: 0;
}
#nav-tabs.con-indicador #tab-indicador { opacity: 1; }
#tab-indicador.anima { transition: transform 260ms var(--ease-in-out); }
#nav-tabs.con-indicador .nav-tab.active { border-top-color: transparent; border-bottom-color: transparent; }
@media (max-width: 560px) {
  /* en el movil la barra va abajo y la marca arriba de la pestana */
  #tab-indicador { top: 0; bottom: auto; }
}

/* 3. ENTRADA DE CADA PESTANA. Los bloques de la vista (cabecera, dibujo,
   lecturas, graficas) suben 8 px y aparecen escalonados 40 ms: se lee
   como una pantalla que se arma, no como un cambio de golpe. Keyframes y
   no transicion porque la vista viene de display:none. */
.view.entrando > * { animation: vistaEntra 240ms var(--ease-out) both; }
.view.entrando > :nth-child(2) { animation-delay: 40ms; }
.view.entrando > :nth-child(3) { animation-delay: 80ms; }
.view.entrando > :nth-child(4) { animation-delay: 120ms; }
.view.entrando > :nth-child(n+5) { animation-delay: 160ms; }
@keyframes vistaEntra { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes vistaEntraSuave { from { opacity: 0; } to { opacity: 1; } }

/* Reducir movimiento: menos y mas suave, no cero. Se queda el fundido,
   se va todo desplazamiento y escala. */
@media (prefers-reduced-motion: reduce) {
  button:active:not(:disabled), .syn-link:active, #btn-sistema:active, #btn-volver:active,
  .side-panel:active:not(:disabled) { scale: none; }
  #tab-indicador.anima { transition: none; }
  .view.entrando > * { animation-name: vistaEntraSuave; animation-delay: 0ms; }
}
