/* CMMS ISO 14224 — Estilos */

/* Design System — Fase 1 (tipografía + tokens de color) — pedido del
   usuario: una sola familia tipográfica en toda la app (Inter, vía Google
   Fonts) en vez de depender solo de la fuente del sistema. La paleta de
   color (fondo/superficie/borde/texto/acento) que pidió el usuario YA
   coincidía en un 90% con los tokens --c-* de abajo (quedaron así del
   ajuste de contraste anterior) — no se tocaron para no afectar el resto
   de la app; solo se sumó --c-strong-text (títulos de pantalla, ver
   .cmms-page-header h1 más abajo) con el valor exacto pedido (#0F172A). */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root { --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }

/* Reset global de box-sizing: sin esto, el padding y el borde de un
   elemento se SUMAN a su "width" en vez de quedar incluidos adentro — por
   eso los <input>/<select> de .cmms-field (padding 7px 9px + borde 1px)
   con width:100% terminaban ~20px más anchos que su columna del grid y se
   salían del contorno del formulario (ver .cmms-field input/select/
   textarea más abajo, y cmms-field-row que usa CSS grid). Antes solo unos
   pocos elementos puntuales se arreglaban a mano con box-sizing:border-box
   (ver .cmms-cal-day, #cmms-login-left/right) — con el reset ya no hace
   falta repetirlo elemento por elemento. */
*, *::before, *::after { box-sizing: border-box; }

/* Reset de <html>/<body> (pedido del usuario) — sin esto, el navegador les
   pone su margen por defecto (8px) y fondo blanco/transparente propio, así
   que alrededor de .cmms-app (que sí pinta su fondo con var(--c-bg), ver
   más abajo) quedaba un marco delgado de OTRO color, ajeno al tema
   Día/Noche — se notaba sobre todo en pantalla completa (modo TV, sin
   ninguna barra del navegador alrededor que lo disimule) pero pasaba
   siempre. Esos mismos 16px de margen (8 arriba + 8 abajo) hacían que la
   página completa midiera más que 100vh, empujando una barra de scroll
   vertical de más en el modo TV. height:100% para que los hijos con
   min-height:100vh (.cmms-app) puedan apoyarse en un ancestro con alto
   real, no solo el viewport. */
html, body { margin: 0; padding: 0; height: 100%; background: var(--c-bg); }

/* Blindaje extra contra la barra de scroll del navegador en pantalla
   completa — enterTvMode()/exitTvMode() (app.js) agregan/sacan esta clase
   en <html> Y <body>. .cmms-tv-mode ya tiene su propio overflow:hidden y
   height:100vh (ver más abajo), pero 100vh puede no dar un número entero
   de píxeles exacto (redondeo de subpíxel) — eso seguía dejando aparecer
   una barra de scroll "fantasma" de 1px. Esto la elimina de raíz, a nivel
   del documento completo. */
html.cmms-tv-lock, body.cmms-tv-lock { overflow: hidden; }

/* ── Selector de tema (Día / Noche) ──────────────────────────────────────
   Dos variantes en la escala de azul (ver toggle ☀️/🌙 arriba a la derecha
   del topbar, cmmsThemeSwitcherHTML() en app.js). Los tokens de abajo
   (:root) son el tema NOCHE por defecto — así, si por cualquier motivo el
   JS de tema no llega a correr, la app se ve exactamente igual que antes de
   este cambio. El tema DÍA pisa los mismos tokens vía [data-cmms-theme=
   "light"] en <html>, que es lo que alterna cmmsApplyTheme() en app.js
   (persistido en localStorage). Los colores "semánticos" (danger/success/
   warning, badges de estado, RPN) se mantienen fijos en ambos temas a
   propósito — son colores de estado (rojo/verde/naranja), no parte de la
   paleta de tema. */
:root {
  --c-bg:       #0b1220;
  --c-surface:  #141d30;
  --c-surface-2:#1a2540;
  --c-border:   #263049;
  --c-text:     #ccd4e3;
  --c-muted:    #94a3b8;
  --c-accent:   #3b82f6;
  --c-accent-h: #2563eb;
  --c-danger:   #ef4444;
  --c-success:  #22c55e;
  --c-warning:  #f59e0b;
  --radius:    8px;

  /* Paneles con degradé (tarjetas / KPI / secciones de formulario / tiles) */
  --c-panel-1:  #182440;
  --c-panel-2:  #111a2e;
  --c-kpi-1:    #1a2540;
  --c-kpi-2:    #111a2e;
  --c-panel-shadow:       0 10px 24px -12px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.05);
  --c-panel-shadow-hover: 0 16px 30px -12px rgba(0,0,0,.6),  inset 0 1px 0 rgba(255,255,255,.07);
  --c-donut-inner: #17223c;
  --c-row-hover: rgba(255,255,255,.02);
  /* KPI de alerta (ver .cmms-kpi.warn/.danger — "Ítems bajo stock mínimo",
     "Compras pendientes de aprobar", etc: se pintan así cuando se cumple
     alguna condición de alerta). El tinte de color SÍ es semántico (naranja/
     rojo, fijo en los 3 temas — mismo criterio que badges/RPN), pero el
     fondo sobre el que se apoya ese tinte debe seguir el tema igual que
     cualquier otro KPI, si no queda oscuro también en modo claro. */
  --c-kpi-warn-1:   #2a2214;
  --c-kpi-warn-2:   #171d31;
  --c-kpi-danger-1: #2c1a1e;
  --c-kpi-danger-2: #171d31;
  --c-kpi-icon-bg:  rgba(255,255,255,.07);

  /* Topbar */
  --c-topbar-1: #0d1728;
  --c-topbar-2: #182746;
  --c-topbar-text: #dde3f0;
  --c-topbar-btn-bg: rgba(255,255,255,.14);
  --c-topbar-btn-border: rgba(255,255,255,.22);
  --c-topbar-btn-text: #fff;
  --c-topbar-btn-hover-bg: rgba(255,255,255,.24);
  --c-topbar-btn-hover-border: rgba(255,255,255,.4);

  /* Sidebar / navegación */
  --c-sidebar-bg: #0d1626;
  --c-nav-text: #93a0b8;
  --c-nav-hover-bg: rgba(255,255,255,.05);
  --c-nav-hover-text: #e4e9f5;
  --c-nav-active-bg: rgba(59,130,246,.14);
  --c-nav-icon: #e7ebf5;

  /* Botones / texto fuerte sobre superficie (p.ej. pestaña activa del wizard) */
  --c-btn-hover-bg: #232f4a;
  --c-strong-text: #fff;

  /* Pastilla de <code> (TAG de activo / equipo en tablas) — identificador
     primario de la fila, tiene que leerse de un vistazo. Fondo/texto
     propios (no reusan --c-surface-2/--c-muted) para poder afinarlos sin
     tocar el resto de la paleta — ver .cmms-table code más abajo. */
  --c-code-bg:   rgba(255,255,255,.07);
  --c-code-text: #dbe4f5;

  /* Línea divisoria entre filas de tabla — hiper sutil en modo Noche (el
     borde normal, --c-border, queda demasiado marcado sobre el fondo
     oscuro de la tabla). */
  --c-table-divider: rgba(255,255,255,.05);

  /* Simulador de roles (Configuración > Permisos > Simulador) — paleta
     categórica para distinguir hasta 9 roles a la vez en el diagrama de
     flujo, más su fondo pastel a juego (--role-N-bg, usado en el nodo de
     etapa cuando ese rol la cubre). Tema Noche: tonos saturados sobre fondo
     oscuro. */
  --role-1: #60a5fa; --role-1-bg: rgba(96,165,250,.16);
  --role-2: #a78bfa; --role-2-bg: rgba(167,139,250,.16);
  --role-3: #2dd4bf; --role-3-bg: rgba(45,212,191,.16);
  --role-4: #fbbf24; --role-4-bg: rgba(251,191,36,.16);
  --role-5: #f472b6; --role-5-bg: rgba(244,114,182,.16);
  --role-6: #818cf8; --role-6-bg: rgba(129,140,248,.16);
  --role-7: #4ade80; --role-7-bg: rgba(74,222,128,.16);
  --role-8: #38bdf8; --role-8-bg: rgba(56,189,248,.16);
  --role-9: #a8a29e; --role-9-bg: rgba(168,162,158,.16);
}

/* Tema DÍA — blanco con acentos azules. */
html[data-cmms-theme="light"] {
  /* v51 — antes #eef3fb (celeste pastel saturado) para el fondo Y --c-
     surface-2/--c-border/--c-panel-2/--c-kpi-2/--c-btn-hover-bg también en
     tonos celeste: con .cmms-app pintando además "blobs" azules encima (ver
     radial-gradient más abajo) y CADA tarjeta/tabla/botón usando de fondo
     ese mismo celeste, la app entera quedaba con un tinte lavanda parejo
     que cansa la vista rápido. Estos tokens (los que arma cualquier
     superficie de contenido: fondo de página, fondo alterno de
     tabla/botón/chip, degradé de tarjetas/KPI, borde) pasan a una escala
     de gris neutro (slate) — así el nuevo look "blanco + gris" queda
     aplicado en toda la app automáticamente, no tarjeta por tarjeta. El
     acento azul (--c-accent) y el topbar/sidebar (los tokens --c-topbar-N y
     --c-sidebar-bg, con su propio tinte celeste a propósito, ver más abajo)
     quedan igual: son la marca/el "chrome" de navegación, no la superficie
     de contenido que generaba el cansancio visual. */
  --c-bg:        #f8fafc;
  --c-surface:   #ffffff;
  --c-surface-2: #f1f5f9;
  --c-border:    #e2e8f0;
  --c-text:      #16233c;
  --c-muted:     #5b6b85;
  --c-accent:    #2563eb;
  --c-accent-h:  #1d4ed8;

  --c-panel-1: #ffffff;
  --c-panel-2: #f1f5f9;
  --c-kpi-1:   #ffffff;
  --c-kpi-2:   #f1f5f9;
  --c-panel-shadow:       0 6px 16px -10px rgba(30,58,110,.25), inset 0 1px 0 rgba(255,255,255,.6);
  --c-panel-shadow-hover: 0 10px 22px -10px rgba(30,58,110,.3), inset 0 1px 0 rgba(255,255,255,.7);
  --c-donut-inner: #ffffff;
  --c-row-hover: rgba(37,99,235,.05);
  --c-kpi-warn-1:   #fff8ea;
  --c-kpi-warn-2:   #ffffff;
  --c-kpi-danger-1: #fff1f1;
  --c-kpi-danger-2: #ffffff;
  --c-kpi-icon-bg:  rgba(22,35,60,.06);

  /* Design System — Fase 1: navbar #E2E8F0/#D9E2EC exactos (antes un celeste
     más saturado, #dbe7fb/#c3d8f7). */
  --c-topbar-1: #e2e8f0;
  --c-topbar-2: #d9e2ec;
  --c-topbar-text: #16233c;
  --c-topbar-btn-bg: rgba(22,35,60,.08);
  --c-topbar-btn-border: rgba(22,35,60,.16);
  --c-topbar-btn-text: #16233c;
  --c-topbar-btn-hover-bg: rgba(22,35,60,.14);
  --c-topbar-btn-hover-border: rgba(22,35,60,.24);

  /* Design System — Fase 1: sidebar #F1F5F9 exacto (antes celeste #e3ecfa;
     el borde derecho ya usa var(--c-border) = #E2E8F0, ver .cmms-sidebar). */
  --c-sidebar-bg: #f1f5f9;
  --c-nav-text: #3c4d6b;
  --c-nav-hover-bg: rgba(37,99,235,.08);
  --c-nav-hover-text: #16233c;
  --c-nav-active-bg: rgba(37,99,235,.14);
  --c-nav-icon: #111827;

  --c-btn-hover-bg: #e2e8f0;
  /* #0F172A exacto (Design System, títulos de pantalla) — antes #16233c,
     casi el mismo tono navy, se afina para calzar con el valor pedido. */
  --c-strong-text: #0f172a;

  /* Pastilla de <code> (TAG) en modo Día — antes heredaba texto claro
     (pensado para fondo oscuro) sobre un fondo blanquecino: casi invisible.
     Texto navy oscuro, buen contraste sobre cualquier fondo claro. */
  --c-code-bg:   #eef2f8;
  --c-code-text: #1e293b;

  /* Design System — Fase 2/3: separador de fila #F1F5F9 exacto (más suave
     que --c-border, para que la línea entre filas no compita visualmente
     con el borde de la tarjeta). */
  --c-table-divider: #f1f5f9;

  /* Simulador de roles — mismos 9 slots, tema Día: mismos tonos base, algo
     más saturados para seguir leyéndose bien sobre fondo blanco/gris claro. */
  --role-1: #2563eb; --role-1-bg: #dbeafe;
  --role-2: #7c3aed; --role-2-bg: #ede9fe;
  --role-3: #0d9488; --role-3-bg: #ccfbf1;
  --role-4: #b45309; --role-4-bg: #fef3c7;
  --role-5: #be185d; --role-5-bg: #fce7f3;
  --role-6: #4338ca; --role-6-bg: #e0e7ff;
  --role-7: #15803d; --role-7-bg: #dcfce7;
  --role-8: #0369a1; --role-8-bg: #e0f2fe;
  --role-9: #57534e; --role-9-bg: #f5f5f4;
}

/* Design System — Fase 2: badges en 4 familias semánticas exactas (tema
   Día), fondo pastel + texto oscuro de alto contraste en vez del tono
   "neón sobre transparencia" (pensado para el tema Noche, que no se toca).
   Mapeo por significado, no por nombre de clase — algunas clases que antes
   compartían color con otras ahora quedan en familias distintas porque el
   spec las define de forma explícita (ver comentario en cada bloque):

   ROJO (Correctiva / Falla / Crítico) */
html[data-cmms-theme="light"] .cmms-crit-A,
html[data-cmms-theme="light"] .cmms-st-Falla,
html[data-cmms-theme="light"] .cmms-prio-D,
html[data-cmms-theme="light"] .cmms-wost-Ppto-Rechazado,
html[data-cmms-theme="light"] .cmms-ot-CM { background: #fee2e2; color: #991b1b; }

/* ÁMBAR (En proceso / Pendiente / Mantención) */
html[data-cmms-theme="light"] .cmms-crit-B,
html[data-cmms-theme="light"] .cmms-st-Mantenimiento,
html[data-cmms-theme="light"] .cmms-ot-PdM,
html[data-cmms-theme="light"] .cmms-prio-A,
html[data-cmms-theme="light"] .cmms-wost-Pendiente-Aprobación,
html[data-cmms-theme="light"] .cmms-wost-Suspendida { background: #fef3c7; color: #92400e; }

/* VERDE (Preventiva / Operando / Aprobada) */
html[data-cmms-theme="light"] .cmms-crit-C,
html[data-cmms-theme="light"] .cmms-st-Operativo,
html[data-cmms-theme="light"] .cmms-ot-PM,
html[data-cmms-theme="light"] .cmms-prio-C,
html[data-cmms-theme="light"] .cmms-wost-Cerrada,
html[data-cmms-theme="light"] .cmms-wost-Pagada,
html[data-cmms-theme="light"] .cmms-wost-Activa { background: #dcfce7; color: #166534; }

/* CELESTE (Información / Por Asignar / En Tránsito — incluye lo que antes
   era violeta: "P-Cierre"/"RG" son informativos, el spec agrupa violeta
   dentro de esta misma familia). "Abierta" pasa acá (antes compartía color
   con "Correctiva"): una OT recién abierta, sin asignar, es más "por
   asignar" que "problema sin resolver" — si preferís que "Abierta" quede en
   rojo en vez de celeste, es un cambio de una sola línea, avisame. */
html[data-cmms-theme="light"] .cmms-prio-B,
html[data-cmms-theme="light"] .cmms-wost-P-Ejecutar,
html[data-cmms-theme="light"] .cmms-wost-Informe-Cargado,
html[data-cmms-theme="light"] .cmms-wost-P-Asignar,
html[data-cmms-theme="light"] .cmms-wost-Abierta,
html[data-cmms-theme="light"] .cmms-ot-RG,
html[data-cmms-theme="light"] .cmms-wost-P-Cierre { background: #e0f2fe; color: #075985; }

/* GRIS NEUTRO — extensión fuera del spec (que solo define 4 familias): "sin
   registro"/anulada/pausada/de baja no son ni error ni éxito ni pendiente,
   son estados inactivos/cancelados. Usa el mismo slate del resto de la
   paleta en vez de inventar un 5to color ajeno. */
html[data-cmms-theme="light"] .cmms-st-Baja,
html[data-cmms-theme="light"] .cmms-wost-SIN-REGISTRO,
html[data-cmms-theme="light"] .cmms-wost-Anulada,
html[data-cmms-theme="light"] .cmms-wost-Pausada { background: #f1f5f9; color: #475569; }

/* Design System — Fase 1: color exacto de texto de tablas/formularios
   (#1E293B) y encabezados de columna TH (#64748B), solo tema Día — no se
   tocan --c-text/--c-muted globales para no afectar el resto de la app
   (menús, KPIs, etc.), que ya estaban bien como estaban. */
html[data-cmms-theme="light"] .cmms-table td,
html[data-cmms-theme="light"] .cmms-field input,
html[data-cmms-theme="light"] .cmms-field select,
html[data-cmms-theme="light"] .cmms-field textarea { color: #1e293b; }
html[data-cmms-theme="light"] .cmms-table th { color: #64748b; }

/* Columna "Acciones" de las tablas — botones "ghost": sin borde ni fondo en
   reposo, aparecen recién al pasar el cursor. Menos ruido visual que un
   borde/fondo gris constante en cada fila. Eliminar usa gris neutro (igual
   que Ver/Editar) y solo se pone rojo al hacer hover, para evitar que un
   color de alerta permanente invite al clic accidental. */
/* Design System — Fase 3: íconos de fila (Ver/Editar) en color discreto,
   pasan a color de marca (--c-accent) en hover en vez de solo oscurecerse.
   "Eliminar" se deja en rojo en hover (ver .cmms-btn-danger:hover abajo) —
   es la única excepción a propósito: rojo = acción destructiva, una
   convención más clara que forzarla a celeste igual que Ver/Editar. */
.cmms-actions .cmms-btn { background: transparent; border-color: transparent; color: var(--c-muted); }
.cmms-actions .cmms-btn:hover { background: var(--c-surface-2); border-color: var(--c-border); color: var(--c-accent); }
.cmms-actions .cmms-btn-danger { color: var(--c-muted); }
.cmms-actions .cmms-btn-danger:hover { background: rgba(239,68,68,.12); border-color: rgba(239,68,68,.4); color: #f87171; }
html[data-cmms-theme="light"] .cmms-actions .cmms-btn-danger:hover { color: #dc2626; }

/* Tarjetas en modo Día (KPIs, tiles del menú, secciones de formulario,
   tarjetas de Roles y Permisos) — blanco puro con borde gris mínimo, en vez
   del degradé pastel (--c-panel-1/--c-panel-2, ya neutralizado arriba pero
   igual se ve más limpio como superficie plana) + la capa "vidrio"
   translúcida (backdrop-filter + color-mix sobre --c-panel-1/2, ver
   .cmms-card, .cmms-menu-tile, ... más abajo) que en modo Día sin fondos
   coloridos detrás no aporta nada y sólo agrega un borde casi invisible
   (rgba blanco). El selector html[data-cmms-theme="light"] ya le gana en
   especificidad a esas reglas "vidrio" (1 clase) sin depender del orden en
   el archivo, así que no hace falta más que esto.
   El contenedor de tabla (Activos, Contratos, OTs, etc.) es un .cmms-card
   más — con esto ya queda blanco puro, que era el pedido original: que se
   distinga del fondo por contraste de superficie, no porque todo comparta
   el mismo tono pastel. */
html[data-cmms-theme="light"] .cmms-card,
html[data-cmms-theme="light"] .cmms-menu-tile,
html[data-cmms-theme="light"] .cmms-kpi,
html[data-cmms-theme="light"] .cmms-form-section,
html[data-cmms-theme="light"] .perm-role-card {
  background: #ffffff;
  border: 1px solid #e2e8f0;
}
/* KPI de alerta (stock bajo, compras pendientes, etc.) conservan su tinte
   semántico ámbar/rojo — apenas perceptible, casi blanco — para seguir
   distinguiéndose de un KPI normal; no se los deja lisos como el resto. */
html[data-cmms-theme="light"] .cmms-kpi.warn   { background: linear-gradient(160deg, #fff8ea, #ffffff); border-color: #f3e2b3; }
html[data-cmms-theme="light"] .cmms-kpi.danger { background: linear-gradient(160deg, #fff1f1, #ffffff); border-color: #f3caca; }

.cmms-app { font-family: var(--font-family); font-size: 14px; color: var(--c-text); background: var(--c-bg); min-height: 100vh; }
/* position+z-index acá (v49): el tema "vidrio" de más abajo le pone
   backdrop-filter al topbar, y backdrop-filter/filter SIEMPRE crea un nuevo
   "contexto de apilamiento" — sin esto, el z-index:1000 del desplegable de
   notificaciones (ver .cmms-notif-panel más abajo) queda "atrapado" adentro
   del topbar y nunca puede pintarse por ENCIMA de .cmms-body (que viene
   después en el HTML), aunque su z-index sea más alto. Con el topbar mismo
   en un z-index alto (por encima de .cmms-sidebar, 900), todo lo que esté
   adentro —incluido el desplegable— queda por sobre el resto de la pantalla. */
.cmms-topbar { position: relative; z-index: 950; background: linear-gradient(90deg, var(--c-topbar-1), var(--c-topbar-2)); color: var(--c-topbar-text); padding: 0 20px; height: 72px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--c-border); transition: background .2s, color .2s; }
.cmms-brand  { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; }
/* v42 — envoltorio del botón ☰ + marca, a la izquierda del topbar (ver
   topbar() en app.js) — permite que el botón hamburguesa (oculto en
   desktop, ver .cmms-menu-toggle-btn más abajo) se agregue sin romper el
   "justify-content:space-between" que separa esto de .cmms-topbar-right. */
.cmms-topbar-left { display: flex; align-items: center; gap: 14px; }
/* Botón ☰ del menú lateral en celular/tablet angosta — oculto por defecto
   (desktop), se muestra solo dentro del @media max-width:760px de abajo. */
.cmms-menu-toggle-btn {
  display: none; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; width: 38px; height: 38px; padding: 0; border-radius: 8px;
  border: 1px solid var(--c-topbar-btn-border); background: var(--c-topbar-btn-bg);
  cursor: pointer; flex-shrink: 0;
}
.cmms-menu-toggle-btn:hover { background: var(--c-topbar-btn-hover-bg); }
.cmms-menu-toggle-bar { display: block; width: 18px; height: 2px; border-radius: 1px; background: var(--c-topbar-btn-text); }
/* Usuario logueado y botones de acción del topbar (🏠 Inicio / 🔄 Actualizar /
   🚪 Salir): antes usaban el mismo tamaño chico (cmms-btn-sm) que los botones
   de acción de las tablas, y se perdían contra el degradé oscuro del topbar.
   El pedido fue el doble de grandes y con mejor contraste — de ahí el fondo
   más claro y el borde más marcado que .cmms-btn normal. */
.cmms-user-pill { display: inline-flex; align-items: center; gap: 8px; background: var(--c-topbar-btn-bg); padding: 10px 18px; border-radius: 24px; font-size: 16px; font-weight: 600; color: var(--c-topbar-btn-text); border: 1px solid var(--c-topbar-btn-border); }
.cmms-body   { display: flex; min-height: calc(100vh - 72px); }
.cmms-sidebar { width: 230px; min-width: 230px; background: var(--c-sidebar-bg); padding: 12px; flex-shrink: 0; border-right: 1px solid var(--c-border); transition: background .2s; display: flex; flex-direction: column; gap: 8px; }
/* v83 — el nav lateral deja de verse como una pila de tarjetas/píldoras
   sueltas (fondo degradé + borde + sombra + "levantarse" al hover en cada
   ítem, una por una) y pasa a ser una barra unificada: fila plana, sin
   fondo ni borde propio, con un relleno de color parejo (mismo look que un
   menú lateral típico) al pasar el mouse o al estar activa, más una franja
   vertical de acento pegada al borde izquierdo que marca la selección
   (pedido del usuario: "dejar de ser píldoras"). */
.cmms-nav-item {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 12px 9px 10px;
  background: none; border: none; border-left: 3px solid transparent; border-radius: 8px;
  color: var(--c-nav-text); cursor: pointer; font-size: 13px; text-align: left;
  transition: background .12s, color .12s, border-color .12s;
}
.cmms-nav-item:hover { background: var(--c-topbar-btn-hover-bg, rgba(148,163,184,.12)); color: var(--c-nav-hover-text); }
.cmms-nav-item:active { background: var(--c-nav-active-bg); }
.cmms-topbar-right { display: flex; align-items: center; gap: 10px; }

/* Campana de notificaciones (v49) — a la izquierda del botón "🏠 Inicio", ver
   notifBellHTML()/notifPanelHTML() en app.js. */
.cmms-notif-wrap { position: relative; }
.cmms-notif-bell { position: relative; font-size: 16px; line-height: 1; }
.cmms-notif-badge {
  position: absolute; top: -4px; right: -4px; min-width: 16px; height: 16px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--c-danger, #dc2626); color: #fff; font-size: 10px; font-weight: 700;
  border-radius: 999px; line-height: 1;
}
.cmms-notif-backdrop { position: fixed; inset: 0; z-index: 999; background: transparent; }
.cmms-notif-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 1000; width: 340px; max-width: 90vw;
  background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border);
  border-radius: var(--radius); box-shadow: var(--c-panel-shadow-hover); overflow: hidden;
}
.cmms-notif-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--c-border); }
.cmms-notif-list { max-height: 420px; overflow-y: auto; }
.cmms-notif-item { padding: 10px 14px; border-bottom: 1px solid var(--c-border); cursor: pointer; transition: background .12s; }
.cmms-notif-item:last-child { border-bottom: none; }
.cmms-notif-item:hover { background: var(--c-topbar-btn-hover-bg, rgba(148,163,184,.12)); }
.cmms-notif-item.is-unread { background: var(--c-accent-soft, rgba(59,130,246,.08)); border-left: 3px solid var(--c-accent); padding-left: 11px; }
.cmms-notif-item-title { font-size: 13px; font-weight: 600; }
.cmms-notif-item-msg { font-size: 12px; color: var(--c-muted); margin-top: 2px; }
.cmms-notif-item-time { font-size: 11px; color: var(--c-muted); margin-top: 4px; }

/* Menú desplegable "Usuario: <nombre>" del topbar (pedido del usuario) —
   agrupa "Salir" y el selector de tema Día/Noche (antes botones sueltos)
   adentro de un menú, mismo patrón visual/mecánico que .cmms-notif-panel
   de arriba (fondo invisible .cmms-notif-backdrop reutilizado para cerrar
   al clickear afuera — ver userMenuHTML()/userMenuPanelHTML() en app.js).
   Pensado para poder sumar más opciones (perfil, preferencias) más
   adelante sin agregar más botones sueltos al topbar. */
.cmms-user-menu-wrap { position: relative; }
/* La píldora pasa de <span> a <button> — reset de las propiedades que el
   navegador le pone por defecto a un <button> (fuente, cursor) ya que
   .cmms-user-pill le fija el resto (fondo/borde/color) sin problema. */
.cmms-user-pill-btn { cursor: pointer; font-family: inherit; }
.cmms-user-menu-caret { font-size: 11px; opacity: .7; }
.cmms-user-menu-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 1000; width: 280px; max-width: 90vw;
  background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border);
  border-radius: var(--radius); box-shadow: var(--c-panel-shadow-hover); overflow: hidden; padding: 6px;
}
.cmms-user-menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 10px;
  border-radius: 8px; color: var(--c-text); font-size: 13px; font-weight: 500;
}
.cmms-user-menu-item-icon { font-size: 15px; line-height: 1; flex-shrink: 0; }
.cmms-user-menu-item-label { flex: 1; text-align: left; }
/* Ítem que ES un botón — reset de estilos default de <button> + hover. */
.cmms-user-menu-item-btn { background: none; border: none; cursor: pointer; text-align: left; transition: background .12s; }
.cmms-user-menu-item-btn:hover { background: var(--c-topbar-btn-hover-bg, rgba(148,163,184,.12)); }
.cmms-user-menu-item-danger:hover { background: color-mix(in srgb, var(--c-danger, #dc2626) 14%, transparent); color: var(--c-danger, #dc2626); }
.cmms-user-menu-sep { height: 1px; background: var(--c-border); margin: 4px 6px; }
.cmms-nav-item.active { background: var(--c-nav-active-bg); color: var(--c-nav-hover-text); border-left-color: var(--c-accent); }
/* v84 — sin chip de color de fondo (pedido del usuario): ícono monocromo,
   currentColor, con su propio tono fijo y oscuro (--c-nav-icon) para
   asegurar contraste contra el fondo del sidebar más allá del color de
   texto normal/hover/activo del ítem. */
.cmms-nav-item-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--c-nav-icon, currentColor); }
.cmms-nav-item-label { font-weight: 600; flex: 1; }
.cmms-nav-item-arrow { display: none; }
/* Grupos del menú lateral ("Operación" / "Sistema", pedido del usuario) —
   antes todos los ítems del NAV iban en una sola lista plana; ahora se
   separan en 2 bloques con un rótulo chico en mayúsculas encima de cada
   uno (ver sidebar() en app.js). */
.cmms-nav-group { display: flex; flex-direction: column; gap: 2px; }
.cmms-nav-group + .cmms-nav-group { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--c-border); }
.cmms-nav-group-label { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--c-muted); padding: 0 6px; margin-bottom: 2px; }
.cmms-main   { flex: 1; padding: 22px 26px; overflow-x: auto; min-width: 0; }
.cmms-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
/* Design System — Fase 4 (estética, sin agregar botones): agrupa las
   acciones de la derecha del header en un solo contenedor, para que
   queden pegadas entre sí (en vez de repartidas por space-between si el
   header tiene más de 2 hijos) y el primario quede siempre al extremo
   derecho — es el ÚLTIMO botón dentro de .cmms-header-actions en el
   markup. El borde derecho de este contenedor ya coincide con el de la
   tarjeta/tabla de abajo porque ambos comparten el mismo padre y padding;
   no hizo falta tocar el ancho de nada. */
.cmms-header-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Acciones secundarias tipo "outline/ghost": fondo blanco/neutro con borde
   fino gris (antes .cmms-btn traía un fondo gris claro sólido, --c-surface-2,
   igual en toda la app — acá se aclara SOLO dentro del grupo de acciones
   del header, sin tocar el .cmms-btn genérico que se usa en modales,
   formularios, etc.). El botón primario (.cmms-btn-primary) no se toca. */
.cmms-header-actions .cmms-btn:not(.cmms-btn-primary) { background: var(--c-surface); }
.cmms-header-actions .cmms-btn:not(.cmms-btn-primary):hover { background: var(--c-surface-2); }
.cmms-page-header h1 { font-size: 21px; font-weight: 700; margin: 0; color: var(--c-strong-text); }

/* Recuadros "OT Correctiva"/"OT Preventiva" del header de "Órdenes de
   trabajo" (ver woListView() en app.js) — reemplazan al botón único
   "+ Nueva OT" + popup: dejan explícito, de entrada, que hay dos tipos de
   OT y sirven de acceso directo a cada uno (crear correctiva / ir al
   listado de Planes de Mantenimiento Preventivo). */
.cmms-wo-header-cards { display: flex; gap: 10px; flex-wrap: wrap; }
/* v29: antes eran casi transparentes (fondo blanco al 3% de opacidad, borde
   tenue) — se veían muy poco contra el fondo de la pantalla, sobre todo en
   el tema oscuro. Ahora son botones "sólidos" (mismo criterio que
   .cmms-btn-primary de abajo): fondo del color de acento del tema + texto
   blanco, así se distinguen claramente sin importar qué tema esté activo
   (oscuro/intermedio/claro, ver los tokens --c-accent en :root arriba). */
.cmms-wo-header-card {
  display: flex; align-items: center; gap: 8px; padding: 9px 14px; border-radius: var(--radius);
  border: 1px solid var(--c-accent); background: var(--c-accent);
  color: #fff; cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 700;
  box-shadow: 0 2px 6px -2px rgba(0,0,0,.35);
  transition: background .12s, border-color .12s;
}
.cmms-wo-header-card:hover { background: var(--c-accent-h); border-color: var(--c-accent-h); }
/* "+ RG" (07-ago-2026) — placeholder deshabilitado a la izquierda de "+ OT
   Correctiva" (ver woListView() en app.js): mismo criterio de atenuado que
   ya usan los ítems "Próximamente" del menú desplegable
   (.cmms-wo-header-dropdown-item[disabled] más abajo), aplicado acá al
   propio botón porque este placeholder no tiene menú que desplegar. */
.cmms-wo-header-card[disabled] { opacity: .55; cursor: not-allowed; }
.cmms-wo-header-card[disabled]:hover { background: var(--c-accent); border-color: var(--c-accent); }
.cmms-wo-header-card-icon { font-size: 16px; }
/* 06-ago-2026 (pedido del usuario) — "+ OT Correctiva"/"+ OT Preventiva"
   pasan de ser accesos directos a botones tipo DROPDOWN: cada uno despliega
   un menú con 2 opciones (elegir la naturaleza de la OT ANTES de entrar al
   formulario, en vez de adentro — ver woFormView(), donde el paso "Tipo de
   OT correctiva" se sacó). Mismo patrón que el menú "Usuario: <nombre>" del
   topbar (.cmms-user-menu-*, ver userMenuHTML() en app.js): wrapper
   position:relative + panel absoluto + fondo invisible de página completa
   (.cmms-notif-backdrop, reutilizado) para cerrar al clickear afuera. Se
   reutilizan las clases .cmms-user-menu-panel/-item/-item-btn tal cual para
   que la tipografía/alturas/colores de los ítems queden IDÉNTICOS al resto
   del sistema (pedido explícito) — acá solo se ajusta la POSICIÓN del panel
   (pegado a la izquierda del botón, no a la derecha como el de usuario) y
   se agrega la carita ▾/▴ al botón disparador, mismo criterio que
   .cmms-user-menu-caret. */
.cmms-wo-header-dropdown-wrap { position: relative; }
.cmms-wo-header-card-btn { font-family: inherit; }
.cmms-wo-header-card-caret { font-size: 10px; opacity: .85; margin-left: 2px; }
.cmms-wo-header-dropdown-panel {
  /* v2 (pedido del usuario) — sin texto de ayuda bajo cada ítem, las
     etiquetas quedan cortas ("OT CATALOGO", "Inspección", "Nuevo PMP"): el
     panel se angosta de 300px a 220px (era demasiado ancho para el
     contenido y en el botón "+ OT Preventiva", pegado al lado derecho de
     "+ OT Correctiva", se salía del viewport — ver captura del usuario). */
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 1000; width: 220px; max-width: 90vw;
  background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border);
  border-radius: var(--radius); box-shadow: var(--c-panel-shadow-hover); overflow: hidden; padding: 6px;
}
.cmms-wo-header-dropdown-panel--right { left: auto; right: 0; }
.cmms-wo-header-dropdown-item { align-items: flex-start; }
.cmms-wo-header-dropdown-item-icon { margin-top: 1px; }
.cmms-wo-header-dropdown-item-desc { display: block; font-size: 11px; font-weight: 400; color: var(--c-muted); margin-top: 2px; }
.cmms-wo-header-dropdown-item[disabled] { opacity: .55; cursor: not-allowed; }
.cmms-wo-header-dropdown-item[disabled]:hover { background: none; }
@media (max-width: 640px) {
  .cmms-page-header { flex-wrap: wrap; gap: 10px; }
  .cmms-wo-header-dropdown-panel { left: auto; right: 0; }
}

/* Dropdown GENÉRICO de "más acciones" por fila de tabla (ícono "⋮", ver
   rowActionsMenuHTML()/rowMenuItem() en app.js, 11-ago-2026, pedido del
   usuario — primer módulo: Inventario, columna "Acciones") — mismo patrón
   visual que .cmms-wo-header-dropdown-*/.cmms-user-menu-* de arriba (fondo
   invisible .cmms-notif-backdrop para cerrar al clickear afuera, reutilizado
   sin CSS propio) PERO con una diferencia clave: el panel usa
   `position: fixed` (no absolute) con top/left calculados por JS al abrir
   (ver el bloque "posicionar el panel..." en bindEvents(), app.js) en vez de
   depender del wrapper. Es a propósito: casi TODAS las tablas del sistema
   viven dentro de un `<div class="cmms-card" style="padding:0;overflow:
   hidden">` (recorte usado para que las esquinas rectas de la tabla no se
   salgan de las esquinas redondeadas de la tarjeta) — con position:absolute
   ese overflow:hidden recortaba el panel y lo volvía invisible aunque sí se
   abría (bug reportado por el usuario, 11-ago-2026). position:fixed escapa
   de CUALQUIER overflow:hidden ancestro sin tener que tocar esas 13+
   tarjetas de tabla una por una — así el dropdown queda a prueba de este
   problema en cualquier módulo nuevo que lo use, sin nada especial que
   recordar al reusarlo. margin-left:auto empuja el botón "⋮" al extremo
   derecho de la columna "Acciones" (.cmms-actions es flex), separado del
   resto de botones de la fila — tal como pidió el usuario. Los ÍTEMS de
   adentro del panel reusan .cmms-user-menu-item/-item-btn/-item-icon/
   -item-label tal cual (mismo criterio que .cmms-wo-header-dropdown-item),
   así que este bloque NO necesita tocarse para agregar el dropdown a otro
   módulo: solo cambia qué itemsHtml arma cada uno con rowMenuItem(). */
.cmms-row-menu-wrap { position: relative; margin-left: auto; flex-shrink: 0; }
.cmms-row-menu-panel {
  position: fixed; z-index: 1000; width: 170px; max-width: 90vw;
  background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border);
  border-radius: var(--radius); box-shadow: var(--c-panel-shadow-hover); overflow: hidden; padding: 6px;
  /* BUGFIX (20-ago-2026, pedido del usuario — mismo motivo ya corregido en
     .cmms-col-hint-panel, ver comentario grande ahí): este panel también lo
     agrega render() como HERMANO de .cmms-app, fuera del único selector que
     fija `font-family: var(--font-family)` — sin esto, los ítems del menú
     "⋮" (.cmms-user-menu-item, que no fija font-family propio, hereda de
     acá) caían al font stack por defecto del navegador en vez de la
     tipografía del sistema. */
  font-family: var(--font-family);
  /* Posición real la fija JS al abrir (ver bindEvents()) — top/left acá son
     solo un valor de arranque razonable por si algo falla al medir. */
  top: -9999px; left: -9999px;
}

/* Menú inicial del Dashboard: tarjetas grandes de acceso directo a cada
   módulo del menú lateral (ver dashboardMenuGridHTML() en app.js) — mismo
   ícono a color + flecha que el mockup de referencia que pidió el cliente. */
/* Hint (i) de encabezado de columna (20-ago-2026, pedido del usuario —
   colHintIconHTML()/colHintPortalHTML() en app.js), primer uso: columnas
   "Próxima mantención"/"Gen. Aut. OT y Notificación" del listado de Planes
   PM. Mismo criterio que .cmms-row-menu-panel de arriba (position:fixed
   calculado por JS al abrir, para escapar del overflow:hidden de
   .cmms-card) — no reusa ese mismo panel porque este lleva texto libre
   (párrafo), no una lista de botones. */
.cmms-col-hint-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; margin-left: 4px; padding: 0;
  border: none; background: none; color: var(--c-muted); cursor: pointer;
  border-radius: 50%; vertical-align: -3px; transition: background .12s, color .12s;
}
.cmms-col-hint-btn:hover, .cmms-col-hint-btn[aria-expanded="true"] { background: var(--c-topbar-btn-hover-bg, rgba(148,163,184,.12)); color: var(--c-text); }
.cmms-col-hint-panel {
  position: fixed; z-index: 1000; width: 280px; max-width: 90vw;
  background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border);
  border-radius: var(--radius); box-shadow: var(--c-panel-shadow-hover); padding: 12px 14px;
  /* BUGFIX (20-ago-2026, reportado por el usuario): este panel lo agrega
     render() como HERMANO de .cmms-app, no adentro (ver colHintPortalHTML()
     en app.js y el porqué junto a rowMenuPortalHTML()) — .cmms-app es la
     única regla que fija `font-family: var(--font-family)` (Inter), así que
     sin esta línea el panel caía al font stack por default del navegador
     en vez de la tipografía del sistema. Mismo motivo de fondo aplicaría a
     .cmms-row-menu-panel (el "⋮"), que tiene el mismo problema latente. */
  font-family: var(--font-family);
  font-size: 13px; line-height: 1.45; color: var(--c-text); font-weight: 400;
  top: -9999px; left: -9999px;
}

/* Orden de tabla clickeable en encabezado (20-ago-2026, pedido del usuario
   — sortColHeaderHTML()/applyColSort() en app.js, primer uso: "Planes PM").
   .cmms-col-header-flex envuelve el botón de orden + (si corresponde) el
   ícono (i) de colHintIconHTML() en la misma línea, uno pegado al otro. */
.cmms-col-header-flex { display: inline-flex; align-items: center; }
.cmms-col-sort-btn {
  display: inline-flex; align-items: center; gap: 3px; padding: 0;
  border: none; background: none; cursor: pointer; font: inherit;
  font-size: 11px; font-weight: 600; color: var(--c-muted); text-transform: uppercase;
  letter-spacing: .4px; white-space: nowrap; transition: color .12s;
}
.cmms-col-sort-btn:hover { color: var(--c-text); }
/* BUGFIX/pedido del usuario (20-ago-2026): flecha más notoria. Se pidió
   "negro" textual, pero un negro fijo (#000) se volvería casi invisible en
   Tema Noche (fondo oscuro, texto claro — ver --c-text más arriba: #ccd4e3
   en oscuro vs #16233c en claro) — se usa var(--c-text) en su lugar (negro
   de verdad en Tema Día, blanco/gris muy claro en Tema Noche) más opacidad
   al 100% siempre (antes .35 en reposo), para que se note en ambos temas
   sin quedar "apagada" como el resto del encabezado. */
.cmms-col-sort-arrow { display: inline-flex; color: var(--c-text); opacity: 1; transition: opacity .12s, transform .12s; }
.cmms-col-sort-arrow--active { opacity: 1; }
.cmms-col-sort-btn:hover .cmms-col-sort-arrow { opacity: .9; }

.cmms-menu-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(260px,1fr)); gap: 16px; margin-bottom: 22px; }
.cmms-menu-tile { display: flex; align-items: center; gap: 16px; background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 20px; cursor: pointer; box-shadow: var(--c-panel-shadow); transition: transform .15s, box-shadow .15s, border-color .15s, background .2s; }
.cmms-menu-tile:hover { transform: translateY(-3px); box-shadow: var(--c-panel-shadow-hover); border-color: var(--c-accent); }
.cmms-menu-tile:active { transform: translateY(-1px); }
.cmms-menu-tile-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--c-text); }
.cmms-menu-tile-title { font-size: 16px; font-weight: 700; color: var(--c-text); line-height: 1.3; flex: 1; }
.cmms-menu-tile-arrow { font-size: 22px; color: var(--c-muted); flex-shrink: 0; }

/* KPIs */
.cmms-kpi-grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(170px,1fr)); gap: 14px; margin-bottom: 20px; }
.cmms-kpi { position: relative; background: linear-gradient(160deg, var(--c-kpi-1), var(--c-kpi-2)); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 16px; overflow: hidden; box-shadow: var(--c-panel-shadow); }
.cmms-kpi.warn   { border-color: rgba(245,158,11,.4); background: linear-gradient(160deg, var(--c-kpi-warn-1), var(--c-kpi-warn-2) 55%); }
.cmms-kpi.danger { border-color: rgba(239,68,68,.45); background: linear-gradient(160deg, var(--c-kpi-danger-1), var(--c-kpi-danger-2) 55%); border-left: 3px solid var(--c-danger); }
.cmms-kpi-label  { font-size: 12px; color: var(--c-muted); margin-bottom: 10px; padding-right: 30px; }
.cmms-kpi-value  { font-size: 30px; font-weight: 700; color: var(--c-text); }
.cmms-kpi-icon   { position: absolute; top: 14px; right: 14px; width: 26px; height: 26px; border-radius: 7px; background: var(--c-kpi-icon-bg); box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 2px 4px rgba(0,0,0,.3); display: flex; align-items: center; justify-content: center; font-size: 13px; }
.cmms-kpi.danger .cmms-kpi-icon { background: rgba(239,68,68,.18); }
.cmms-kpi.warn .cmms-kpi-icon   { background: rgba(245,158,11,.18); }
.cmms-kpi-clickable { cursor: pointer; transition: transform .15s, box-shadow .15s, border-color .15s; }
.cmms-kpi-clickable:hover { transform: translateY(-3px); box-shadow: var(--c-panel-shadow-hover); border-color: var(--c-accent); }
.cmms-kpi-clickable:active { transform: translateY(-1px); }

/* Cards */
.cmms-card { background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 16px; box-shadow: var(--c-panel-shadow); }
/* v91 — pedido del usuario: "un poco más de contraste, un 50% más" en los
   títulos de tarjeta ("ESTADO DE LOS ACTIVOS", "OTS POR MES", etc, antes en
   var(--c-muted), pensado para texto secundario/discreto). color-mix() los
   deja a mitad de camino entre --c-muted y --c-text (el texto normal, de
   contraste completo) — mismo criterio en los 2 temas, sin tener que
   inventar un tono a mano por tema. */
/* Design System — Fase 1: subtítulos/encabezados de tarjeta 13px (antes
   12px), letter-spacing .05em (≈.65px a este tamaño, ya estaba en .6px).
   El color exacto #475569 se fija abajo solo para tema Día. */
.cmms-card h3 { font-size: 13px; font-weight: 600; color: color-mix(in srgb, var(--c-muted) 50%, var(--c-text) 50%); margin: 0 0 14px; text-transform: uppercase; letter-spacing: .05em; }
html[data-cmms-theme="light"] .cmms-card h3 { color: #475569; }

/* v52 — assetDetailView(): grilla de tarjetas con ícono (a pedido del
   usuario, replicando el estilo de referencia que trajo — cada bloque de la
   ficha del activo pasa de párrafos apilados "Label: valor" a una tarjeta
   con ícono + título + pares label/valor en grilla de 2 columnas). Reusa
   .cmms-card/--c-panel-N tal cual (mismo look que el resto de la app, no es
   un tema nuevo) — solo agrega el header con ícono y el formato de campo. */
/* v54 — grilla fija de 2 columnas (antes auto-fit, que en pantallas anchas
   reflowaba a 3-5 columnas por fila — ver comentario del usuario "la
   visualización dentro del software debe tener el mismo orden con el cual
   se imprimirá"): ahora pantalla e impresión usan EXACTAMENTE la misma
   grilla de 2 columnas, mismo orden de lectura (izquierda→derecha,
   arriba→abajo) en los dos casos. Se limita el ancho máximo (--asset-grid-
   max) para que las tarjetas no queden estiradas de más en monitores
   anchos — el layout de referencia que trajo el usuario tampoco ocupa todo
   el ancho de una pantalla grande. */
/* v55 — SIN align-items:start (a pedido del usuario: "ordena los 6 bloques
   para que se vean con sus límites a la misma altura" — ej. "Notas y
   Comentarios" debe igualar la altura de "Fabricación y Vida Útil", su
   pareja en la 3ª fila). Con grid normal (stretch, el default) cada
   tarjeta ya se estira sola hasta la altura de la más alta de su misma
   fila — no hace falta fijar un alto en px, que se rompería apenas cambie
   el contenido de un activo a otro. */
/* v86 (pedido del usuario, "ganemos espacio") — gap entre tarjetas 16px→12px
   y padding interno de cada tarjeta 16px→12px (ver
   .cmms-asset-detail-grid .cmms-card más abajo, que pisa el padding:16px
   genérico de .cmms-card SOLO dentro de esta grilla — el resto de la app
   sigue usando .cmms-card normal, sin achicar). */
/* v88 (pedido del usuario) — separación entre tarjetas 12px→7px (misma
   separación mínima que se dejó en la ficha de Proveedor, contractDetailView
   en public/app.js), "solo para que no se vean pegadas" pero sin volver al
   12/16px de antes. */
.cmms-asset-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-bottom: 7px; max-width: 980px; }
.cmms-asset-detail-grid .cmms-card { display: flex; flex-direction: column; padding: 12px; }
.cmms-asset-crit-line { display: flex; justify-content: flex-end; gap: 20px; font-size: 13px; color: var(--c-muted); margin-bottom: 10px; max-width: 980px; }
.cmms-asset-crit-line strong { color: var(--c-text); font-weight: 600; }
.cmms-detail-card-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
/* v58 — se reemplazaron los emoji (📍⚙️⏱️🏗️🏭📋🔗🕘) por numeración 1-8,
   a pedido del usuario — ver detailCard() y las tarjetas "Activos hijos
   vinculados"/"Historial de OT" en assetDetailView(). Círculo con el
   número en vez de la caja cuadrada pensada para un glifo de emoji. */
.cmms-detail-card-icon {
  flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center;
  justify-content: center; font-size: 14px; font-weight: 700; background: var(--c-kpi-icon-bg);
  color: var(--c-accent); border: 1px solid var(--c-border);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 2px 4px rgba(0,0,0,.25);
}
.cmms-detail-card-head h3 { margin: 0 0 2px; }
.cmms-detail-card-eyebrow { font-size: 11px; color: var(--c-muted); text-transform: uppercase; letter-spacing: .5px; }
.cmms-detail-card-title { font-size: 15px; font-weight: 700; color: var(--c-text); margin-top: 1px; }
/* fija (no auto-fit) — mismo motivo que arriba: 2 columnas parejas siempre,
   para que un valor largo en un campo no empuje el ancho de columna y
   desalinee todo lo demás entre pantalla e impresión. */
.cmms-detail-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.cmms-detail-field { min-width: 0; }
.cmms-detail-field label { display: block; font-size: 11px; color: var(--c-muted); text-transform: uppercase; letter-spacing: .4px; margin-bottom: 3px; }
.cmms-detail-value { font-size: 14px; color: var(--c-text); overflow-wrap: anywhere; }
.cmms-detail-sep { border: none; border-top: 1px solid var(--c-border); margin: 14px 0; }
.cmms-detail-card--wide { grid-column: 1 / -1; }

/* v86 (03-ago-2026, pedido del usuario) — foto del activo en el ENCABEZADO
   de la ficha (assetDetailView), no dentro de una tarjeta (ver historial de
   este archivo: el primer intento la puso dentro de la tarjeta 2 y "se veía
   desordenado"). Fila con el TAG/nombre + Criticidad/Estado apilados a la
   izquierda (columna angosta, ver .cmms-asset-detail-header-text) y la foto
   a la derecha, fuera de la grilla de tarjetas de más abajo. */
.cmms-asset-detail-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; max-width: 980px; margin-bottom: 14px; }
.cmms-asset-detail-header-text { min-width: 0; }
.cmms-asset-detail-header-text h1 { font-size: 21px; font-weight: 600; margin: 0; color: var(--c-text); }
/* Bajo el título en vez de a la derecha del todo (a diferencia del
   .cmms-asset-crit-line "clásico" que sigue usando woDetailView/
   preventiveGeneratedDetailView, ver ese selector arriba) — por eso pisa
   justify-content/margin/max-width solo acá adentro. */
.cmms-asset-detail-header-text .cmms-asset-crit-line { justify-content: flex-start; margin: 6px 0 0; max-width: none; }
.cmms-asset-detail-header-right { flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
.cmms-asset-detail-header-actions { display: flex; align-items: center; gap: 10px; }
/* v87 (03-ago-2026, pedido del usuario) — foto rectangular horizontal
   (220×130) en vez del cuadrado de 120×120 del intento anterior: la foto
   real de un activo suele venir apaisada (equipo fotografiado de lado, ej.
   un grupo electrógeno) y en un cuadrado quedaba muy recortada por los
   costados (object-fit:cover). Ya no comparte ancho con ninguna grilla de
   campos (vive fuera de las tarjetas), así que puede ser más ancha sin
   apretar nada más. */
.cmms-asset-photo-detail-box { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 220px; }
.cmms-asset-photo-detail-img { width: 220px; height: 130px; object-fit: cover; border-radius: 8px; border: 1px solid var(--c-border); }
.cmms-asset-photo-detail-placeholder {
  width: 220px; height: 130px; border-radius: 8px; border: 1px dashed var(--c-border);
  display: flex; align-items: center; justify-content: center; color: var(--c-muted); font-size: 30px;
}
.cmms-asset-photo-detail-tag {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; font-weight: 700;
  color: var(--c-text); background: var(--c-surface-2); border: 1px solid var(--c-border);
  border-radius: 5px; padding: 2px 7px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* v87 (pedido del usuario) — número/título de tarjeta y etiquetas de campo
   más chicos, EN TODA la ficha del activo (las 6 tarjetas de la grilla más
   "Activos hijos vinculados"/"Historial de OT" más abajo — por eso el
   scope es .cmms-asset-detail-page, el envoltorio nuevo de assetDetailView,
   y no .cmms-asset-detail-grid, que no cubre esas dos últimas tarjetas).
   Scopeado a esta página nomás: .cmms-detail-card-icon/-title y
   .cmms-detail-field label son las MISMAS clases que usa woDetailView()/
   preventiveGeneratedDetailView() para la ficha de OT (ver comentario junto
   a field()/longField() en woDetailView) — sin este scope, esto también
   habría achicado la ficha de OT, que no se pidió tocar.
   v88 (pedido del usuario) — el título (12px) quedó viéndose MÁS CHICO que
   la etiqueta de campo (10px), aunque en número sea más grande: la etiqueta
   va en mayúsculas (cap-height = casi el 100% del font-size), el título va
   en minúscula/mayúscula mixta (cap-height ≈ 70% del font-size) — a
   font-size igual, el texto en mayúsculas siempre "pesa" más a la vista.
   Se sube el título a 14px (en vez de igualar los 10px de la etiqueta, que
   lo dejaría ilegible) para que el cap-height de ambos quede parejo. */
.cmms-asset-detail-page .cmms-detail-card-icon { width: 26px; height: 26px; font-size: 12px; }
.cmms-asset-detail-page .cmms-detail-card-title { font-size: 14px; }
.cmms-asset-detail-page .cmms-detail-card-eyebrow { font-size: 10px; }
.cmms-asset-detail-page .cmms-detail-card-head { gap: 9px; margin-bottom: 9px; }
.cmms-asset-detail-page .cmms-detail-field label { font-size: 10px; }

@media (max-width: 720px) {
  .cmms-asset-detail-grid { grid-template-columns: 1fr; max-width: none; }
  .cmms-detail-fields { grid-template-columns: 1fr; }
  /* En pantallas angostas el encabezado pasa a apilarse (texto arriba,
     foto abajo centrada) en vez de foto+texto lado a lado — mismo criterio
     que el resto de esta media query (todo a una columna). */
  .cmms-asset-detail-header { flex-direction: column; align-items: center; text-align: center; }
  .cmms-asset-detail-header-text { text-align: center; }
  .cmms-asset-detail-header-text .cmms-asset-crit-line { justify-content: center; }
  .cmms-asset-detail-header-right { align-items: center; }
  /* v89 (pedido del usuario) — resumen de OT + foto (woDetailView): mismo
     criterio, se apila en vez de lado a lado en pantallas angostas. */
  .cmms-wo-summary-row { flex-direction: column; align-items: center; }
}

/* v53 — "Imprimir" de assetDetailView(): genera el PDF con Ctrl+P / el
   diálogo de impresión del navegador (no arma el PDF nosotros — dejamos
   que sea el usuario quien elija "Guardar como PDF" en ese diálogo, mismo
   patrón nativo que ya usan managementReportPdf.js/etc. para lo que sí se
   arma en el servidor). Todo va detrás de html.cmms-printing (agregada por
   el botón antes de window.print(), ver bindEvents) — así el print de esta
   ficha no le cambia el aspecto a un Ctrl+P hecho desde cualquier otra
   pantalla de la app. Colores fijos en hex (no var(--c-*)) a propósito: el
   pedido fue "sin colores de fondo" e independiente del tema elegido — si
   usáramos las variables de tema, el PDF saldría distinto según el usuario
   haya elegido Día o Noche al imprimir, que es justo lo que no se quiere.
   v54 — se sacó el azul bebé de los títulos (a pedido del usuario, quedaba
   muy poco legible en la vista previa/PDF): ahora todos los títulos
   imprimen en el mismo gris oscuro neutro que el resto del texto, solo
   diferenciados por tamaño/negrita — sin ningún color de acento. La grilla
   de 2 columnas ya es la base (ver .cmms-asset-detail-grid arriba, ahora
   fija tanto en pantalla como en impresión) — acá solo se ajustan los gaps/
   márgenes al ancho de una hoja carta. */
/* v80 (31-jul-2026) — opuesto de .cmms-no-print: oculto en pantalla por
   defecto, se muestra solo bajo html.cmms-printing (ver regla dentro de
   @media print más abajo). Reemplaza visualmente, en el PDF, a un control
   interactivo que se oculta en impresión con .cmms-no-print. */
.cmms-print-only { display: none; }

@media print {
  html.cmms-printing { background: #fff; }
  html.cmms-printing body,
  html.cmms-printing .cmms-app,
  html.cmms-printing .cmms-main { background: #fff !important; color: #000 !important; }
  /* v67 — "las letras no se ven claramente, están arrastrando el formato
     original" (a pedido del usuario, reportado en la OT Preventiva): varios
     textos de esa pantalla tienen su color puesto INLINE (style="color:var(
     --c-muted)", ej. la línea de taxonomía bajo "1. Equipo", los "Notificada
     el…", etc.) — un estilo inline sin !important normalmente le gana a
     cualquier regla de una hoja de estilos externa, así que las reglas de
     arriba (que solo tocan .cmms-card/.cmms-detail-value/etc.) no alcanzaban
     a esos casos sueltos. Esta regla con `*` + !important es la única forma
     de garantizar TODO el texto en negro sin perseguir cada estilo inline
     uno por uno — !important SÍ le gana a un inline sin !important, sin
     importar la especificidad del selector. */
  html.cmms-printing * { color: #000 !important; }
  /* v70 — causa real de por qué algunos colores (fondo verde/gris/rojo de
     los chips OK/NA/NOK) se seguían viendo en el PDF a pesar de las reglas
     de arriba: el botón "Imprimir" agrega la clase html.cmms-printing y
     llama a window.print() en el mismo tick — como .cmms-chip-check tiene
     `transition: background .12s, border-color .12s, color .12s` (para el
     hover en pantalla), Chrome arranca una TRANSICIÓN de pantalla→impresión
     justo cuando genera el PDF y a veces alcanza a capturar un frame a
     mitad de camino (fondo de color aún visible). Se anulan todas las
     transiciones/animaciones dentro de la impresión para que el PDF
     siempre capture el estado FINAL ya resuelto (negro/blanco), nunca uno
     intermedio. */
  html.cmms-printing, html.cmms-printing * { transition: none !important; animation: none !important; }
  html.cmms-printing .cmms-topbar,
  html.cmms-printing .cmms-sidebar,
  html.cmms-printing .cmms-nav-backdrop,
  html.cmms-printing .cmms-no-print,
  html.cmms-printing .cmms-actions { display: none !important; }
  /* v80 (31-jul-2026) — opuesto de .cmms-no-print: contenido que solo debe
     verse en el PDF/impresión (ver .cmms-print-only, oculta en pantalla por
     defecto más abajo, fuera de @media print), para reemplazar visualmente
     a un control interactivo que sí se oculta con .cmms-no-print — ej. la
     "Fecha de ejecución" de una OT Preventiva: en pantalla es un <input>
     editable, en el PDF debe verse como texto plano (ver
     preventiveGeneratedDetailView() en public/app.js). */
  html.cmms-printing .cmms-print-only { display: block !important; }
  html.cmms-printing .cmms-body { display: block !important; }
  html.cmms-printing .cmms-main { max-width: none !important; margin: 0 !important; padding: 0 !important; }

  @page { size: letter; margin: 14mm 12mm; }

  /* grilla de 2 columnas ajustada al ancho de una hoja carta (la base ya es
     2 columnas fija — ver .cmms-asset-detail-grid — acá solo se acortan
     gaps/max-width para aprovechar el ancho impreso al máximo) */
  html.cmms-printing .cmms-asset-detail-grid { grid-template-columns: 1fr 1fr; gap: 10px 18px; margin-bottom: 10px; max-width: none; }
  html.cmms-printing .cmms-detail-fields { grid-template-columns: 1fr 1fr; gap: 8px 14px; }
  html.cmms-printing .cmms-asset-crit-line { max-width: none; color: #000 !important; }
  html.cmms-printing .cmms-asset-crit-line strong { color: #000 !important; }

  /* v87 (pedido del usuario) — foto del activo rectangular (190×112,
     mismo criterio 220×130 de pantalla, achicado para el ancho impreso —
     ver .cmms-asset-detail-header más arriba). Ahora en el ENCABEZADO, no
     adentro de una tarjeta, así que no compite por ancho con ninguna
     grilla de campos. Se mantiene fácilmente visible: sigue siendo la
     única superficie a color de toda la hoja (el resto queda en
     blanco/negro, ver `html.cmms-printing * { color:#000 }` más abajo, que
     solo pisa color de TEXTO — no toca la imagen). */
  html.cmms-printing .cmms-asset-detail-header { max-width: none; margin-bottom: 8px; }
  html.cmms-printing .cmms-asset-detail-header-text .cmms-asset-crit-line { margin-top: 4px; }
  html.cmms-printing .cmms-asset-photo-detail-box { width: 190px; gap: 4px; }
  html.cmms-printing .cmms-asset-photo-detail-img,
  html.cmms-printing .cmms-asset-photo-detail-placeholder { width: 190px; height: 112px; }
  html.cmms-printing .cmms-asset-photo-detail-tag { font-size: 10px !important; max-width: 190px; }

  /* v87 (pedido del usuario) — número/título/subtítulo más chicos en
     impresión, mismo scope .cmms-asset-detail-page que en pantalla (ver
     más arriba) para no tocar la ficha de OT impresa, que reusa las mismas
     clases. La etiqueta de campo NO baja tanto como el resto (11px en vez
     de calcar el 10px de pantalla): a la resolución con la que Chrome arma
     el PDF, texto en mayúsculas por debajo de ~10-10.5px se ve borroso —
     mismo motivo que ya había forzado agrandarlas una vez antes (ver v56
     más abajo); acá se prioriza la petición de "que quede bien legible al
     imprimir" por sobre igualar el tamaño exacto de pantalla. */
  /* v88 — mismo ajuste de cap-height que en pantalla (ver comentario junto
     a .cmms-asset-detail-page .cmms-detail-card-title más arriba): el
     título sube de 11px a 13px para no verse más chico que la etiqueta
     (11px, en mayúsculas) de al lado. */
  html.cmms-printing .cmms-asset-detail-page .cmms-detail-card-icon { width: 21px !important; height: 21px !important; font-size: 10.5px !important; }
  html.cmms-printing .cmms-asset-detail-page .cmms-detail-card-title { font-size: 13px !important; }
  html.cmms-printing .cmms-asset-detail-page .cmms-detail-card-eyebrow { font-size: 10px !important; }
  html.cmms-printing .cmms-asset-detail-page .cmms-detail-card-head { gap: 8px !important; margin-bottom: 8px !important; }
  html.cmms-printing .cmms-asset-detail-page .cmms-detail-field label { font-size: 11px !important; letter-spacing: .15px !important; }

  /* tarjetas sin degradé/sombra — solo un borde fino gris; cada una evita
     partirse entre dos hojas (las tarjetas de campos son cortas, la de
     Historial/Activos hijos puede ser larga y se deja fluir libremente,
     ver .cmms-asset-print-flow más abajo) */
  html.cmms-printing .cmms-card { background: #fff !important; box-shadow: none !important; border: 1px solid #c7d0dd !important; break-inside: avoid; page-break-inside: avoid; }
  html.cmms-printing .cmms-asset-print-flow { break-inside: auto; page-break-inside: auto; }
  /* v89 (pedido del usuario) — resumen de OT (badges + TAG/días/creada/
     cierre) + foto grande, ver woDetailView(): que no queden separados
     entre dos hojas. */
  html.cmms-printing .cmms-wo-summary-row { break-inside: avoid; page-break-inside: avoid; }
  /* v2 (17-ago-2026, pedido del usuario: "tiene una tabla cortada" — en la
     tarjeta "4. Informe de inspección"/"4. Informe de ejecución", ver
     .cmms-asset-print-flow más abajo, el título y el rótulo "Resultado por
     tarea" quedaban solos al pie de una hoja y la tabla arrancaba recién en
     la siguiente, con un salto feo en el medio de la tarjeta). Se evita que
     el salto de página caiga justo DESPUÉS de un título/rótulo — el
     navegador empuja el título junto con lo que sigue a la hoja siguiente
     en vez de dejarlo huérfano solo. */
  html.cmms-printing .cmms-card h3,
  html.cmms-printing .cmms-detail-field label { break-after: avoid; page-break-after: avoid; }

  /* v3 (17-ago-2026, corrección del usuario: "el boton imprimir debe estar
     disponible solo en la vista de la OT no en el wizard"). El enfoque v2
     (ocultar el wizard editable con display:none e imprimir un bloque de
     solo lectura inyectado adentro, ver .cmms-print-only) queda abandonado:
     ahora el botón Imprimir solo existe en las fichas de solo lectura
     dedicadas (woDetailView()/rgDetailView()/inspDetailView()/
     preventiveGeneratedReadView()) — la impresión nunca se dispara desde un
     wizard editable, así que no hace falta ninguna regla para ocultarlo. */

  /* v57 — "todas las letras deben ser negras" (a pedido del usuario: el
     gris azulado #4b5768/#45505f que se usaba para el eyebrow/labels/<th>
     — para diferenciarlos visualmente del valor — seguía leyéndose con un
     tinte celeste en el PDF). Ahora TODO el texto de la ficha impresa es
     negro puro (#000), sin excepciones — eyebrow/labels/títulos/valores/
     tabla/badges — y la jerarquía visual entre ellos se logra solo con
     tamaño/negrita/mayúsculas (ver el bloque de tamaños más abajo, v56). */
  html.cmms-printing .cmms-page-header h1,
  html.cmms-printing .cmms-asset-detail-header-text h1,
  html.cmms-printing .cmms-detail-card-eyebrow,
  html.cmms-printing .cmms-detail-card-title,
  html.cmms-printing .cmms-card h3 { color: #000 !important; font-weight: 700; }
  /* v58 — ya no son emoji sino números (ver detailCard()), así que alcanza
     con forzar el mismo negro/borde gris que el resto de la ficha impresa
     — no hace falta el filter:grayscale que se usaba antes para los glifos
     de emoji (que no responden a la propiedad color). */
  html.cmms-printing .cmms-detail-card-icon { background: none !important; box-shadow: none !important; border: 1px solid #000 !important; color: #000 !important; }

  /* v56 — "en la impresión las letras no se leen bien": los textos chicos
     en mayúscula (eyebrow, labels de campo, encabezados de tabla) venían al
     tamaño de pantalla (11px) con letter-spacing — a ese tamaño, en la
     resolución con la que Chrome arma el PDF, la combinación de mayúsculas
     + trazo fino + separado queda borrosa/pixelada. Se agrandan un poco,
     se les baja el letter-spacing y se les sube el peso — mismo criterio
     en los 3 lugares (eyebrow de tarjeta, label de campo, <th> de tabla)
     para que quede parejo. */
  html.cmms-printing .cmms-detail-card-eyebrow,
  html.cmms-printing .cmms-detail-field label,
  html.cmms-printing .cmms-table th { font-size: 12.5px !important; letter-spacing: .2px !important; font-weight: 700 !important; }
  html.cmms-printing .cmms-detail-value,
  html.cmms-printing .cmms-table td { font-size: 13.5px !important; font-weight: 500 !important; }
  html.cmms-printing .cmms-card h3 { font-size: 13px !important; }

  /* v68 — "falta agregarle un poco de grosor a las letras y lineas, para
     PM y CM": el texto normal (valores de campo, celdas de tabla) se imprimía
     en peso 400 (fino) y las líneas separadoras de fila en un gris muy claro
     (#e2e8f0) que a la resolución del PDF casi no se distingue del blanco.
     Se sube el peso del texto de cuerpo a 500 en TODA la impresión (ficha de
     activo, OT correctiva, OT preventiva) y se reutiliza el MISMO gris medio
     que ya usábamos para los bordes de tarjeta/th (#c7d0dd) en vez de
     #e2e8f0 para que las líneas queden visibles — mismo criterio, un solo
     lugar, se aplica parejo a los 3 tipos de ficha impresa. */
  html.cmms-printing select,
  html.cmms-printing textarea,
  html.cmms-printing input:not([type="checkbox"]):not([type="radio"]),
  html.cmms-printing .cmms-detail-field label,
  html.cmms-printing p,
  html.cmms-printing span,
  html.cmms-printing div,
  html.cmms-printing td,
  html.cmms-printing li { font-weight: 500; }
  html.cmms-printing .cmms-page-header h1,
  html.cmms-printing .cmms-asset-detail-header-text h1,
  html.cmms-printing .cmms-detail-card-eyebrow,
  html.cmms-printing .cmms-detail-card-title,
  html.cmms-printing .cmms-card h3,
  html.cmms-printing .cmms-table th,
  html.cmms-printing .cmms-badge,
  html.cmms-printing strong,
  html.cmms-printing b { font-weight: 700 !important; }

  /* v57 — labels y valores también en negro puro, ver comentario de arriba */
  html.cmms-printing .cmms-detail-field label { color: #000 !important; }
  html.cmms-printing .cmms-detail-value,
  html.cmms-printing .cmms-card p,
  html.cmms-printing .cmms-table td,
  html.cmms-printing code { color: #000 !important; background: none !important; }

  /* v55 — "siempre en blanco y negro, sin colores" (a pedido del usuario):
     antes el borde de los badges quedaba en su color de origen
     (currentColor heredado de .cmms-crit-A/.cmms-st-Falla/otTypeBadge, ej.
     rojo/amarillo/violeta) — ahora se fuerza texto y borde negros/gris
     oscuro sin excepción, mismo criterio para TODOS los badges (criticidad,
     estado, tipo de OT en el Historial). */
  html.cmms-printing .cmms-badge { background: none !important; color: #000 !important; border: 1px solid #000 !important; padding: 2px 8px; }

  /* tablas (Activos hijos / Historial de OT): mismo criterio, sin fondos */
  html.cmms-printing .cmms-table th { background: none !important; color: #000 !important; border-bottom: 1.5px solid #475569 !important; }
  html.cmms-printing .cmms-table td { border-bottom: 1.25px solid #64748b !important; }
  html.cmms-printing .cmms-table tr:hover td { background: none !important; }
  /* v2 (17-ago-2026, pedido del usuario: "esto aplica para todas las
     tablas") — red de seguridad para el caso extremo en que una tabla
     (checklist de Inspección/Preventiva, principalmente) sea tan larga que
     ni siquiera quepa en una hoja completa: el .cmms-card que la contiene
     ya no puede evitar el corte (ver comentario junto a .cmms-asset-print-
     flow más arriba), pero al menos el corte cae ENTRE filas, nunca a la
     mitad de una fila. */
  html.cmms-printing .cmms-table tr { break-inside: avoid; page-break-inside: avoid; }

  /* v55 — la ficha impresa arranca SIEMPRE con la información técnica (los
     6 bloques + "Activos hijos vinculados", si los hay) y el "Historial de
     Órdenes de Trabajo" pasa a su propia hoja — ver la clase
     .cmms-asset-print-page2 puesta en assetDetailView() sobre esa tarjeta.
     "Activos hijos vinculados" NO es una OT, así que se queda en la
     primera hoja junto con el resto de la info técnica, tal como pidió el
     usuario ("todo lo que no sean órdenes de trabajo" va en la 1ª hoja). */
  html.cmms-printing .cmms-asset-print-page2 { break-before: page; page-break-before: always; }

  /* v66 — impresión de la OT Preventiva (preventiveGeneratedDetailView()):
     esa pantalla tiene MUCHOS botones de acción repartidos por todo el
     formulario (Enviar/Guardar/Suspender/Reanudar/Avanzar/Retroceder/
     Adjuntar…), a diferencia de la ficha del activo/OT Correctiva donde
     alcanzaba con taggear a mano un puñado de botones con .cmms-no-print.
     Acá se ocultan TODOS los <button> de una — no afecta el checklist
     OK/NA/NOK de la sección "4. Tareas de mantenimiento", que son
     <label><input type=checkbox>, no <button> (ver
     preventiveGenChecklistTableHTML()). */
  html.cmms-printing button { display: none !important; }
  html.cmms-printing input[type="file"] { display: none !important; }

  /* selects/textareas/inputs del informe de ejecución: mismo criterio sin
     colores — se mantienen visibles (a diferencia de los botones) porque
     ahí es donde queda el contenido real (estado del equipo, observaciones). */
  html.cmms-printing select,
  html.cmms-printing textarea,
  html.cmms-printing input:not([type="checkbox"]):not([type="radio"]) {
    background: none !important; color: #000 !important; border: 1px solid #c7d0dd !important;
  }

  /* checklist OK/NA/NOK (.cmms-chip-check, ver preventiveGenChecklistTableHTML()):
     sin relleno de color — la opción marcada se distingue con un borde más
     grueso y negrita en vez de un fondo verde/gris/rojo. */
  html.cmms-printing .cmms-chip-check { border: 1px solid #9aa5b3 !important; color: #000 !important; background: none !important; }
  html.cmms-printing .cmms-chip-check:has(input:checked) { border: 2px solid #000 !important; font-weight: 700 !important; }

  /* v67 — checklist de tareas de la OT Preventiva
     (.cmms-prevgen-checklist-table, ver preventiveGenChecklistTableHTML()):
     colapsa a "una tarjeta apilada por tarea" bajo los 1100px de ancho (ver
     @media (max-width:1100px) en app.css) — el ancho real de una hoja carta
     ya impresa (~720px de contenido) cae justo en ese rango, así que en el
     PDF la tabla se veía como una tarjeta por tarea en vez de una tabla
     compacta, y como cada tarjeta podía medir más que el alto libre de la
     página, una tarea terminaba cortada a la mitad entre dos hojas. Acá se
     fuerza la tabla de vuelta a su forma real (thead/tbody/tr/td como
     tabla) — mucho más compacta, entra completa sin necesitar tanto alto
     por tarea — y cada <tr> evita partirse entre dos páginas. */
  /* 17-ago-2026 (pedido del usuario: "revisa como se está cuadrando la
     impresión, quedan mal las columnas") — en modo wide (pantalla) esta
     tabla NO usa table-layout:fixed (ver comentario largo junto a
     preventiveGenChecklistTableHTML() en app.js): "Campo adicional" y
     "Observaciones" no tienen width declarado, así que se expanden según su
     contenido. En pantalla eso da igual (hay scroll si hace falta), pero en
     la hoja impresa el ancho es fijo y esas dos columnas empujaban a
     "Estado" fuera del margen — se veía cortado, tal como se reportó.
     table-layout:fixed + un % por columna (identificada por data-col, no por
     posición — ver ese mismo comentario en app.js) fuerza que las 6 (o 5,
     sin "Catálogo") columnas siempre sumen dentro del 100% del ancho de la
     hoja, sin importar cuánto texto tenga cada celda. */
  html.cmms-printing .cmms-prevgen-checklist-table { display: table !important; width: 100% !important; table-layout: fixed !important; }
  html.cmms-printing .cmms-prevgen-checklist-table th[data-col="num"] { width: 4% !important; }
  html.cmms-printing .cmms-prevgen-checklist-table th[data-col="catalog"] { width: 8% !important; }
  html.cmms-printing .cmms-prevgen-checklist-table th[data-col="tarea"] { width: 22% !important; }
  html.cmms-printing .cmms-prevgen-checklist-table th[data-col="extra"] { width: 16% !important; }
  html.cmms-printing .cmms-prevgen-checklist-table th[data-col="obs"] { width: 32% !important; }
  html.cmms-printing .cmms-prevgen-checklist-table th[data-col="estado"] { width: 18% !important; }
  html.cmms-printing .cmms-prevgen-checklist-table td { word-break: break-word; overflow-wrap: break-word; }
  html.cmms-printing .cmms-prevgen-checklist-table thead { display: table-header-group !important; }
  html.cmms-printing .cmms-prevgen-checklist-table tbody { display: table-row-group !important; }
  html.cmms-printing .cmms-prevgen-checklist-table tr {
    display: table-row !important; margin-bottom: 0 !important; border: none !important; padding: 0 !important;
    background: none !important; box-shadow: none !important;
    break-inside: avoid !important; page-break-inside: avoid !important;
  }
  html.cmms-printing .cmms-prevgen-checklist-table td {
    display: table-cell !important; border: none !important; border-bottom: 1.25px solid #64748b !important;
    padding: 5px 6px !important; font-size: 11px !important; font-weight: 500 !important; vertical-align: top;
  }
  html.cmms-printing .cmms-prevgen-checklist-table td[data-label]::before { display: none !important; content: none !important; }
  html.cmms-printing .cmms-prevgen-checklist-table thead th { font-size: 10.5px !important; padding: 4px 6px !important; font-weight: 700 !important; border-bottom: 1.5px solid #475569 !important; }
  /* v69 — filas de subtítulo dentro del checklist ("Título"/"Texto libre",
     ver preventiveGenChecklistTableHTML(), <td colspan> con fondo
     var(--c-surface-2) inline): ese fondo depende del tema activo y en
     impresión quedaba invisible — se reemplaza por un borde grueso arriba
     y abajo (sin usar color de fondo) para que la sub-sección se distinga
     claramente de las tareas normales. */
  html.cmms-printing .cmms-prevgen-checklist-table td[colspan] {
    background: none !important; border-top: 1.5px solid #475569 !important;
    border-bottom: 1.5px solid #475569 !important; font-weight: 700 !important;
    padding: 6px 6px !important; text-transform: uppercase; font-size: 10.5px !important; letter-spacing: .3px;
  }

  /* v71 — "en el campo adicional, solo imprime el resultado, no las
     opciones": el tipo "Sí/No" (ver preventiveGenChecklistTableHTML(),
     extra.type==='checkbox') pinta DOS radios uno al lado del otro (Sí/No)
     para marcar en pantalla — en el PDF solo interesa la respuesta, no las
     dos opciones. Se oculta el <label> completo de la opción NO marcada, y
     al <label> de la opción SÍ marcada se le oculta solo el círculo del
     radio (input), dejando nada más el texto ("Sí" o "No") como resultado
     final. Si ninguna quedó marcada (informe sin completar), ambas
     desaparecen y el cuadro queda en blanco — correcto, no hay respuesta
     que mostrar. */
  html.cmms-printing label:has(> input[type="radio"]:not(:checked)) { display: none !important; }
  html.cmms-printing label:has(> input[type="radio"]:checked) input[type="radio"] { display: none !important; }
}
/* Fila de gráficos del Dashboard ("OTs por mes" + "Proyección de MP") uno al
   lado del otro: las tarjetas NO se estiran a repartirse el ancho disponible,
   cada una respeta su propio max-width (.cmms-card-chart-otsmes/
   .cmms-card-chart-mpproj) y quedan pegadas a la izquierda. */
.cmms-charts-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-start; }

/* v88b — FIX: este bloque era CSS legado de un donut viejo que ya no se usa
   en ninguna vista (grep confirma que ningún template arma `class="cmms-donut"`
   salvo el nuevo assetStatusHTML(), ver más abajo) — pero como reutilizaba
   el MISMO nombre de clase, su ::after (un círculo SÓLIDO, opaco, pensado
   para un donut de 84px) quedaba pintado ENCIMA del texto del centro del
   donut nuevo (148px) y lo tapaba por completo — de ahí el hueco en blanco
   sin el total. Se borra ese ::after; el donut real se define más abajo,
   junto a .cmms-donut-ring/.cmms-donut-center (v88). */
.cmms-legend { display: flex; flex-direction: column; gap: 7px; font-size: 13px; }
.cmms-legend-item { display: flex; align-items: center; gap: 7px; color: var(--c-text); }
.cmms-legend-item b { margin-left: auto; padding-left: 10px; color: var(--c-muted); font-weight: 500; }
.cmms-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }

/* OTs por mes — barras verticales apiladas (correctiva/roja + preventiva/amarillo),
   con el desglose numérico chico arriba de cada barra. Columnas casi pegadas
   (gap mínimo) y barra ancha (85% de la columna) para que se lean como un
   bloque continuo, con solo un hilo de separación entre meses. El ANCHO de
   la tarjeta se limita en .cmms-card-chart-otsmes (más abajo) — acá adentro
   ya no hace falta max-width, el contenido llena la tarjeta angosta. */
.cmms-monthbar-wrap { position: relative; display: flex; align-items: flex-end; gap: 3px; height: 190px; padding-top: 26px; overflow-x: auto; }
.cmms-monthbar-col { flex: 1; min-width: 26px; display: flex; flex-direction: column; align-items: center; height: 100%; }
/* Total de la barra (OT abiertas del mes: CM real + PM proyectada) — mismo
   formato que .cmms-vbarchart-value ("Cumplimiento de mantenimiento
   preventivo", ver más abajo) para que ambas tarjetas se lean como un
   mismo sistema. El desglose CM/PM ya no va acá arriba — se muestra
   DENTRO de cada segmento de color (.cmms-monthbar-seg-num, ver abajo). */
.cmms-monthbar-total { font-size: 12px; color: var(--c-muted); margin-bottom: 4px; }
.cmms-monthbar-track { flex: 1; width: 85%; display: flex; flex-direction: column-reverse; background: var(--c-donut-inner, rgba(148,163,184,.15)); border-radius: 4px; overflow: hidden; }
.cmms-monthbar-seg { width: 100%; transition: height .3s; display: flex; align-items: center; justify-content: center; overflow: visible; }
.cmms-monthbar-seg-cm { background: #ef4444; }
.cmms-monthbar-seg-pm { background: #eab308; }
/* Número CM/PM dentro de su propio segmento de color (pedido del usuario:
   antes iban arriba de la barra, separados de su color correspondiente).
   text-shadow en vez de un color fijo — se lee bien tanto sobre el rojo
   como sobre el amarillo. position:relative + z-index (reportado: "no cae
   el número 1" — un segmento muy angosto, ej. 1 OT sobre un máximo de 24,
   mide unos pocos px de alto; el número desborda hacia arriba gracias a
   overflow:visible en .cmms-monthbar-seg, pero sin esto quedaba TAPADO
   por el segmento de al lado, que se pinta después en el orden normal del
   documento — position:relative lo saca de ese orden y lo deja siempre
   arriba, sin importar cuál segmento sea más angosto). */
.cmms-monthbar-seg-num { position: relative; z-index: 2; font-size: 10px; font-weight: 800; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.55); line-height: 1; }
.cmms-monthbar-label { font-size: 11px; color: var(--c-muted); margin-top: 6px; }
/* Tarjeta de "OTs por mes" — v87: vuelve a compartir fila con las OTRAS 3
   tarjetas (Cumplimiento MP / Estado de los activos / Activos críticos en
   falla — pedido del usuario: "las 4 en una sola fila"), así que baja de
   50% (pensado para compartir fila con una sola tarjeta más) a ~28%. El
   gráfico ya trae su propio scroll horizontal de respaldo
   (.cmms-monthbar-wrap/.cmms-vbarchart overflow-x:auto) para cuando 12
   meses no entran cómodos en ese ancho. */
/* v89 — "Activos críticos en falla" bajó de 5 a 3 columnas (OT/TAG/Apertura,
   pedido del usuario), así que necesita mucho menos ancho; ese espacio
   ganado se reparte EQUITATIVAMENTE entre esta tarjeta y
   .cmms-card-chart-mpproj (pedido explícito) — de 28% sube a 36% cada una. */
.cmms-card-chart-otsmes { flex: 1 1 300px; max-width: 36%; min-width: 0; }

/* Barra apilada — legado, ya no se usa (ver .cmms-donut-* más abajo,
   "Estado de los activos" pasó a donut v88), se deja sin borrar por si
   alguna vista vieja la sigue referenciando. */
.cmms-stackbar-wrap { display: flex; flex-direction: column; gap: 14px; }
.cmms-stackbar-track { display: flex; height: 14px; border-radius: 7px; overflow: hidden; background: var(--c-donut-inner, rgba(148,163,184,.15)); }
.cmms-stackbar-seg { height: 100%; transition: width .3s; }

/* v88 — "Estado de los activos" en donut (pedido del usuario: menos ancho,
   más alto, para acercarse a la altura de "Cumplimiento de mantenimiento
   preventivo" en la misma fila). El anillo es un conic-gradient recortado
   con `mask` (radial-gradient transparente en el centro) en vez de tapar el
   hueco con un círculo sólido del color de fondo de la tarjeta — así no
   hace falta adivinar ese color (degradé, cambia con el tema día/noche) y
   el hueco queda perfecto en cualquier fondo. drop-shadow le da el relieve
   "3D" pedido. */
/* v90 — pedido del usuario: la leyenda pasa de ir AL LADO del donut a ir
   DEBAJO, en una fila horizontal (no una lista vertical) — así la tarjeta
   completa se puede angostar (ver .cmms-card-chart-status más abajo), ese
   ancho liberado es justamente lo que gana "Activos críticos en falla" en
   la misma fila. */
.cmms-donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 6px 0; }
.cmms-donut { position: relative; width: 148px; height: 148px; flex-shrink: 0; }
.cmms-donut-ring {
  width: 100%; height: 100%; border-radius: 50%;
  -webkit-mask: radial-gradient(circle at center, transparent 0 61%, #000 62% 100%);
  mask: radial-gradient(circle at center, transparent 0 61%, #000 62% 100%);
  filter: drop-shadow(0 10px 16px rgba(15,23,42,.22));
}
.cmms-donut-center {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center; pointer-events: none;
}
.cmms-donut-center-label { font-size: 10px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase; color: var(--c-muted); }
.cmms-donut-center-value { font-size: 28px; font-weight: 800; color: var(--c-nav-hover-text, var(--c-text)); line-height: 1.15; white-space: nowrap; }
/* Leyenda EN FILA (no columna) para ir debajo del donut sin ocupar alto de
   más — envuelve a 2 líneas sola si el ancho angosto de la tarjeta no
   alcanza para las 3 juntas. */
.cmms-legend-row { flex-direction: row !important; flex-wrap: wrap; justify-content: center; font-size: 13px; gap: 6px 16px !important; }
.cmms-legend-row .cmms-legend-item { gap: 6px; }
.cmms-legend-row .cmms-legend-item b { margin-left: 4px; padding-left: 0; }

/* Proyección de MP — barras verticales por mes, columnas casi pegadas
   (mismo criterio de compactado que OTs por mes, ver .cmms-monthbar-wrap).
   El ANCHO de la tarjeta se limita en .cmms-card-chart-mpproj más abajo.
   overflow-x/min-width (v43, pasó de 6 a 12 meses): con el doble de columnas
   hace falta el mismo scroll horizontal de respaldo que ya tenía
   .cmms-monthbar-wrap, para pantallas angostas. */
/* height:190px — igualada a .cmms-monthbar-wrap (pedido del usuario, para
   que "Cumplimiento de mantenimiento preventivo" quede a la misma altura
   que "OTs por mes" en vez de verse más bajo/desalineado en la misma
   fila, ver .cmms-charts-row). */
.cmms-vbarchart { position: relative; display: flex; align-items: flex-end; gap: 3px; height: 190px; padding-top: 10px; overflow-x: auto; }
.cmms-vbarchart-col { flex: 1; min-width: 24px; display: flex; flex-direction: column; align-items: center; height: 100%; }
.cmms-vbarchart-value { font-size: 12px; color: var(--c-muted); margin-bottom: 4px; }
.cmms-vbarchart-track { flex: 1; width: 85%; display: flex; align-items: flex-end; background: var(--c-donut-inner, rgba(148,163,184,.15)); border-radius: 4px; overflow: hidden; }
.cmms-vbarchart-fill { width: 100%; border-radius: 4px 4px 0 0; min-height: 3px; overflow: hidden; display: flex; flex-direction: column; }
/* v44: la barra de "Cumplimiento de MP" (mpProjectionHTML) pasó de un solo
   color a 2 segmentos apilados dentro de .cmms-vbarchart-fill — verde
   (ya generadas) abajo, amarillo (programadas, aún sin generar) arriba; si
   un mes está 100% al día el segmento amarillo mide 0 y la barra queda de
   un solo color verde. */
.cmms-vbarchart-fill-generated { width: 100%; background: #22c55e; }
.cmms-vbarchart-fill-pending { width: 100%; background: #eab308; }
.cmms-vbarchart-label { font-size: 11px; color: var(--c-muted); margin-top: 6px; }
/* Tarjeta de "Proyección de MP" — v89: sube a 36% (mismo criterio y misma
   proporción que .cmms-card-chart-otsmes de arriba — ver ese comentario). */
.cmms-card-chart-mpproj { flex: 1 1 300px; max-width: 36%; min-width: 0; }

/* Tarjeta de "Estado de los activos" (pedido del usuario: moverla al costado
   derecho de "Cumplimiento de mantenimiento preventivo" en la misma fila —
   ahí sobraba espacio porque esas dos tarjetas de arriba no se estiran a
   llenar el ancho disponible, ver comentario junto a .cmms-charts-row). Se
   agranda (v49, pedido del usuario: "sobra mucho espacio y las leyendas no
   se leen") — la leyenda pasó de una fila horizontal a la lista vertical
   por defecto de .cmms-legend (cada estado en su propia línea, cantidad
   alineada a la derecha vía .cmms-legend-item b), así que ya no se corta
   aunque la tarjeta crezca. */
/* v88b — FIX: flex-grow:1 la estiraba a repartirse el ancho de la fila en
   partes iguales con sus vecinas, pero su contenido (donut + leyenda) es
   angosto — sobraba un colchón de espacio en blanco a la derecha adentro de
   la tarjeta (pedido del usuario: "ese espacio podría ganarse para la tabla
   de la derecha"). flex-grow:0 la deja del ancho de su flex-basis (no se
   estira de más); el espacio que antes desperdiciaba pasa a repartirse
   entre las otras tarjetas de la fila, sobre todo "Activos críticos en
   falla" (ver flex-grow:2 más abajo). min-width:0 sigue siendo necesario
   (v86): sin eso no se puede encoger por debajo del ancho de su contenido. */
/* v90 — más angosta (240px, antes 320px): con la leyenda debajo del donut
   (ver .cmms-donut-wrap más arriba) ya no necesita tanto ancho — ese
   espacio liberado pasa DIRECTO a "Activos críticos en falla" (pedido del
   usuario, ver .cmms-card-chart-critical más abajo). */
.cmms-card-chart-status { flex: 0 1 240px; max-width: 240px; min-width: 0; }
/* "Activos críticos en falla" — v90: sube de 280px a 380px (el ancho que
   "Estado de los activos" dejó libre al angostarse), pensado para que el
   TAG quepa cómodo hasta con 12 caracteres SIN scroll horizontal (fuente
   un poco más chica que el resto de la app — 11.5px — y celdas más
   compactas). El scroll (.cmms-table-scroll) se deja solo como red de
   seguridad para un caso extremo, no debería activarse en el uso normal. */
.cmms-card-chart-critical { flex: 0 1 380px; max-width: 380px; min-width: 0; }
.cmms-card-chart-critical .cmms-table-scroll { overflow-x: auto; }
.cmms-card-chart-critical .cmms-table { font-size: 11.5px; table-layout: auto; }
.cmms-card-chart-critical .cmms-table th,
.cmms-card-chart-critical .cmms-table td { white-space: nowrap; padding: 8px 10px; }
/* Fila final "Más activos en falla" (v90, pedido del usuario: cuando hay
   más de 6 OT críticas abiertas, en vez de una 7ª fila normal) — texto
   centrado, look de "ver más" en vez de una fila de datos más. */
.cmms-critical-more-row td {
  text-align: center; color: var(--c-accent, #3b82f6); font-weight: 600;
  background: var(--c-topbar-btn-hover-bg, rgba(148,163,184,.08));
}

/* Línea de tendencia sobre "OTs por mes"/"Proyección de MP" — removida a
   pedido del usuario (ver comentario junto a drawDashboardTrendLines en
   app.js). Reglas .cmms-trendline* eliminadas por no usarse. */

/* ── Responsive del Dashboard ──────────────────────────────────────────
   Las tarjetas de .cmms-charts-row usan max-width en PORCENTAJE de esa fila
   (25/30/50%), pensado para pantallas de escritorio anchas. Al angostar la
   ventana esas mismas tarjetas se van reduciendo en proporción (25% de una
   fila angosta es un cuadradito minúsculo) en vez de apilarse — mientras que
   .cmms-kpi-grid (que usa auto-fit/minmax) sí se reacomoda solo. Acá
   forzamos que, por debajo de cierto ancho, la fila de gráficos pase a
   apilarse una debajo de otra a 100% de ancho, como cualquier tarjeta
   normal. */
@media (max-width: 1000px) {
  .cmms-charts-row { flex-direction: column; }
  .cmms-charts-row > .cmms-card,
  .cmms-card-chart-otsmes,
  .cmms-card-chart-mpproj,
  .cmms-card-chart-critical {
    max-width: 100% !important;
    flex: 1 1 100% !important;
    width: 100%;
  }
}
/* ── v42 — Menú lateral en celular/tablet angosta (≤760px): panel deslizable
   (drawer) en vez de la fila envuelta que había antes (esa fila, con ~10-12
   opciones, terminaba ocupando 3-4 líneas de alto ARRIBA del contenido en un
   celular real, empujando todo hacia abajo). Ahora el nav queda oculto fuera
   de pantalla (transform) y se abre con el botón ☰ del topbar (ver
   .cmms-menu-toggle-btn arriba y #btn-menu-toggle en app.js), superpuesto
   sobre el contenido con un fondo oscuro detrás (.cmms-nav-backdrop) que lo
   cierra al tocarlo — mismo patrón que cualquier drawer de menú estándar.
   Arriba de 760px (tablet en horizontal / desktop) no cambia nada: sigue la
   columna fija de siempre (.cmms-sidebar de la regla base, más arriba). ── */
@media (max-width: 760px) {
  .cmms-menu-toggle-btn { display: flex; }
  .cmms-sidebar {
    position: fixed; top: 0; left: 0; z-index: 900; height: 100vh;
    width: 82vw; max-width: 300px; min-width: 0;
    flex-direction: column; flex-wrap: nowrap; overflow-y: auto;
    border-right: 1px solid var(--c-border); border-bottom: none;
    transform: translateX(-100%); transition: transform .25s ease;
    box-shadow: 10px 0 28px -12px rgba(0,0,0,.5);
  }
  .cmms-sidebar.is-open { transform: translateX(0); }
  .cmms-nav-item { flex: none; }
  .cmms-nav-backdrop {
    display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 890;
  }
  .cmms-body--nav-open .cmms-nav-backdrop { display: block; }
}

/* ── v42 (Fase 0) — objetivos táctiles mínimos en celular/tablet angosta:
   los botones de acción de tabla (Editar/Ver/Eliminar/Historial, hoy
   `.cmms-btn-sm`, pensados para mouse) quedan más chicos que el mínimo
   recomendado para tocar con el dedo (~44px). Este ajuste solo aplica
   ≤600px — en desktop/tablet ancha se mantienen como estaban. ── */
@media (max-width: 600px) {
  .cmms-btn-sm { padding: 9px 14px; font-size: 12.5px; min-height: 40px; }
  .cmms-actions { gap: 8px; row-gap: 10px; }
  .cmms-nav-item { padding: 13px 14px; }
}

/* ── v42 (Fase 4/5) — Barra de acciones de formulario (Cancelar/Anterior/
   Siguiente/Guardar — .cmms-form-actions, usada en TODOS los formularios de
   la app: wizard de OT, informe de ejecución de checklist, Activos,
   Inventario, Compras, Usuarios, etc.) pegada abajo de la pantalla en
   celular/tablet: antes había que scrollear hasta el final del formulario
   (a veces varias pantallas de largo, ej. el wizard de OT o un checklist
   largo) para encontrar el botón "Guardar"/"Siguiente" — ahora queda
   siempre visible mientras se completa el formulario.
   Nota: se usa position:FIXED, no sticky — .cmms-main tiene
   "overflow-x:auto" (ver arriba), y por spec CSS eso fuerza también
   "overflow-y:auto" calculado (no se puede tener un eje "auto" y el otro
   "visible" a la vez): eso convierte a .cmms-main en su propio contenedor
   de scroll para position:sticky, pero como su alto NO está acotado a la
   pantalla (crece con el contenido, no hay scroll interno real), el
   "stick" nunca se activa — el botón quedaba flotando en su posición
   normal del documento en vez de pegado abajo. position:fixed no tiene ese
   problema (se posiciona respecto del viewport real). Como el formulario
   pierde el espacio que ocupaba esta barra al sacarla del flujo normal, se
   compensa con padding-bottom en .cmms-form (ver regla de abajo) para que
   el fixed no tape el último campo. Corte en 900px (no 760px) a propósito
   — igual que el resto de los ajustes de "pantalla chica" (tablas → tarjetas,
   panel de Tareas/Vista previa del Plan Preventivo): una tablet en vertical
   también se usa con el dedo en terreno, así que conviene que la barra
   de acciones se quede siempre visible ahí también, no solo en celular. ── */
@media (max-width: 900px) {
  .cmms-form-actions {
    /* left:230px (no 0) entre 761-900px: en ese rango el menú lateral
       TODAVÍA es la columna fija de siempre (el drawer recién reemplaza el
       menú por debajo de 760px, ver @media max-width:760px más abajo) — si
       la barra usara left:0 acá, quedaría superpuesta encima del menú en
       vez de ocupar solo el ancho del contenido. */
    position: fixed; left: 230px; right: 0; bottom: 0; z-index: 200; flex-wrap: wrap;
    background: var(--c-surface); border-top: 1px solid var(--c-border);
    padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
    box-shadow: 0 -8px 16px -10px rgba(0,0,0,.45);
  }
  /* Los popups (Anular/Suspender/Reabrir OT, catálogos, etc.) son cortos y
     ya se ven enteros sin scrollear — ahí la barra de acciones NO debe
     "escaparse" del popup y pegarse al fondo de toda la pantalla, así que
     se excluye del tratamiento fixed de arriba (vuelve a su posición
     normal, dentro de la tarjeta del modal). */
  .cmms-modal-overlay .cmms-form-actions {
    position: static; box-shadow: none; border-top: none; background: none; padding: 4px 0 16px;
  }
  /* Compensa el espacio que .cmms-form-actions deja de ocupar en el flujo
     normal al pasar a fixed, para que no tape el último campo del
     formulario — aplica a cualquier <form> que contenga esa barra,
     independiente de si es .cmms-form (wizard de OT, etc.) o .cmms-card
     (informe de ejecución del checklist, ver preventiveGeneratedDetailView()). */
  form:has(> .cmms-form-actions),
  form:has(.cmms-form-actions) { padding-bottom: 92px; }
}
/* Por debajo de 760px el menú lateral pasa a drawer oculto (ver más abajo)
   — ahí la barra de acciones sí debe usar todo el ancho de la pantalla. */
@media (max-width: 760px) {
  .cmms-form-actions { left: 0; }
}

/* ── v42 — Topbar en celular: antes era una sola fila fija de 72px con
   "justify-content:space-between" y SIN wrap — con el botón ☰ nuevo sumado
   a Inicio/Actualizar/Salir/toggle de tema/pill de usuario, en un celular
   angosto los botones de la derecha ya no entraban en esa fila y quedaban
   cortados o superpuestos fuera del panel. Ahora se permite que
   .cmms-topbar-right pase a una segunda línea completa (el topbar crece de
   alto en vez de recortar contenido), y los botones se achican un poco
   para que quepan más por fila. Ningún botón se oculta: todo sigue
   presente y clickeable, solo cambia cómo se acomoda. ── */
@media (max-width: 700px) {
  .cmms-topbar { flex-wrap: wrap; height: auto; min-height: 64px; padding: 10px 14px; row-gap: 8px; }
  .cmms-topbar-right { flex-wrap: wrap; row-gap: 8px; width: 100%; justify-content: flex-end; }
  .cmms-topbar-btn { padding: 9px 14px; font-size: 13px; }
  .cmms-topbar-btn-compact { padding: 8px 12px; font-size: 12px; }
  .cmms-user-pill { padding: 7px 12px; font-size: 13px; }
  /* El topbar acá arriba pasa a alto variable (envuelve a 2 filas) — un
     "top" fijo en px para .cmms-msg (pensado para el topbar de 72px fijo de
     desktop) ya no queda centrado en ninguna fila en particular. Más simple
     y confiable: en celular el aviso baja al pie de la pantalla en vez de
     perseguir la fila de R1-CMMS. */
  .cmms-msg { top: auto; bottom: 16px; max-width: calc(100% - 32px); }
}

/* ── v42 (Fase 2/3) — tabla → tarjetas en celular/tablet, para las tablas
   marcadas con la clase modificadora "cmms-table--responsive" (por ahora:
   listado de Órdenes de Trabajo y checklist de ejecución de OT Preventiva
   — las dos prioridades pedidas; el resto de las tablas sigue con el
   fallback de scroll horizontal general de .cmms-main mientras se migran
   en una siguiente pasada).
   El corte se fija en 900px (no 600-700px) a propósito: estas dos tablas
   tienen 10-11 y 5 columnas respectivamente, y ya no entran cómodas ni
   siquiera en una tablet en vertical (768-834px de ancho típico) — de
   quedar en modo tabla ahí, se cortarían las últimas columnas (Estado,
   Acciones) igual que en celular. Por eso el modo tarjeta cubre celular Y
   tablet vertical; recién en tablet horizontal / desktop (>900px) vuelve a
   la tabla clásica con todas las columnas visibles de una.
   Técnica estándar: se oculta el <thead> y cada <tr> pasa a ser una
   tarjeta vertical, con cada <td> mostrando "rótulo: valor" (el rótulo sale
   del atributo data-label agregado a cada celda en woTable()/
   preventiveGenChecklistTableHTML(), ver app.js) en vez de perderse fuera
   de la pantalla. ── */
@media screen and (max-width: 900px) {
  .cmms-table--responsive thead { display: none; }
  .cmms-table--responsive, .cmms-table--responsive tbody, .cmms-table--responsive tr {
    display: block; width: 100%;
  }
  /* El checklist de OT Preventiva (preventiveGenChecklistTableHTML() en
     app.js) le fija min-width:960px a la tabla para que "Tarea"/"Estado" no
     se compriman en contenedores angostos (ver comentario largo ahí) — en
     modo tarjeta (acá) eso sobra: display:block ya la hace ancho-completo
     por fila, así que se anula para no forzar scroll horizontal en
     celular/tablet. */
  .cmms-table--responsive { min-width: 0 !important; }
  .cmms-table--responsive tr {
    margin-bottom: 12px; border: 1px solid var(--c-border); border-radius: var(--radius);
    padding: 4px 12px; background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2));
    box-shadow: var(--c-panel-shadow);
  }
  .cmms-table--responsive td {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    border: none !important; padding: 8px 0; font-size: 13px;
  }
  .cmms-table--responsive td[data-label]::before {
    content: attr(data-label); font-weight: 700; font-size: 10.5px; text-transform: uppercase;
    letter-spacing: .4px; color: var(--c-muted); flex-shrink: 0;
  }
  /* Filas "colspan" (título de sección / texto libre del checklist): se
     muestran como bloque completo, sin el rótulo "clave: valor" (no tienen
     data-label, no aplica acá). */
  .cmms-table--responsive td[colspan] { display: block; }
  .cmms-table--responsive .cmms-actions { justify-content: flex-end; flex-wrap: wrap; width: 100%; }
  .cmms-table--responsive .cmms-chip-row { justify-content: flex-end; flex: 1; }
}

/* v65 (25-jul-2026) — BUGFIX reportado: en la pantalla completa de "Informe
   de ejecución" (preventiveGeneratedDetailView()), el techo real de ancho
   disponible para la tabla NO es la ventana completa — se le resta el menú
   lateral (230px) + el padding de .cmms-main + el padding del <form
   class="cmms-card"> que la contiene — y en monitores angostos/ventanas no
   maximizadas eso deja MENOS de los 960px que la tabla pide como mínimo,
   pero MÁS de los 900px del corte genérico de arriba, así que ni entraba
   completa ni pasaba a modo tarjeta: quedaba con scroll horizontal y
   columnas cortadas. Esta tabla (marcada aparte con
   "cmms-prevgen-checklist-table", SOLO en la vista de informe de ejecución
   — no en la "Vista previa" angosta del plan, que sigue igual) pasa a modo
   tarjeta con un corte más generoso (1100px) para no depender de cuánto
   sobre después del menú lateral. Mismas reglas que el bloque de arriba,
   repetidas porque los @media no se pueden "sumar" — si cambia una, debe
   cambiar la otra. */
@media screen and (max-width: 1100px) {
  .cmms-prevgen-checklist-table thead { display: none; }
  .cmms-prevgen-checklist-table, .cmms-prevgen-checklist-table tbody, .cmms-prevgen-checklist-table tr {
    display: block; width: 100%;
  }
  .cmms-prevgen-checklist-table { min-width: 0 !important; }
  .cmms-prevgen-checklist-table tr {
    margin-bottom: 12px; border: 1px solid var(--c-border); border-radius: var(--radius);
    padding: 4px 12px; background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2));
    box-shadow: var(--c-panel-shadow);
  }
  .cmms-prevgen-checklist-table td {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    border: none !important; padding: 8px 0; font-size: 13px;
  }
  .cmms-prevgen-checklist-table td[data-label]::before {
    content: attr(data-label); font-weight: 700; font-size: 10.5px; text-transform: uppercase;
    letter-spacing: .4px; color: var(--c-muted); flex-shrink: 0;
  }
  .cmms-prevgen-checklist-table td[colspan] { display: block; }
  .cmms-prevgen-checklist-table .cmms-chip-row { justify-content: flex-end; flex: 1; }
  /* La celda "Campo adicional" pone su rótulo/control en columna (ver
     extraLabelDisplay en preventiveGenChecklistTableHTML()) — en modo
     tarjeta el data-label ya cumple ese rol, así que ese rótulo interno
     sobra y desperdicia una línea completa por tarjeta. */
  .cmms-prevgen-checklist-table td label[style*="display:block"] { display: none !important; }
}

/* @container (17-ago-2026) — mismas reglas que el @media de arriba, pero
   disparadas según el ancho REAL del <div class="cmms-prevgen-table-wrap">
   que envuelve esta tabla (ver preventiveGenChecklistTableHTML() en
   app.js), no el ancho de la ventana. Necesario porque la pestaña "Informe
   de inspección" de OT de Inspección vive dentro de un formulario fijo en
   900px (ver inspFormView()) que puede estar dentro de una ventana MUY
   ancha — ahí el @media de arriba nunca se activaba (mide viewport) y la
   tabla en wide=true quedaba con la columna Estado desproporcionada. Con
   este @container el modo tarjeta se activa igual apenas el contenedor
   real baja de 1100px, sea cual sea el tamaño de la ventana — mismo
   resultado visual que la pestaña "Tareas de mantenimiento" de la OT ya
   generada (contenedor ancho, así que esta regla no la afecta en la
   práctica: ya le alcanza con el @media de arriba). */
.cmms-prevgen-table-wrap { container-type: inline-size; container-name: cmms-prevgen-table; }
@container cmms-prevgen-table (max-width: 1100px) {
  .cmms-prevgen-checklist-table thead { display: none; }
  .cmms-prevgen-checklist-table, .cmms-prevgen-checklist-table tbody, .cmms-prevgen-checklist-table tr {
    display: block; width: 100%;
  }
  .cmms-prevgen-checklist-table { min-width: 0 !important; }
  .cmms-prevgen-checklist-table tr {
    margin-bottom: 12px; border: 1px solid var(--c-border); border-radius: var(--radius);
    padding: 4px 12px; background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2));
    box-shadow: var(--c-panel-shadow);
  }
  .cmms-prevgen-checklist-table td {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    border: none !important; padding: 8px 0; font-size: 13px;
  }
  .cmms-prevgen-checklist-table td[data-label]::before {
    content: attr(data-label); font-weight: 700; font-size: 10.5px; text-transform: uppercase;
    letter-spacing: .4px; color: var(--c-muted); flex-shrink: 0;
  }
  .cmms-prevgen-checklist-table td[colspan] { display: block; }
  .cmms-prevgen-checklist-table .cmms-chip-row { justify-content: flex-end; flex: 1; }
  .cmms-prevgen-checklist-table td label[style*="display:block"] { display: none !important; }
}

/* Chips OK/NA/NOK del checklist de ejecución (reemplazan los checkboxes
   nativos sueltos, muy chicos para tocar con el dedo en terreno — ver
   preventiveGenChecklistTableHTML() en app.js). Un solo grupo de 3 chips por
   tarea, excluyentes entre sí (misma lógica de siempre, ver bindEvents() —
   acá solo cambia cómo se pintan).
   v?? — a pedido: con checklists largos (20+ tareas) los 3 chips apilados en
   hasta 3 filas por tarea (el tamaño original, pensado solo para touch)
   hacían crecer muchísimo el alto de la lista. Se compactaron (menos
   padding/alto/tipografía, gap más chico, flex-wrap:nowrap) y la columna
   "Estado" se ensanchó lo justo (ver preventiveGenChecklistTableHTML()) para
   que los 3 entren SIEMPRE en una sola línea por tarea — bastante más
   compacto en alto, sin volver a los checkboxes chicos de antes. */
/* Centrado (15-ago-2026, pedido explícito del usuario: "solo faltaria
   centrar el texto de los estados"). Primer intento (display:flex block-level
   + justify-content:center) alcanzaba en la Vista previa (table-layout:fixed,
   columna "Estado" fija en 195px) pero NO en la tabla `wide` de "Informe de
   inspección"/"Informe de ejecución" (sin table-layout:fixed — la columna
   puede terminar bastante más ancha que esos 195px, y un <div> flex
   block-level sigue ocupando el 100% de esa celda ya ensanchada: quedaba
   "centrado" dentro de una celda enorme, que visualmente se sigue viendo
   pegado a la izquierda bajo el encabezado "Estado" (left-aligned por
   default en .cmms-table th) — ver también el <td style="text-align:center">
   agregado en preventiveGenChecklistTableHTML(), app.js). Con
   display:inline-flex (se achica al contenido, ya no ocupa toda la celda),
   el text-align:center del <td> que lo contiene SÍ lo centra de verdad, sea
   cual sea el ancho real de la columna. Los overrides de abajo
   (.cmms-table--responsive/.cmms-prevgen-checklist-table, modo tarjeta en
   pantallas angostas) siguen con flex-end + flex:1 (necesitan volver a
   ocupar todo el ancho de la fila en modo tarjeta) — no se tocaron. */
.cmms-chip-row { display: inline-flex; gap: 6px; flex-wrap: nowrap; justify-content: center; }
.cmms-chip-check {
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  min-height: 30px; padding: 5px 9px; border-radius: 14px; border: 1px solid var(--c-border);
  background: var(--c-surface-2); color: var(--c-muted); font-weight: 700; font-size: 11px;
  white-space: nowrap;
  cursor: pointer; user-select: none; transition: background .12s, border-color .12s, color .12s;
}
/* REVERTIDO (16-ago-2026): se probaron 2 fixes de CSS al checkbox
   (margin:0, luego appearance:none dibujado a mano) y NINGUNO de los dos
   ayudó — el segundo intento (appearance:none) empeoró todo, el navegador
   del usuario terminó dibujando un control tipo "interruptor" gigante en
   vez de un checkbox chico (probablemente el navegador/SO del usuario no
   respeta appearance:none igual que en un Chrome estándar). Conclusión: el
   problema NUNCA fue el checkbox en sí — es exactamente el MISMO que usa
   la Vista previa de la pestaña 3 (que el usuario confirmó que se ve bien),
   mismo CSS, mismo HTML. El verdadero causante es que esta tabla usa
   `wide=true` (columnas con table-layout:auto, se ensanchan más que en la
   Vista previa) — ver el fix aplicado en preventiveGenChecklistTableHTML()/
   inspChecklistExecFieldHtml() en app.js: ahora también usa wide=false acá,
   para que la tabla sea BIT a BIT igual a la Vista previa (mismo ancho de
   columna Estado, mismos chips). Se vuelve a la versión ORIGINAL simple de
   este selector (accent-color nativo), la misma que ya se veía bien en la
   pestaña 3 antes de tocar nada. */
.cmms-chip-check input { width: 14px; height: 14px; flex-shrink: 0; accent-color: var(--c-accent); cursor: pointer; }
/* CAUSA REAL encontrada con DevTools (16-ago-2026, el usuario mandó el
   panel "Styles" del inspector): el checkbox de arriba, cuando vive dentro
   de un contenedor con clase "cmms-field" (como #insp-checklist-exec-field
   de "Informe de inspección" — la Vista previa de la pestaña 3 NO usa esa
   clase, por eso ahí nunca se notó), pierde contra la regla genérica
   ".cmms-field input" (más abajo en este archivo, MISMA especificidad
   pero definida después → gana ella) que le pone width:100%/padding/
   border/fondo de input de texto — el checkbox terminaba midiendo
   ~50x14px en vez de 14x14px (confirmado con el "Box Model" del
   inspector), de ahí el espacio raro y el "interruptor" gigante de los
   intentos anteriores. Esta regla, un nivel más específica
   (.cmms-field .cmms-chip-check input > .cmms-field input), gana siempre
   y deja el checkbox del tamaño correcto sin importar en qué contenedor
   esté. */
.cmms-field .cmms-chip-check input {
  width: 14px; height: 14px; padding: 0; border: 0; border-radius: 0;
  background: none; flex-shrink: 0; accent-color: var(--c-accent); cursor: pointer;
}
.cmms-field .cmms-chip-check input:focus { outline: none; box-shadow: none; }
.cmms-field .cmms-chip-check input[readonly],
.cmms-field .cmms-chip-check input:disabled { background: none; }
.cmms-chip-check:hover { border-color: var(--c-accent); }
.cmms-chip-check--ok:has(input:checked)  { border-color: #16a34a; color: #16a34a; background: rgba(22,163,74,.14); }
.cmms-chip-check--na:has(input:checked)  { border-color: #64748b; color: var(--c-text); background: rgba(100,116,139,.16); }
.cmms-chip-check--nok:has(input:checked) { border-color: #dc2626; color: #dc2626; background: rgba(220,38,38,.14); }

/* Chips de "equipos seleccionados" (Plan de Mantenimiento Preventivo,
   selección múltiple v43 — ver prevAssetChipsHTML() en app.js) y de
   "especialidad seleccionada" (Activo, catálogo editable) — mismo estilo
   simple: etiqueta + botón chico "✕" para quitar. */
.cmms-chip-list { display: flex; flex-wrap: wrap; gap: 8px; min-height: 32px; }
.cmms-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 6px 5px 12px; border-radius: 16px; border: 1px solid var(--c-border);
  background: var(--c-surface-2); color: var(--c-text); font-size: 12.5px; font-weight: 600;
}
.cmms-chip-remove {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%; border: none; cursor: pointer;
  background: rgba(148,163,184,.25); color: var(--c-text); font-size: 11px; line-height: 1; padding: 0;
}
.cmms-chip-remove:hover { background: rgba(220,38,38,.25); color: #fca5a5; }

/* Calendario del Dashboard */
.cmms-cal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.cmms-cal-legend { display: flex; gap: 16px; font-size: 12px; margin-bottom: 10px; }
.cmms-cal-legend .cmms-legend-item { display: flex; align-items: center; gap: 6px; color: var(--c-muted); }
.cmms-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.cmms-cal-weekdays { margin-bottom: 4px; }
.cmms-cal-weekday { text-align: center; font-size: 11px; font-weight: 600; color: var(--c-muted); text-transform: uppercase; letter-spacing: .4px; padding: 4px 0; }
/* v49 (pedido del usuario: "aumenta el ancho de cada recuadro de día, sobra
   mucho espacio") — antes tenía aspect-ratio:1/1, que en pantallas anchas
   forzaba cada celda a un cuadrado angosto (acotado por max-height) sin
   llenar el ancho real de su columna del grid (7 columnas de 1fr), dejando
   un hueco vacío a la derecha de cada celda mucho más grande que el `gap`
   de 4px entre filas. Al sacar el aspect-ratio, el ancho vuelve a estirarse
   solo (stretch, comportamiento por defecto del grid) hasta llenar su
   columna — el alto sigue acotado por separado (min/max-height), así que
   la única separación entre celdas, tanto horizontal como vertical, vuelve
   a ser el mismo `gap: 4px` del grid. */
/* v49 (pedido del usuario: "aumenta el alto un 100%") — min/max-height al
   doble de antes (40/90 → 80/180) para que entre cómodo el día + la grilla
   de códigos de OT de abajo. flex-direction:column apila el número del día
   arriba y la grilla de OT debajo (antes era una fila simple, alcanzaba con
   el día solo). overflow:hidden en la celda + overflow-y:auto en
   .cmms-cal-day-ots (más abajo): si un día tiene MUCHAS OT, la lista scrollea
   adentro de su propio espacio en vez de romper el alto parejo de la fila
   del calendario. */
.cmms-cal-day { position: relative; min-height: 80px; max-height: 180px; border-radius: 6px; background: var(--c-surface-2); display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; padding: 4px 6px; box-sizing: border-box; overflow: hidden; }
.cmms-cal-day-empty { background: transparent; }
.cmms-cal-day-num { font-size: 12px; color: var(--c-text); flex-shrink: 0; }
.cmms-cal-day[style*="background"] .cmms-cal-day-num { color: #fff; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,.4); }
/* Listado de códigos de OT del día (v49, pedido del usuario) — grilla de 3
   columnas, cada código mantiene el color de su tipo (CAL_COLOR_CM/PM en
   app.js) a todo color; el fondo de la celda ahora es una versión atenuada
   de ese mismo color (ver CAL_BG_CM/CAL_BG_PM en app.js) para que el chip a
   color completo se siga leyendo — antes, con el fondo de la celda ya
   sólido del mismo color, un chip también sólido casi desaparecía. */
.cmms-cal-day-ots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; width: 100%; margin-top: 3px; padding-bottom: 14px; overflow-y: auto; }
.cmms-cal-day-ot { font-size: 8.5px; font-weight: 700; color: #fff; text-align: center; border-radius: 3px; padding: 1px 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 13px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.18); }
/* Casillero "…" cuando un día supera CAL_MAX_OT_CHIPS (v49, pedido del
   usuario) — mismo rectángulo que un código de OT normal, pero sin color de
   tipo (no representa una OT puntual, sino "hay más") para no confundirlo
   con un CM o PM real. El total real de ese día sigue en
   .cmms-cal-day-count, esto es solo un aviso de que la lista se recortó. */
.cmms-cal-day-ot-more { background: rgba(148,163,184,.35); letter-spacing: 1px; }
.cmms-cal-day-count { position: absolute; bottom: 3px; right: 4px; font-size: 10px; font-weight: 700; color: #fff; background: rgba(0,0,0,.35); border-radius: 8px; padding: 0 5px; line-height: 15px; }
.cmms-cal-day-today { outline: 2px solid var(--c-accent); outline-offset: -2px; }
.cmms-cal-day-clickable { cursor: pointer; transition: transform .1s ease; }
.cmms-cal-day-clickable:hover { transform: scale(1.05); box-shadow: var(--c-panel-shadow-hover); }

/* Vista anual compacta de "Frecuencia" (Plan de Mantenimiento Preventivo) —
   ver renderPrevFreqYearCalendar() en app.js. 12 mini-meses en cuadrícula,
   pensada para mostrar el año completo de un vistazo. */
.cmms-yearcal-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.cmms-yearcal-months { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
@media (max-width: 900px) { .cmms-yearcal-months { grid-template-columns: repeat(2, 1fr); } }
.cmms-yearcal-month { background: var(--c-surface-2); border-radius: 8px; padding: 6px 8px 8px; }
.cmms-yearcal-month-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; color: var(--c-muted); text-align: center; margin-bottom: 4px; }
.cmms-yearcal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.cmms-yearcal-weekdays { margin-bottom: 2px; }
.cmms-yearcal-weekdays div { text-align: center; font-size: 9px; color: var(--c-muted); }
.cmms-yearcal-day { aspect-ratio: 1 / 1; min-height: 16px; border-radius: 3px; font-size: 9px; line-height: 16px; text-align: center; color: var(--c-text); background: var(--c-surface); }
.cmms-yearcal-day-empty { background: transparent; }
.cmms-yearcal-day-due { background: var(--c-accent, #2563eb); color: #fff; font-weight: 700; }
/* Ocurrencia con excepción puntual (click distinto al día por defecto de su
   período) — color aparte del resto de las ocurrencias "regulares", para
   que salte a la vista qué mes/semana/día quedó movido manualmente (ej. por
   un feriado). Ver computePrevFreqOccurrences()/renderPrevFreqYearCalendar()
   en app.js. */
.cmms-yearcal-day-override { background: #8b5cf6; color: #fff; font-weight: 700; }
/* v56 — 1er click del flujo de "mover una mantención puntual" (ver
   `pendingMove` en bindEvents()/renderPrevFreqYearCalendar() en app.js):
   pisa el azul/violeta normal del día mientras espera el 2do click que
   confirme el día nuevo. Rojo a propósito — mismo color que .cmms-btn-danger
   / .cmms-yearcal-day-end, para leerse como "atención, pendiente de mover",
   no como un estado final. */
.cmms-yearcal-day-pending-move { background: #ef4444 !important; color: #fff; font-weight: 700; }
/* Día clickeable (formulario editable): click fija/quita una excepción
   puntual del período al que pertenece ese día — ver bindEvents() en app.js. */
.cmms-yearcal-day-pick { cursor: pointer; transition: transform .1s ease; }
.cmms-yearcal-day-pick:hover { transform: scale(1.25); box-shadow: var(--c-panel-shadow-hover); position: relative; z-index: 1; }
/* Día que hoy es la "Fecha de inicio" elegida (período 0, el que define el
   día por defecto del resto) — anillo aparte de la marca de "mantención
   programada"/"excepción". */
.cmms-yearcal-day-anchor { outline: 2px solid var(--c-warn, #f59e0b); outline-offset: 1px; }
/* Día "Fecha de término" del plan — anillo distinto al de la ancla, para no
   confundir inicio con término. */
.cmms-yearcal-day-end { outline: 2px solid #ef4444; outline-offset: 1px; }
/* Días posteriores a la "Fecha de término": fuera del plazo del plan — no
   se generan mantenciones ahí ni son clickeables (ver renderPrevFreqYearCalendar). */
.cmms-yearcal-day-past-end { opacity: .35; }
/* v76 (30-jul-2026) — a pedido del usuario (confusión reportada: "días
   hábiles bloqueados sin sentido" cuando en realidad ya habían pasado, no
   que dejaran de ser hábiles): día HÁBIL pero ya transcurrido respecto de
   hoy — mismo criterio de "no editable, por trazabilidad" que
   .cmms-yearcal-day-past-end, pero con un patrón a rayas en vez de solo
   opacidad reducida, para distinguirlo a simple vista de un día no hábil o
   fuera de plazo (ver comentario largo junto a cmms-yearcal-day-elapsed en
   renderPrevFreqYearCalendar(), public/app.js). */
.cmms-yearcal-day-elapsed {
  opacity: .55;
  background-image: repeating-linear-gradient(135deg, rgba(0,0,0,.08) 0 4px, transparent 4px 8px);
}

/* v69 (30-jul-2026) — a pedido del usuario: "flash" rojo sobre TODO el
   calendario (2 segundos, se saca solo) cuando el guardado se bloquea por
   el bug del "día fantasma" (ver validatePrevWoStep() en app.js) — para que
   la vista se vaya derecho ahí, además del texto del banner de error.
   #prevwo-freq-calendar-wrap necesita position:relative para que el overlay
   (position:absolute) quede pegado exactamente a su tamaño. pointer-events:
   none en el overlay para NO bloquear los clicks del calendario ni durante
   ni después de la animación (pedido explícito). */
#prevwo-freq-calendar-wrap { position: relative; }
.cmms-yearcal-flash-error {
  position: absolute; inset: 0; z-index: 5; pointer-events: none;
  background: rgba(220,38,38,.4); border-radius: var(--radius);
  animation: cmms-yearcal-flash-fade 2s ease-out forwards;
}
@keyframes cmms-yearcal-flash-fade {
  0% { opacity: 1; }
  80% { opacity: 1; }
  100% { opacity: 0; }
}

/* v71 (30-jul-2026) — a pedido del usuario: mientras el plan no tenga lo
   mínimo indispensable para calcular una "Vista anual" real (modo elegido,
   y si es Automática, además Fecha inicio + Frecuencia — ver
   refreshPrevFreqCal() en app.js), el calendario se pinta atenuado y sin
   poder interactuar (pointer-events:none acá SÍ bloquea clicks — a
   diferencia de .cmms-yearcal-flash-error de arriba, que es solo un aviso
   pasajero, esto dura mientras falten esos datos). */
.cmms-yearcal-gated .cmms-yearcal { opacity: .5; filter: grayscale(60%); pointer-events: none; }

/* v73 (30-jul-2026) — a pedido del usuario: versión GENÉRICA (para
   cualquier elemento del sistema, no solo la Vista anual) del mismo "flash"
   rojo de baja opacidad — ver flashElement() en app.js. .cmms-flash-error-target
   solo aporta el position:relative que el overlay necesita para calzar
   exacto sobre el elemento (si ese elemento ya fuera position:relative o
   superior de por sí, no hace daño repetirlo). El overlay en sí es idéntico
   en comportamiento a .cmms-yearcal-flash-error (2s, pointer-events:none,
   no bloquea la interacción ni durante ni después de la animación). */
.cmms-flash-error-target { position: relative; }
.cmms-flash-error-overlay {
  position: absolute; inset: 0; z-index: 5; pointer-events: none;
  background: rgba(220,38,38,.4); border-radius: var(--radius);
  animation: cmms-yearcal-flash-fade 2s ease-out forwards;
}

/* Table */
.cmms-table { width: 100%; border-collapse: collapse; }
.cmms-table th { padding: 12px 12px; background: var(--c-surface-2); border-bottom: 1px solid var(--c-border); font-size: 11px; font-weight: 600; color: var(--c-muted); text-align: left; white-space: nowrap; text-transform: uppercase; letter-spacing: .4px; }
/* Design System — Fase 3: alto de fila 44-48px (antes 10px de padding
   vertical daba ~38px). El card compacto de "Activos críticos en falla"
   (dashboard) tiene su propio override más abajo y no se ve afectado. */
.cmms-table td { padding: 14px 12px; border-bottom: 1px solid var(--c-table-divider); font-size: 13px; vertical-align: middle; color: var(--c-text); }
.cmms-table tr:last-child td { border-bottom: none; }
.cmms-table tr:hover td { background: var(--c-row-hover); }
.cmms-row-clickable { cursor: pointer; }
.cmms-row-clickable:hover td { background: rgba(59,130,246,.06); }
/* Design System — Fase 1: códigos/TAGs (OT-2040, GRU-0001, etc.) en
   monoespaciada 12px/500 — antes heredaba la tipografía general a 11px/600.
   Regla genérica (no solo dentro de tablas) para que los mismos TAGs se
   vean igual en tarjetas de detalle, modales, etc. */
code { font-family: 'JetBrains Mono', 'Roboto Mono', ui-monospace, Consolas, monospace; font-size: 12px; font-weight: 500; }
.cmms-table code, code.cmms-code { background: var(--c-code-bg); color: var(--c-code-text); padding: 1px 6px; border-radius: 3px; }
.cmms-actions { white-space: nowrap; display: flex; gap: 5px; }

/* v38 — sugerencias de la vista previa de carga masiva de activos (Región/
   Comuna/Fabricante que no calzan exacto con algo ya conocido, ver
   buildRowSuggestions en assets.routes.js): antes usaban un color de fondo
   fijo (#f4f4f4) que quedaba ilegible en los temas oscuro/medianoche — acá
   se usan las mismas variables de tema que el resto de la app
   (--c-surface-2/--c-text/--c-border), así se adapta solo en los 3 temas.
   .cmms-row-conflict resalta en rojo la fila de la tabla mientras tenga
   alguna sugerencia sin resolver (ver previewRowsTable/bind() en app.js).
   Primer intento: box-shadow inset en cada <td> — se veía como una línea
   vertical roja repetida en cada columna (una por celda) en vez de un solo
   recuadro alrededor de la fila entera, confundía más de lo que ayudaba
   (reportado por el usuario). Con border-collapse:collapse (ver .cmms-table
   más arriba) un <tr> no puede tener borde propio que el navegador respete,
   así que el recuadro se arma a mano: borde arriba/abajo en TODAS las
   celdas de la fila + borde izquierdo en la primera celda y derecho en la
   última — junto con border-collapse, el navegador funde esos bordes en
   una sola línea continua por lado, dando el efecto de "una fila con un
   recuadro rojo unificado alrededor" en vez de líneas sueltas por celda. */
.cmms-bulk-suggestion { background: var(--c-surface-2); border: 1px solid var(--c-border); color: var(--c-text); border-radius: 6px; padding: 8px 10px; font-size: 0.92em; }
.cmms-bulk-suggestion + .cmms-bulk-suggestion { margin-top: 6px; }
/* Selector "table tr.cmms-row-conflict td" a propósito (con el "tr"
   explícito) en las reglas de abajo: iguala la especificidad de
   ".cmms-table tr:last-child td" (línea de arriba, que le saca el borde
   inferior a la ÚLTIMA fila de la tabla) — un simple ".cmms-row-conflict
   td" pierde ese empate porque a esa regla le falta el elemento "tr" (CSS
   compara primero cantidad de clases/pseudo-clases, y ahí ambas quedan
   2 a 2, pero después compara cantidad de elementos de tipo: "tr"+"td" = 2
   contra solo "td" = 1, y gana la de más elementos). Con la especificidad
   ya empatada, gana la regla que aparece DESPUÉS en este archivo — que es
   esta — así que la fila en conflicto conserva su borde rojo abajo incluso
   cuando es la última fila de la tabla (caso común: cargas de pocas filas). */
.cmms-table tr.cmms-row-conflict td { background: rgba(239,68,68,.08); border-top: 2px solid var(--c-danger); border-bottom: 2px solid var(--c-danger); }
.cmms-table tr.cmms-row-conflict td:first-child { border-left: 2px solid var(--c-danger); }
.cmms-table tr.cmms-row-conflict td:last-child { border-right: 2px solid var(--c-danger); }
.cmms-table tr.cmms-row-conflict:hover td { background: rgba(239,68,68,.14); }

/* Filters */
.cmms-filters { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
/* Design System — Fase 3: alto 36-38px (antes ~33px), borde #CBD5E1 y radio
   6px (más angosto que el --radius de 8px que usan tarjetas/botones, a
   propósito: distingue visualmente los campos de filtro del resto). */
.cmms-search  { flex: 1; min-width: 180px; padding: 9px 10px; border: 1px solid var(--c-border); border-radius: 6px; font-size: 13px; background: var(--c-surface); color: var(--c-text); }
.cmms-filters select { padding: 9px 8px; border: 1px solid var(--c-border); border-radius: 6px; font-size: 13px; background: var(--c-surface); color: var(--c-text); }
html[data-cmms-theme="light"] .cmms-search,
html[data-cmms-theme="light"] .cmms-filters select { border-color: #cbd5e1; }
.cmms-date-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--c-muted); }
.cmms-date-label input[type="date"] { padding: 6px 8px; border: 1px solid var(--c-border); border-radius: var(--radius); font-size: 13px; background: var(--c-surface); color: var(--c-text); }

/* Filters — variante "panel" (por ahora solo Activos, ver assetsView() en
   app.js): unifica el buscador (TAG/nombre/clase) con los selects de
   Estado/Criticidad/Clase dentro de una sola tarjeta con etiqueta "Filtros",
   en vez de dejarlos como una fila suelta sin agrupar. Reusa las clases
   .cmms-search / .cmms-filters select ya existentes (y su estilo "glass" de
   más abajo) — solo cambia el contenedor y agrega el encabezado. */
.cmms-filters.cmms-filters--panel {
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  width: 50%;
  min-width: 420px;
  max-width: 640px;
  border: 1px solid var(--c-divider, var(--c-border));
  border-radius: var(--radius);
  padding: 12px 14px;
  background: var(--c-surface);
}
.cmms-filters-head { display: flex; align-items: center; gap: 12px; }
.cmms-filters-title { display: flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 600; color: var(--c-muted); white-space: nowrap; }
.cmms-filters-row {
  display: flex; gap: 8px; flex-wrap: wrap;
  padding-top: 10px;
  border-top: 1px solid var(--c-divider, var(--c-border));
}
/* Checkboxes tipo "Stock bajo mínimo" / "Pendiente recepción" (ver
   inventoryView() en app.js) — filtros independientes entre sí, no un
   select, pero deben verse a la misma altura que los selects vecinos
   dentro de .cmms-filters-row. */
.cmms-filter-check {
  display: flex; align-items: center; gap: 6px;
  padding: 7px 10px; font-size: 13px; color: var(--c-text);
  border: 1px solid var(--c-divider, var(--c-border)); border-radius: var(--radius);
  background: var(--c-surface); cursor: pointer; white-space: nowrap;
}
/* v1 — el tema claro usa --c-border #e2e8f0 (gris casi blanco) sobre un
   fondo --c-surface también blanco: la línea divisoria entre el buscador y
   los selects, y el borde de cada select, quedaban invisibles a simple
   vista. --c-divider sube el contraste solo para esta caja, sin afectar
   --c-border en el resto de la app (tablas, tarjetas, botones, etc). */
html[data-cmms-theme="light"] .cmms-filters--panel { --c-divider: #cbd5e1; }
html[data-cmms-theme="light"] .cmms-filters--panel .cmms-search,
html[data-cmms-theme="light"] .cmms-filters--panel select { border-color: #cbd5e1; }
@media (max-width: 900px) {
  .cmms-filters.cmms-filters--panel { width: 100%; min-width: 0; max-width: none; }
}
@media (max-width: 640px) {
  .cmms-filters-head { flex-wrap: wrap; }
}

/* Badges — la base (.cmms-badge) sigue siendo pastilla sólida + texto
   blanco: la usan puntos aislados sin clase de color propia (via style=
   "background:…" inline, ver p.ej. calendario/tipo de OT en historial) que
   no se repiten fila tras fila, así que mantenerlos 100% opacos no cansa la
   vista. Las familias de estado de abajo (Criticidad/Estado/Tipo de OT/
   Prioridad/Estado de OT-Compra-Inventario), que sí se repiten en cada fila
   de cada tabla, usan el estilo "soft" en su lugar — ver v51 más abajo. */
/* Design System — Fase 2: padding exacto 4px 8px (antes 3px 10px). El
   font-size 11px / font-weight 600 ("SemiBold") ya coincidía. */
.cmms-badge { display: inline-block; padding: 4px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; color: #fff; }
/* v51 — Criticidad/Estado: antes pastilla 100% opaca + texto blanco fijo,
   muy intensa para una columna que aparece en CADA fila de la tabla.
   Estilo "soft": fondo al ~15% de opacidad del color semántico, texto en
   ese mismo color (saturado en modo Noche; una versión más oscura del
   mismo tono en modo Día, ver el override junto a html[data-cmms-theme=
   "light"] más abajo, para que siga cumpliendo contraste AA sobre fondo
   casi blanco). */
.cmms-crit-A { background: rgba(220,38,38,.15); color: #f87171; }
.cmms-crit-B { background: rgba(217,119,6,.16); color: #fbbf24; }
.cmms-crit-C { background: rgba(22,163,74,.16); color: #4ade80; }
.cmms-st-Operativo     { background: rgba(22,163,74,.16); color: #4ade80; }
.cmms-st-Mantenimiento { background: rgba(217,119,6,.16); color: #fbbf24; }
.cmms-st-Falla         { background: rgba(220,38,38,.15); color: #f87171; }
.cmms-st-Baja          { background: rgba(75,85,99,.25); color: #cbd5e1; }
/* v51 — mismo criterio "soft" que Criticidad/Estado arriba, extendido al
   resto de las familias de badge (Tipo de OT, Prioridad, Estado de OT/
   Compra/Inventario) para que la temática nueva quede pareja en toda la
   app y no solo en la tabla de Activos. Los overrides de texto más oscuro
   para modo Día están agrupados por color más abajo, junto a los de
   Criticidad/Estado. */
.cmms-ot-CM  { background: rgba(224,68,124,.16); color: #f472b6; }
.cmms-ot-PM  { background: rgba(22,163,74,.16);  color: #4ade80; }
.cmms-ot-PdM { background: rgba(217,119,6,.16);  color: #fbbf24; }
.cmms-ot-RG  { background: rgba(124,58,237,.16); color: #a78bfa; }
.cmms-prio-D { background: rgba(220,38,38,.15);  color: #f87171; }
.cmms-prio-A { background: rgba(217,119,6,.16);  color: #fbbf24; }
.cmms-prio-B { background: rgba(37,99,235,.15);  color: #60a5fa; }
.cmms-prio-C { background: rgba(22,163,74,.16);  color: #4ade80; }
/* v23: la OT nueva todavía no existe en la base (no tiene id) mientras el
   usuario no llegue a la pestaña 2 — ver comentario largo junto a
   woStatusBox() en app.js. Gris neutro para diferenciarlo claramente de
   "Abierta" (que sí es un estado real, ya persistido). */
.cmms-wost-SIN-REGISTRO { background: rgba(100,116,139,.2);  color: #94a3b8; }
.cmms-wost-Abierta     { background: rgba(224,68,124,.16); color: #f472b6; }
.cmms-wost-P-Asignar   { background: rgba(217,119,6,.16);  color: #fbbf24; }
.cmms-wost-P-Ejecutar  { background: rgba(37,99,235,.15);  color: #60a5fa; }
.cmms-wost-P-Cierre    { background: rgba(124,58,237,.16); color: #a78bfa; }
.cmms-wost-Cerrada     { background: rgba(22,163,74,.16);  color: #4ade80; }
.cmms-wost-Suspendida  { background: rgba(234,88,12,.16);  color: #fb923c; } /* v25: fijado a mano vía "⏸ Suspender OT" (pestaña "Informe de ejecución") */
/* Estas ya NO corresponden a ningún estado de OT (se dieron de baja en la
   revisión v16), pero se reutilizan como colores de badge genéricos en
   otros módulos no relacionados: Solicitudes de compra (todos sus estados),
   Inventario (stock bajo / pendiente recepción). */
.cmms-wost-Ppto-Rechazado      { background: rgba(220,38,38,.15); color: #f87171; }
.cmms-wost-Informe-Cargado     { background: rgba(37,99,235,.15); color: #60a5fa; }
.cmms-wost-Pendiente-Aprobación{ background: rgba(217,119,6,.16); color: #fbbf24; }
.cmms-wost-Pagada              { background: rgba(22,163,74,.16); color: #4ade80; }
.cmms-wost-Anulada             { background: rgba(75,85,99,.25);  color: #cbd5e1; text-decoration:line-through; }
/* Estados de OT Preventiva (tabla preventive_wos) — módulo separado de las
   OT correctivas de arriba. */
.cmms-wost-Activa              { background: rgba(22,163,74,.16); color: #4ade80; }
.cmms-wost-Pausada             { background: rgba(75,85,99,.25);  color: #cbd5e1; }
/* Estados de OT Preventiva GENERADA (tabla preventive_wo_generated) — desde
   la unificación de vocabulario reutilizan las MISMAS clases de arriba
   (.cmms-wost-P-Asignar / .cmms-wost-P-Ejecutar / .cmms-wost-P-Cierre /
   .cmms-wost-Cerrada), ver preventiveGeneratedListHTML()/
   preventiveGeneratedDetailView() en app.js — nada propio que definir acá. */
.cmms-hint { display:block; margin-top:4px; font-size:11px; color:#f59e0b; }

/* RPN */
.cmms-rpn { font-size: 13px; font-weight: 600; padding: 8px 12px; border-radius: var(--radius); background: var(--c-surface-2); border: 1px solid var(--c-border); margin-top: 8px; color: var(--c-text); }
.cmms-rpn.high { background:rgba(239,68,68,.15); color:#f87171; }
.cmms-rpn.med  { background:rgba(245,158,11,.15); color:#fbbf24; }
.cmms-rpn.low  { background:rgba(34,197,94,.15); color:#4ade80; }
.cmms-rpn-cell { padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; color: #fff; }
.cmms-rpn-cell.high { background:#dc2626; }
.cmms-rpn-cell.med  { background:#d97706; }
.cmms-rpn-cell.low  { background:#16a34a; }

/* Form */
.cmms-form { max-width: 900px; }
/* Pestaña "Tareas de mantenimiento" del Plan de Mantenimiento Preventivo (ver
   preventiveFormView() en app.js): ensancha SOLO esa pestaña (clase agregada
   condicionalmente al <form>, solo cuando step===1) para que quepan, lado a
   lado, el editor de tareas y su vista previa en vivo (#prevwo-preview) sin
   quedar apretados dentro del ancho normal de 900px del resto del formulario. */
.cmms-form-wide { max-width: 1400px; }
/* v42 (Fase 5) — editor de tareas + vista previa en vivo del Plan de
   Mantenimiento Preventivo (pestaña "Tareas de mantenimiento", ver
   preventiveFormView() en app.js): antes eran divs con estilos inline
   (flex:3/min-width:520px y flex:2/min-width:340px) — en celular esos
   min-width fijos superaban el ancho real de la pantalla y forzaban scroll
   horizontal de toda la pestaña en vez de apilarse. Se movieron a estas
   clases para poder anular esos min-width por debajo de 900px (mismo
   corte que el resto de los ajustes de tablet/celular, ver más abajo). */
.cmms-prevwo-split { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; }
.cmms-prevwo-editor { flex: 3; min-width: 520px; }
.cmms-prevwo-preview { flex: 2; min-width: 340px; position: sticky; top: 12px; }
@media (max-width: 900px) {
  .cmms-prevwo-editor, .cmms-prevwo-preview { flex: 1 1 100%; min-width: 0; }
  .cmms-prevwo-preview { position: static; }
}
.cmms-form-section { background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 16px; margin-bottom: 14px; box-shadow: var(--c-panel-shadow); }
.cmms-form-section h3 { font-size: 12px; font-weight: 600; margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--c-border); color: var(--c-muted); text-transform: uppercase; letter-spacing: .5px; }
.cmms-iso-tag { display: inline-block; font-size: 10px; padding: 2px 7px; border-radius: 4px; background: rgba(59,130,246,.15); color: #93c5fd; border: 1px solid rgba(59,130,246,.3); margin-bottom: 10px; font-weight: 500; }
/* Sección "Taxonomía activo" del formulario de Activos: dos columnas — el
   resto de los campos a la izquierda, el TAG del equipo a la derecha en su
   propio recuadro (25-jul-2026: antes había una imagen de referencia acá,
   se sacó a pedido del usuario). */
.cmms-taxonomy-cols { display: grid; grid-template-columns: 1fr 280px; gap: 24px; align-items: start; }
.cmms-taxonomy-fields { min-width: 0; }
.cmms-taxonomy-side { background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 14px; position: sticky; top: 8px; }
@media (max-width: 900px) {
  .cmms-taxonomy-cols { grid-template-columns: 1fr; }
  .cmms-taxonomy-side { position: static; }
}
/* Especialidad de mantenimiento del activo (multiSelectFieldHtml): filas
   apilables, mismo criterio que .cmms-multival (N7/N8) pero eligiendo de un
   catálogo en vez de texto libre — ver bindMultiSelectFields() en app.js. */
.cmms-multiselect-rows { margin-top: 4px; }
/* v52 — Selector propio de Especialidad (reemplaza al <select> nativo de
   cada fila): botón "trigger" con el mismo look que un <select>/<input> del
   resto de la app, y un panel flotante propio (.cmms-specialty-menu) con
   cada opción + su botón 🗑️ para borrarla del catálogo compartido — ver
   openSpecialtyMenu()/showDeleteSpecialtyModal() en app.js. */
.cmms-specialty-picker { position: relative; }
.cmms-specialty-trigger {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 7px 9px; border: 1px solid var(--c-border); border-radius: var(--radius);
  font-size: 13px; font-family: inherit; color: #0f1723; background: #dfe6f0; cursor: pointer; text-align: left;
}
.cmms-specialty-trigger:hover { border-color: var(--c-accent); }
.cmms-specialty-trigger-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmms-specialty-trigger-arrow { color: #5b6577; font-size: 11px; flex-shrink: 0; }
/* v67 — a pedido del usuario: las Especialidades pasan a verse como
   píldoras/chips (misma pinta que .cmms-chip-list de "Equipos") en vez de
   una fila con forma de <select> — ver specialtyRowHTML()/multiSelectFieldHtml()
   en app.js. El trigger que abre openSpecialtyMenu (catálogo compartido,
   agregar/eliminar) sigue siendo el mismo botón por dentro; acá solo se le
   saca el look de <select> (borde/fondo/ancho completo) para que se vea
   integrado DENTRO de la píldora que ya trae ese fondo/borde. */
.cmms-chip .cmms-specialty-trigger {
  width: auto; border: none; background: transparent; padding: 0; font-size: 12.5px; font-weight: 600;
}
.cmms-chip .cmms-specialty-trigger-label { max-width: 220px; }
/* v93 (pedido del usuario, wizard de OT) — .cmms-specialty-row-primary es la
   fila obligatoria cuando multiSelectFieldHtml() se llama con
   opts.primaryAsSelect (ver app.js) — vuelve a verse como un <select>
   normal (ancho completo, blanco, rectangular) en vez de píldora, y ocupa
   todo el ancho del renglón (flex-basis:100%) para que las píldoras
   opcionales de abajo ("+ Agregar otra especialidad") queden en su propia
   línea, debajo — mismo patrón "menú arriba, píldoras abajo" que
   #wo-closer-field. */
.cmms-specialty-row-primary { flex-basis: 100%; }
.cmms-specialty-row-primary .cmms-specialty-picker { width: 100%; }
.cmms-specialty-menu {
  position: fixed; z-index: 1005; max-height: 320px; overflow-y: auto;
  /* v52c — antes usaba el mismo fondo oscuro con degradé de los paneles
     flotantes (notificaciones/usuario). El usuario notó que este
     desplegable en particular queda visualmente distinto al resto de
     listas del formulario (los <select> nativos de Industria/Categoría/
     etc., que el navegador pinta con fondo claro/plano) — como este
     picker reemplaza justo a UNO de esos <select> nativos en la misma
     fila del formulario, el contraste llamaba la atención. Se cambia a
     fondo claro liso + resalte suave (en vez de azul sólido), igual
     criterio que .cmms-field select/.cmms-specialty-trigger. */
  background: #fff; border: 1px solid var(--c-border);
  border-radius: var(--radius); box-shadow: 0 4px 14px rgba(15,23,35,.18); padding: 4px;
  /* v52b — el panel se ensancha solo hasta el texto más largo (antes
     quedaba fijo al ancho del trigger, angosto, y truncaba nombres largos
     como "Redes / Telecomunicaciones / TI") — min-width lo puso el JS
     (openSpecialtyMenu) para no verse angosto si todo es corto. Tope en
     min(...) para no ocupar media pantalla si alguien carga un nombre
     kilométrico. */
  width: max-content; max-width: min(480px, 90vw);
}
.cmms-specialty-menu-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; border-radius: 4px; cursor: pointer; font-size: 13px; color: #0f1723;
}
.cmms-specialty-menu-item:hover { background: rgba(59,130,246,.14); color: #0f1723; }
.cmms-specialty-menu-item:hover .cmms-specialty-menu-item-del { opacity: 1; }
.cmms-specialty-menu-item-active { font-weight: 600; background: rgba(59,130,246,.08); }
/* v70 (30-jul-2026) — a pedido del usuario: opción ya elegida en OTRA fila
   del mismo widget (ver openSpecialtyMenu() en app.js) — gris, no
   clickeable (sin cursor pointer ni hover azul), para evitar duplicados.
   El 🗑️ de borrado del catálogo sigue funcionando igual (no depende de
   esto). */
.cmms-specialty-menu-item-disabled { color: var(--c-muted); opacity: .5; cursor: default; }
.cmms-specialty-menu-item-disabled:hover { background: none; color: var(--c-muted); }
/* v52c — el ancho de esta columna ya NO se define en %/flex acá: lo fija
   openSpecialtyMenu() en JS, en px, midiendo la fila de texto más ancha
   del listado y aplicando ESE mismo ancho (+30% de aire) a TODAS las
   filas — así el 🗑️ queda siempre en la misma columna/línea vertical sin
   importar cuánto texto tenga cada especialidad (pedido explícito del
   usuario: "considera la fila con el mayor ancho para darle formato al
   resto de filas"). Acá solo queda lo que no depende de esa medición. */
.cmms-specialty-menu-item-label { white-space: nowrap; flex-shrink: 0; }
.cmms-specialty-menu-item-del {
  background: none; border: none; cursor: pointer; font-size: 13px; padding: 2px 4px; border-radius: 4px;
  opacity: .55; flex-shrink: 0; line-height: 1; margin-left: auto;
}
.cmms-specialty-menu-item-del:hover { opacity: 1; background: rgba(239,68,68,.25); }
.cmms-specialty-menu-addnew { color: var(--c-accent); font-weight: 600; border-top: 1px solid var(--c-border); margin-top: 2px; padding-top: 9px; }
.cmms-specialty-menu-addnew:hover { background: rgba(59,130,246,.14); color: var(--c-accent); }
.cmms-field-row { display: grid; grid-template-columns: repeat(auto-fit,minmax(180px,1fr)); gap: 10px; margin-bottom: 10px; }
/* v65 (25-jul-2026) — a pedido del usuario: sub-tarjetas dentro de un mismo
   paso del wizard (ej. el paso "General" de la OT Preventiva), para agrupar
   visualmente campos relacionados (Título/Especialidad, Programación de la
   mantención, Vista anual) en vez de una lista plana — mismo espíritu que
   .cmms-form-section pero un nivel más adentro, por eso el fondo es más
   sutil (var(--c-panel-2) en vez del degradé de .cmms-form-section). */
.cmms-prevwo-subsection { background: var(--c-panel-2); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 14px; }
.cmms-prevwo-subsection > .cmms-field:last-child,
.cmms-prevwo-subsection > .cmms-field-row:last-child { margin-bottom: 0; }
.cmms-prevwo-subsection h4 { margin: 0 0 12px; font-size: 12px; font-weight: 600; letter-spacing: .5px; text-transform: uppercase; color: var(--c-muted); padding-bottom: 8px; border-bottom: 1px solid var(--c-border); }
/* SIMPLIFICACIÓN SISTEMA (19-ago-2026, pedido del usuario) — "Trazabilidad"
   y "Título y especialidad" del Plan de Mantenimiento Preventivo quedaban
   con el fondo gris de .cmms-prevwo-subsection (var(--c-panel-2)), distinto
   del diseño que ya usan las tarjetas "Mantenimiento interno"/"Proveedor
   externo" del mismo wizard (.cmms-field con solo borde, sin fondo propio —
   ver preventiveFormView() en app.js). Modificador que anula SOLO el fondo,
   dejando el resto (borde/radio/padding/h4) igual — se aplica nada más a
   esos dos bloques puntuales, no a los demás .cmms-prevwo-subsection de
   este mismo wizard (Programación de la mantención, Vista anual, etc.),
   que conservan el fondo a propósito. */
.cmms-prevwo-subsection--plain { background: none; }
.cmms-field { display: flex; flex-direction: column; gap: 4px; }
.cmms-field label { font-size: 11px; color: var(--c-text); font-weight: 600; letter-spacing: .2px; }
.cmms-field .req { color: var(--c-danger); }
.cmms-field input, .cmms-field select, .cmms-field textarea { padding: 7px 9px; border: 1px solid var(--c-border); border-radius: var(--radius); font-size: 13px; font-family: inherit; color: #0f1723; background: #dfe6f0; width: 100%; }
.cmms-field input::placeholder, .cmms-field textarea::placeholder { color: #5b6577; }
.cmms-field select { color: #0f1723; }
.cmms-field input:focus, .cmms-field select:focus, .cmms-field textarea:focus { outline: none; border-color: var(--c-accent); box-shadow: 0 0 0 2px rgba(59,130,246,.35); }
.cmms-field input[readonly], .cmms-field input:disabled { background: rgba(255,255,255,.65); color: #4b5563; }
.cmms-field select:disabled, .cmms-field textarea:disabled { background: rgba(255,255,255,.65); color: #4b5563; cursor: not-allowed; }
.cmms-wizard-step button:disabled { opacity: .45; cursor: not-allowed; }
.cmms-field textarea { resize: vertical; line-height: 1.5; }
.cmms-form-actions { display: flex; gap: 10px; justify-content: flex-end; padding: 4px 0 16px; }

/* Wizard de pestañas (usado en el formulario de OT: Identificación > Análisis
   de falla > Solicitante y ejecución > Texto libre) */
.cmms-wizard-tabs { display: flex; gap: 2px; margin-bottom: 16px; border-bottom: 1px solid var(--c-border); overflow-x: auto; }
.cmms-wizard-tab {
  display: flex; align-items: center; gap: 8px; padding: 10px 16px 12px; border: none; background: none;
  color: var(--c-muted); font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap;
  border-bottom: 2px solid transparent; margin-bottom: -1px; font-family: inherit; transition: color .12s, border-color .12s;
}
.cmms-wizard-tab:hover { color: var(--c-text); }
.cmms-wizard-tab.active { color: var(--c-strong-text); border-bottom-color: var(--c-accent); }
.cmms-wizard-tab-num {
  display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%;
  border: 1px solid var(--c-border); font-size: 11px; font-weight: 600; color: var(--c-muted); flex-shrink: 0;
}
.cmms-wizard-tab.active .cmms-wizard-tab-num { background: var(--c-accent); border-color: var(--c-accent); color: #fff; }
.cmms-wizard-tab.done .cmms-wizard-tab-num { background: var(--c-success); border-color: var(--c-success); color: #fff; font-size: 0; }
.cmms-wizard-tab.done .cmms-wizard-tab-num::before { content: '✓'; font-size: 11px; }

/* Buttons */
.cmms-btn { padding: 7px 14px; border-radius: var(--radius); border: 1px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text); cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 5px; text-decoration: none; white-space: nowrap; }
.cmms-btn:hover { background: var(--c-btn-hover-bg); }
/* v46 — "6. Cierre de OT" (Avanzar/Retroceder): gris + sin click una vez que
   la OT queda "Cerrada", salvo que el usuario tenga "Permiso total OT
   Preventiva" — mismo criterio visual que .cmms-wizard-step button:disabled
   de más abajo, generalizado acá a CUALQUIER .cmms-btn (antes solo aplicaba
   dentro de un wizard). */
.cmms-btn:disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }
.cmms-btn-primary { background: var(--c-accent); color: #fff; border-color: var(--c-accent); }
.cmms-btn-primary:hover { background: var(--c-accent-h); }
.cmms-btn-danger { color: #f87171; border-color: rgba(239,68,68,.4); }
.cmms-btn-danger:hover { background: rgba(239,68,68,.12); }
/* v25: "⏸ Suspender OT" — mismo estilo "outline" que .cmms-btn-danger pero en
   ámbar/naranja, para distinguirlo visualmente de "⛔ Anular OT" (rojo, más
   destructivo/definitivo). */
.cmms-btn-warning { color: #ea580c; border-color: rgba(234,88,12,.4); }
.cmms-btn-warning:hover { background: rgba(234,88,12,.12); }
/* v26: "🔓 Reabrir OT" — estilo "outline" en verde, para distinguirlo de las
   acciones terminales (Anular/Suspender, en rojo/naranja): reabrir devuelve
   la OT al flujo normal, no la saca de él. */
.cmms-btn-success { color: #16a34a; border-color: rgba(22,163,74,.4); }
.cmms-btn-success:hover { background: rgba(22,163,74,.12); }
.cmms-btn-sm { padding: 3px 9px; font-size: 11px; }
/* Botones del topbar (🏠 Inicio / 🔄 Actualizar / 🚪 Salir) — el doble de
   grandes que un .cmms-btn normal y con fondo/borde más claros para que
   contrasten bien contra el degradé oscuro del topbar (ver .cmms-user-pill
   arriba, mismo criterio). */
.cmms-topbar-btn { padding: 12px 22px; font-size: 16px; font-weight: 600; gap: 8px; background: var(--c-topbar-btn-bg); border: 1px solid var(--c-topbar-btn-border); color: var(--c-topbar-btn-text); }
.cmms-topbar-btn:hover { background: var(--c-topbar-btn-hover-bg); border-color: var(--c-topbar-btn-hover-border); }
/* Actualizar / Salir — sin ícono, 20% más chicos que un .cmms-topbar-btn normal
   (12px 22px / 16px → ~10px 18px / 13px). */
.cmms-topbar-btn-compact { padding: 10px 18px; font-size: 13px; }

/* v83 — misma altura para TODOS los botones de la franja derecha del topbar
   (campana / Inicio / Actualizar / píldora de usuario): antes "Inicio" usaba
   .cmms-topbar-btn normal (12px 22px) y "Actualizar" usaba la variante
   -compact (10px 18px), así que quedaban de alturas distintas — pedido del
   usuario: parejo. Se define acá, con selector más específico (ámbito
   .cmms-topbar-right) para NO afectar los usos de .cmms-topbar-btn-compact
   fuera del topbar (botones de encabezado de otras pantallas, como el modo
   TV o "Generar reporte de gestión", que siguen con su tamaño propio). */
.cmms-topbar-right .cmms-topbar-btn,
.cmms-topbar-right .cmms-user-pill {
  height: 40px; box-sizing: border-box; padding: 0 16px; font-size: 14px;
  font-weight: 600; border-radius: 10px; display: inline-flex; align-items: center; gap: 8px;
}
.cmms-topbar-right .cmms-user-pill { border-radius: 999px; padding: 0 16px 0 14px; }
.cmms-topbar-right .cmms-notif-bell { padding: 0; width: 40px; justify-content: center; }
.cmms-topbar-right .cmms-user-menu-caret { margin-left: -2px; }
/* Reloj/calendario del topbar normal (20-ago-2026, pedido explícito del
   usuario: "el reloj y calendario que utilizamos en el dashboard pantalla
   completa, deberiamos utilizarlo a la izquierda de las notificaciones pero
   que se ajuste el tamaño a la altura de los demas iconos de la fila") —
   mismos chips .cmms-tv-chip/.cmms-tv-clock-group del modo TV (ver
   comentario junto a esa clase más abajo en este archivo), reescalados acá
   para calzar con los 40px del resto de esta franja en vez de su tamaño
   grande pensado para verse desde lejos en una TV. */
.cmms-topbar-clock-group .cmms-tv-chip {
  height: 40px; box-sizing: border-box; font-size: 13px; padding: 0 12px;
  border-radius: 10px;
}
/* "Inicio" queda siempre visible en el topbar (pedido del usuario) en vez de
   desaparecer al estar ya en el Dashboard — para no dejarlo clickeable sin
   hacer nada se deshabilita visualmente en ese caso, en vez de esconderse. */
.cmms-topbar-btn:disabled { opacity: .45; cursor: default; pointer-events: none; }

/* Loading */
.cmms-loading { display: flex; align-items: center; gap: 10px; padding: 40px; color: var(--c-muted); font-size: 15px; }
.cmms-spinner { width: 20px; height: 20px; border: 2px solid var(--c-border); border-top-color: var(--c-accent); border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Messages — v3 (07-ago-2026, pedido explícito del usuario): "cuando aparece
   y desaparece el mensaje hace que se mueva toda la RG/OT". Antes vivía en
   el flujo normal del documento (margin-bottom empujaba todo lo de abajo),
   insertado condicionalmente al tope de varias plantillas (ver msgBanner()
   en public/app.js) — aparecer/desaparecer producía un salto de layout
   visible en cualquier vista, no solo RG. Se saca del flujo con position:
   fixed (mismo criterio que un toast estándar): ahora flota sobre el
   contenido sin desplazar nada debajo, sin tocar cada punto donde se
   inserta msgBanner(). */
/* 08-ago-2026 (pedido del usuario) — antes quedaba fijo arriba a la
   derecha, tapando la píldora del usuario. Ahora va centrado en la MISMA
   fila del topbar que el logo "R1-CMMS" (el topbar mide 72px de alto fijo
   en desktop, ver .cmms-topbar más arriba — top:18px lo deja centrado ahí
   verticalmente), columna central, sin mover nada del contenido de abajo
   (sigue siendo position:fixed, un overlay puro). */
.cmms-msg { position: fixed; top: 18px; left: 50%; transform: translateX(-50%); z-index: 9999; max-width: 420px; padding: 10px 16px; border-radius: var(--radius); font-size: 13px; font-weight: 500; box-shadow: 0 4px 16px rgba(0,0,0,.25); background: var(--c-surface); text-align: center; }
.cmms-msg-success { background: rgba(34,197,94,.12); color: #4ade80; border: 1px solid rgba(34,197,94,.35); }
.cmms-msg-error   { background: rgba(239,68,68,.12); color: #f87171; border: 1px solid rgba(239,68,68,.35); }

/* Aviso de lista cortada por el tope de seguridad (04-sep-2026, PERF-01 — ver
   src/util/listCap.js). Inline dentro de la lista, no overlay: describe lo que
   se está mirando en ese momento, así que tiene que quedarse ahí mientras se
   mira, no desaparecer solo como .cmms-msg. Ámbar y no rojo: no es un error,
   es información sobre el alcance de lo mostrado. */
.cmms-list-capped {
  margin: 0 0 12px; padding: 10px 14px; border-radius: var(--radius);
  font-size: 13px; line-height: 1.45;
  background: rgba(245,158,11,.10); color: #f59e0b;
  border: 1px solid rgba(245,158,11,.35);
}
.cmms-list-capped strong { color: inherit; font-weight: 600; }

/* Empty */
.cmms-empty { color: var(--c-muted); font-size: 13px; text-align: center; padding: 20px; }

/* ── Pantalla de login: dos columnas (panel oscuro + formulario) ── */
#cmms-login-screen { position: fixed; inset: 0; display: flex; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; z-index: 500; }

/* Panel izquierdo: marca + imagen de fondo (opcional) + texto informativo.
   Para poner una foto real, guardá un archivo public/login-bg.jpg — el
   degradado queda arriba para que el texto blanco siga siendo legible. */
#cmms-login-left {
  flex: 1 1 45%; min-width: 320px; box-sizing: border-box;
  padding: 56px 48px; color: #fff;
  display: flex; flex-direction: column; justify-content: space-between;
  background:
    linear-gradient(135deg, rgba(15,23,42,.94), rgba(15,23,42,.80)),
    url('/login-bg.jpg') center/cover no-repeat,
    linear-gradient(135deg, #0f172a, #1e293b);
}
#cmms-brand-title { font-size: 64px; font-weight: 800; margin: 0; line-height: 1; letter-spacing: -1px; }
#cmms-brand-subtitle { font-size: 24px; font-weight: 600; margin: 10px 0 0; max-width: 420px; line-height: 1.3; color: #e2e8f0; }
#cmms-info-footer { font-size: 13px; color: #cbd5e1; max-width: 440px; }
#cmms-info-label { font-weight: 700; letter-spacing: .04em; margin: 0 0 6px; font-size: 11px; color: #94a3b8; text-transform: uppercase; }
#cmms-info-text { margin: 0; line-height: 1.55; }

/* Panel derecho: formulario */
#cmms-login-right { flex: 1 1 55%; display: flex; align-items: center; justify-content: center; background: #fff; padding: 24px; box-sizing: border-box; }
#cmms-login-root { width: 100%; max-width: 340px; }

.cmms-login-card { display: flex; flex-direction: column; gap: 16px; }
.cmms-login-field { display: flex; flex-direction: column; gap: 6px; font-size: 15px; font-weight: 600; color: #1a1a1a; }
.cmms-login-field input { font-size: 14px; font-weight: 400; padding: 11px 14px; border: 1px solid #e0e0e0; border-radius: 8px; font-family: inherit; background: #fff; color: #1a1a1a; }
.cmms-login-field input:focus { outline: none; border-color: var(--c-accent); box-shadow: 0 0 0 3px rgba(59,130,246,.12); }
.cmms-login-submit { width: 100%; justify-content: center; padding: 11px; font-size: 14px; border-radius: 8px; margin-top: 4px; }
.cmms-login-error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; border-radius: var(--radius); padding: 8px 14px; font-size: 13px; margin: 0; }

/* Error de submit mostrado DENTRO del formulario (ver showFormError en app.js) */
/* v65 (25-jul-2026) — white-space:pre-line: algunos mensajes de error ahora
   vienen con varias líneas (\n), ej. la lista de tareas sin completar del
   informe de ejecución (ver submitPreventiveGenReport()) — sin esto, un
   <p> normal colapsa los saltos de línea en espacios y todo queda en una
   sola línea corrida. No afecta a los mensajes de una sola línea de siempre. */
/* v69 — BUGFIX reportado: el banner aparecía/desaparecía del flujo del
   documento (se creaba/sacaba del DOM), corriendo todo el formulario de
   abajo cada vez — sobre todo notorio en validaciones que se repiten varias
   veces seguidas (ej. "＋ Agregar técnico"). Ahora SIEMPRE reserva el mismo
   alto (min-height, fijo con o sin texto) y el color/borde/fondo solo
   aparecen con la clase --visible (ver clearFormError()/showFormError() en
   app.js, que ya no sacan el elemento del DOM, solo lo vacían) — transparente
   y "colapsado" a min-height mientras no hay error, sin mover el resto. */
.cmms-form-error-banner {
  background: transparent; color: transparent; border: 1px solid transparent; border-radius: var(--radius);
  padding: 10px 14px; font-size: 13px; margin: 0 0 16px; white-space: pre-line; min-height: 38px;
  transition: background-color .15s, color .15s, border-color .15s;
}
.cmms-form-error-banner--visible { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
.cmms-field-error { background: rgba(220,38,38,.08) !important; border-color: #dc2626 !important; transition: background .2s, border-color .2s; }
.cmms-forgot-msg { font-size: 12px; color: #666; margin: -6px 0 0; }

.cmms-link-btn { background: none; border: none; color: #666; font-size: 13px; cursor: pointer; font-family: inherit; text-align: center; padding: 2px; }
.cmms-link-btn:hover { color: var(--c-accent); text-decoration: underline; }

/* ── Modal base (usada por popups DENTRO de la app ya logueada — Anular/
   Suspender/Reabrir OT, "+ Agregar nuevo…" de catálogos/taxonomía,
   buscador de instalación, confirmar cierre de OT, etc.) — se mantiene
   claro/blanco a propósito por defecto, fijo, sin importar el tema
   oscuro/intermedio/claro de la app (que recién aplica una vez logueado, ver
   .cmms-modal-card-themed más abajo). ── */
.cmms-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; z-index: 1000; }
/* v42 (Fase 0): max-width:92vw agregado — antes el ancho fijo de 320px se
   cortaba (u obligaba a scroll horizontal) en celulares angostos de
   ~320-360px reales de viewport, porque 320px + el padding propio del
   popup (28px por lado) ya superaban ese ancho. Mismo criterio que ya usa
   .cmms-modal-wide más abajo. */
.cmms-modal-card { position: relative; background: #fff; color: #1a1a1a; border-radius: var(--radius); padding: 28px; width: 320px; max-width: 92vw; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
.cmms-modal-card h2 { margin: 0 0 16px; font-size: 18px; color: #1a1a1a; }
.cmms-modal-card form { display: flex; flex-direction: column; gap: 10px; }
.cmms-modal-card input { padding: 8px 10px; border: 1px solid #e0e0e0; border-radius: var(--radius); font-size: 14px; font-family: inherit; background: #fff; color: #1a1a1a; }
.cmms-modal-card .cmms-btn { justify-content: center; margin-top: 4px; }
.cmms-modal-close { position: absolute; top: 10px; right: 12px; background: none; border: none; font-size: 16px; cursor: pointer; color: #666; }

/* Variante "themed" de .cmms-modal-card, para los popups chicos DENTRO de la
   app ya logueada (Anular/Suspender/Reabrir OT, "+ Agregar nuevo…" de
   catálogos y de taxonomía de Activos, buscador de instalación, confirmar
   cierre de OT) — antes reusaban sin querer la misma clase blanca fija del
   login de arriba, lo que se notaba poco mientras el contenido era solo
   texto (el navegador podía llegar a "forzar oscuro" un popup blanco de
   forma pareja), pero se volvió evidente al agregar un elemento que SÍ sigue
   el tema real (la lista de valores existentes de
   showAddTaxonomyValueModal(), que usa las mismas variables oscuras que el
   resto de la app) al lado de un fondo de popup que seguía fijo en blanco:
   dos oscuros distintos, uno "real" y uno "forzado", que no calzaban entre
   sí. Con esta variante el popup entero sigue las mismas variables de tema
   (--c-surface/--c-text/--c-border) que ya usa .cmms-modal-wide, así no hay
   ningún fondo fijo con el que choque. */
/* Ojo: a propósito NO se pisa el color de los <input>/<select> acá — esos ya
   los pinta `.cmms-field input`/`.cmms-field select` (ver más arriba) con el
   mismo fondo claro que usa CUALQUIER formulario de la app (asset-form,
   wizard de OT, etc.), oscuro o no: es la convención ya establecida para
   campos de formulario, y hay que mantenerla igual acá para que el popup no
   desentone con el resto de los formularios. */
.cmms-modal-card.cmms-modal-card-themed { background: var(--c-surface); color: var(--c-text); border: 1px solid var(--c-border); }
.cmms-modal-card.cmms-modal-card-themed h2 { color: var(--c-text); }
.cmms-modal-card.cmms-modal-card-themed .cmms-modal-close { color: var(--c-muted); }

/* Modal ancho para historiales (OT / OC / Inventario) — se usa DENTRO de la app
   ya logueada, por eso va en tema oscuro (a diferencia del modal de login,
   que se mantiene claro a propósito). */
.cmms-modal-wide { width: 640px; max-width: 92vw; max-height: 80vh; overflow-y: auto; background: var(--c-surface); color: var(--c-text); border: 1px solid var(--c-border); }
.cmms-modal-wide h2 { color: var(--c-text); }
.cmms-modal-wide .cmms-modal-close { color: var(--c-muted); }
.cmms-modal-wide table { margin-top: 4px; }

/* Buscador de equipo (showAssetSearchModal, "Buscar equipo"/"Buscar y
   seleccionar equipos") e instalación (showInstallationSearchModal, mismo
   estilo de tarjeta) — la tabla de resultados tiene 6 columnas (TAG/Nombre/
   Clase equipo/Planta-sección/Ubicación geográfica/acción) que no entraban
   en el ancho de .cmms-modal-wide (640px): el contenido se recortaba en vez
   de mostrarse completo (ej. el botón "Seleccionar" quedaba cortado como
   "Se..."). Esta variante solo ensancha ESTE popup — no se toca
   .cmms-modal-wide porque la reusan otros popups más chicos (historiales,
   "+ Agregar nuevo…", etc.) que no necesitan tanto ancho. */
.cmms-modal-asset-search { width: 980px; max-width: 95vw; }
@media (max-width: 720px) {
  .cmms-modal-asset-search { width: 95vw; }
}
/* 06-ago-2026 (pedido del usuario, con captura) — el popup entero heredaba
   "max-height:80vh; overflow-y:auto" de .cmms-modal-wide: con los filtros +
   pestañas + grilla + el botón "Agregar equipos seleccionados" (modo multi)
   todos apilados, el popup se volvía más alto que la pantalla y HABÍA que
   scrollear TODO el popup para llegar al botón — quedaba fuera de vista.
   Pedido explícito: el popup en sí NO debe necesitar scroll, solo la grilla
   de resultados de adentro. Se logra con flex-column + altura fija
   (max-height/height acotadas al viewport) + overflow:hidden en el popup —
   los filtros/pestañas/footer quedan con su alto natural (flex-shrink:0
   implícito) y SOLO el bloque de resultados (flex:1 + min-height:0, ver
   .cmms-search-modal-results más abajo) se estira/encoge para llenar el
   espacio sobrante y scrollear ahí adentro si hace falta. El botón de
   confirmar (.cmms-form-actions, último hijo) queda siempre visible porque
   es el último ítem del flex-column, después del bloque que se encoge. */
.cmms-modal-asset-search {
  display: flex; flex-direction: column;
  max-height: min(88vh, 760px); overflow: hidden;
}
.cmms-modal-asset-search > * { flex: 0 0 auto; }
.cmms-search-modal-results { flex: 1 1 auto; min-height: 80px; }
.cmms-modal-asset-search .cmms-form-actions { flex-shrink: 0; }

/* Popup "¿Qué tipo de OT quiere crear?" (botón "+ Nueva OT") — dos tarjetas
   grandes, una por tipo de OT (Correctiva / Preventiva), ver
   showNewWoTypeModal() en app.js. */
.cmms-wo-type-options { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
.cmms-wo-type-option {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  width: 100%; text-align: left; padding: 14px 16px; border-radius: var(--radius);
  border: 1px solid var(--c-border); background: rgba(255,255,255,.03);
  color: var(--c-text); cursor: pointer; font-family: inherit; transition: background .12s, border-color .12s;
}
.cmms-wo-type-option:hover { background: rgba(59,130,246,.12); border-color: var(--c-accent); }
.cmms-wo-type-option-icon { font-size: 20px; }
.cmms-wo-type-option-label { font-size: 14px; font-weight: 700; }
.cmms-wo-type-option-desc { font-size: 12px; color: var(--c-muted); }
/* Opción sin permiso — opaca y sin interacción (ver showNewWoTypeModal()). */
.cmms-wo-type-option-disabled,
.cmms-wo-type-option-disabled:hover {
  opacity: .45; cursor: not-allowed; background: rgba(255,255,255,.03); border-color: var(--c-border);
}

/* En pantallas angostas, apilar en vez de dos columnas */
@media (max-width: 720px) {
  #cmms-login-screen { flex-direction: column; overflow-y: auto; position: static; min-height: 100vh; }
  #cmms-login-left { flex: none; padding: 36px 28px; }
  #cmms-login-right { flex: none; padding: 32px 24px; }
}

/* .perm-flow-empty (28-jul-2026: se sacó el "Diagrama de flujo — Roles →
   Permisos → Usuarios" al pie de la pantalla Permisos, sin uso — el resto
   de las clases .perm-flow-*/.perm-chip que eran exclusivas de ese diagrama
   se eliminaron con él) — esta clase queda porque también la usan las
   listas de adjuntos vacías (ver ".perm-flow-empty" en app.js, secciones de
   adjuntos de OT/Activos), aunque el nombre ya no tenga que ver con
   "flow". */
.perm-flow-empty { font-size: 11px; color: var(--c-muted); font-style: italic; }

/* Pantalla Permisos — tarjetas de rol con toggles (reemplaza la vieja tabla
   de una fila por rol y una columna por cada uno de los ~13 permisos, que
   obligaba a scrollear horizontal y era difícil de leer). Cada rol es una
   tarjeta con sus permisos agrupados por tema (ver PERM_ATTR_GROUPS en
   app.js) y un interruptor (switch) en vez de un checkbox pelado — mismo
   input[type=checkbox] con data-role/data-attr por debajo, así el resto de
   la lógica de guardado no cambia. */
/* Layout en grilla de 4 columnas FIJAS (02-sep-2026, revertido el mismo día — se probó mampostería/masonry con column-width y el usuario la descartó con captura: al reordenar tarjetas por columna en vez de por fila, con varias tarjetas expandidas a la vez el resultado se veía caótico/sin ningún criterio visual). Se vuelve a CSS Grid, pero con grid-template-columns FIJO en 4 (antes era auto-fill/minmax(300px,1fr), que ponía más o menos columnas según el ancho de pantalla) — así siempre son filas parejas de 4 roles, y a medida que se agregan roles nuevos simplemente se suman filas hacia abajo, en vez de reacomodar todo. Se mantiene align-items:start (fix del mismo día, previo a masonry): sin esto, CSS Grid estira por defecto cada tarjeta al alto de la más alta de su fila (comportamiento 'stretch') — con align-items:start cada tarjeta colapsada queda compacta; el único costo aceptado es que la FILA entera sigue siendo tan alta como la tarjeta expandida (deja hueco vacío debajo de las tarjetas cortas de esa fila hasta que empieza la fila siguiente) — se prefiere ese hueco predecible, dentro de una fila fija, antes que el reordenamiento caótico de masonry. */
.perm-role-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-top: 14px; align-items: start; }
.perm-role-card { background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 16px; box-shadow: var(--c-panel-shadow); display: flex; flex-direction: column; gap: 12px; }
/* Indicador de autoguardado de la matriz de Permisos (02-sep-2026) — junto al <h1>Permisos</h1>, ver setPermSaveStatus()/adminPermissionsView() en public/app.js. Vacío por defecto (sin toggles todavía). */
.perm-save-status { font-size: 12px; font-weight: 400; margin-left: 10px; vertical-align: middle; }
.perm-save-status-saving { color: var(--c-muted); }
.perm-save-status-saved { color: var(--c-success); }
.perm-save-status-error { color: var(--c-danger); }
.perm-role-card.perm-role-card-new { align-items: center; justify-content: center; border-style: dashed; background: rgba(59,130,246,.05); cursor: pointer; min-height: 120px; color: var(--c-muted); font-size: 13px; gap: 6px; transition: border-color .15s, color .15s; }
.perm-role-card.perm-role-card-new:hover { border-color: var(--c-accent); color: var(--c-text); }
.perm-role-card-new-icon { font-size: 26px; line-height: 1; color: var(--c-accent); }
.perm-role-card-header { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
/* Tarjeta de rol colapsable (02-sep-2026) — arranca colapsada, ver data-role-toggle en adminPermissionsView()/bindEvents() en public/app.js. El botón envuelve título+subtítulo, no todo el header (el botón Deshabilitar/Habilitar queda afuera, con su propio click). */
.perm-role-card-toggle { background: none; border: none; padding: 0; margin: 0; text-align: left; cursor: pointer; font: inherit; color: inherit; display: flex; align-items: center; justify-content: space-between; gap: 8px; flex: 1; min-width: 0; }
.perm-role-card-title-group { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.perm-role-card-toggle::after { content: '▾'; font-size: 11px; opacity: .6; flex-shrink: 0; transition: transform .15s; }
.perm-role-card.collapsed .perm-role-card-toggle::after { transform: rotate(-90deg); }
.perm-role-card-body { display: flex; flex-direction: column; gap: 12px; }
.perm-role-card.collapsed .perm-role-card-body { display: none; }
.perm-role-card-title { font-size: 15px; font-weight: 700; color: var(--c-text); }
.perm-role-card-sub { font-size: 11px; color: var(--c-muted); margin-top: 2px; }
.perm-group + .perm-group { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--c-border); }
/* Categorías de permisos "acordeón" (28-jul-2026, a pedido del usuario: la
   pantalla de Permisos mostraba TODOS los checkboxes de TODAS las
   categorías siempre abiertos, obligando a scrollear mucho para comparar
   roles). Colapsado por defecto (ver .collapsed abajo, aplicado en el HTML
   generado por adminPermissionsView()/showNewRoleModal() en app.js) — un
   clic en el título despliega SOLO esa categoría, dentro de esa tarjeta de
   rol; las demás categorías (de esa u otras tarjetas) no se ven afectadas.
   Puramente visual: no cambia qué checkboxes existen ni cómo se guardan. */
.perm-group-title { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .6px; color: #7fb0fb; margin-bottom: 8px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 8px; user-select: none; }
.perm-group-title::after { content: '▾'; font-size: 10px; opacity: .7; transition: transform .15s; }
.perm-group.collapsed .perm-group-title::after { transform: rotate(-90deg); }
.perm-group.collapsed .perm-group-title { margin-bottom: 0; }
.perm-group.collapsed .perm-toggle-list { display: none; }
.perm-toggle-list { display: flex; flex-direction: column; gap: 8px; }
.perm-toggle { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px; color: var(--c-text); }
.perm-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.perm-toggle-switch { position: relative; flex: 0 0 auto; width: 34px; height: 19px; border-radius: 20px; background: var(--c-surface-2); border: 1px solid var(--c-border); transition: background .15s, border-color .15s; }
.perm-toggle-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #8b96ac; transition: transform .15s, background .15s; }
.perm-toggle input:checked + .perm-toggle-switch { background: var(--c-accent); border-color: var(--c-accent); }
.perm-toggle input:checked + .perm-toggle-switch::after { transform: translateX(15px); background: #fff; }
.perm-toggle input:focus-visible + .perm-toggle-switch { box-shadow: 0 0 0 2px rgba(59,130,246,.4); }
.perm-toggle-label { line-height: 1.25; }
/* Hint de cada permiso (02-sep-2026, ajustado el mismo día) — popup con la
   característica principal del permiso. Arranca SIEMPRE pegado al puntero
   (pedido explícito del usuario: con el hover en toda la fila, un popup
   anclado con CSS al ícono del final quedaba lejos del cursor cuando se
   pasaba el mouse por la izquierda de la fila) — por eso deja de
   posicionarse con CSS (`position:absolute` relativo al ícono) y pasa a
   `position:fixed`, con `left`/`top` puestos por JS en cada `mousemove`
   sobre la fila (ver permHintBindMouseTracking() en public/app.js).
   `pointer-events:none` para que el popup nunca capture el mouse — así el
   `mousemove` que lo sigue jamás se corta al pasar el cursor por encima del
   popup mismo. Mismo lenguaje visual que .sim-gap-tooltip/.sgt-tip del
   Simulador de roles. */
.perm-toggle-hint { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; border-radius: 50%; font-size: 11px; line-height: 1; color: var(--c-muted); flex: 0 0 auto; cursor: help; }
.perm-toggle:hover .perm-toggle-hint, .perm-toggle-hint:focus-visible { color: var(--c-accent); outline: none; }
.perm-toggle-hint-tip { display: none; position: fixed; z-index: 9999; width: 240px; background: var(--c-text); color: var(--c-bg); font-family: var(--font-family); font-size: 13px; line-height: 1.45; padding: 9px 10px; border-radius: 7px; box-shadow: 0 8px 20px rgba(0,0,0,.25); font-weight: 500; text-align: left; pointer-events: none; }
.perm-toggle-hint-tip.perm-toggle-hint-tip--visible { display: block; }
.perm-fixed-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: -4px; }

/* Modal "Nuevo rol" — mismos grupos/switches que las tarjetas de arriba. */
.perm-role-modal-groups { display: flex; flex-direction: column; gap: 4px; max-height: 50vh; overflow-y: auto; margin-top: 6px; padding-right: 4px; }

/* Adjuntos de OT (pestaña "Informe de ejecución") */
.wo-attach-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; padding: 4px 10px; border-radius: 20px; background: var(--c-surface-2); border: 1px solid var(--c-border); color: var(--c-text); }
.wo-attach-chip a { color: #bcd3fb; text-decoration: underline; }
.wo-attach-chip small { color: var(--c-muted); }
.wo-attach-chip button { background: none; border: none; color: var(--c-danger); cursor: pointer; font-size: 12px; padding: 0 2px; }

/* Recuadro de estado en la esquina superior derecha de la OT (revisión v16:
   proceso de OT limpio, el estado se recalcula solo — ver woStatusBox en app.js) */
.cmms-wo-status-box { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.cmms-wo-status-box-label { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; color: var(--c-muted); white-space: nowrap; }

/* v92 (04-ago-2026, pedido del usuario) — plantilla igual al dibujo que
   mandó: UN recuadro grande (negro = la OT, .cmms-wo-wizard-card, mismo
   fondo/borde/sombra que .cmms-card) que "envuelve" su contenido alrededor
   de un recuadro chico y SEPARADO (rojo = foto+TAG+Estado,
   .cmms-wo-wizard-photo-slot, con su PROPIO fondo/borde) encajado en la
   esquina superior derecha. Dos intentos previos no calzaban con lo pedido:
   el primero (position:absolute) chocaba con los campos; el segundo (2
   columnas de igual jerarquía) dejaba la foto viéndose "aparte", no como
   una esquina recortada de la misma caja.
   Acá .cmms-wo-wizard-photo-slot usa float:right DENTRO de la tarjeta
   grande — el contenido de cada pestaña (título, campos) se acomoda/fluye
   alrededor de esa esquina en vez de superponerse, exactamente como "la
   información de la OT se comprime y ordena dentro del recuadro negro,
   dejando un margen con esa forma" (pedido explícito). Vive fuera de los
   <div data-step="…">, así que la foto se mantiene fija en el mismo lugar
   en las 4 pestañas del ciclo de vida de la OT. Las .cmms-form-section de
   cada paso pierden su propio fondo/borde acá adentro (si no, quedaría una
   tarjeta dentro de otra tarjeta). */
.cmms-wo-wizard-card { background: linear-gradient(160deg, var(--c-panel-1), var(--c-panel-2)); border: 1px solid var(--c-border); border-radius: var(--radius); box-shadow: var(--c-panel-shadow); padding: 16px; }
/* El tema "glass 3D" (más abajo en este archivo) le pone backdrop-filter:
   blur(...) a TODA .cmms-form-section, sin importar el fondo — con
   background:none pero el blur activo, el resultado era una mancha borrosa
   con el color de la foto flotante "sangrando" a través (se vio en una
   captura del usuario). Se anula acá el backdrop-filter (además del fondo/
   borde propio) para que estas tarjetas internas queden 100% transparentes
   de verdad, sin desenfocar lo que tienen detrás. */
.cmms-wo-wizard-card .cmms-form-section { background: none; border: none; box-shadow: none; padding: 0; margin-bottom: 0; backdrop-filter: none; -webkit-backdrop-filter: none; }
/* 06-ago-2026 (reporte del usuario, con captura): en tema claro,
   html[data-cmms-theme="light"] .cmms-form-section (más arriba en este
   archivo) le vuelve a poner "border:1px solid #e2e8f0" a .cmms-form-section
   — y como esa regla tiene MÁS especificidad que la de acá arriba (el
   atributo [data-cmms-theme="light"] cuenta como una clase extra), le gana
   y el borde reaparece pegado al título/costados de cada pestaña de la OT
   (se ve como una línea suelta encima de "Solicitante e identificación" y
   compañía). Se anula acá con el mismo prefijo de tema para igualar
   especificidad y ganar por orden (esta regla queda después en el archivo). */
html[data-cmms-theme="light"] .cmms-wo-wizard-card .cmms-form-section { border: none; background: none; }
.cmms-wo-wizard-card .cmms-form-section.cmms-wo-photo-clearance,
.cmms-wo-wizard-card .cmms-card.cmms-wo-photo-clearance { backdrop-filter: none; -webkit-backdrop-filter: none; }
/* v96 (pedido del usuario) — la píldora de Estado se sacó de acá adentro:
   ahora es .cmms-wo-status-pill, independiente, flotando arriba del <h3>
   de cada pestaña (ver woStatusPillSnippet en app.js) — la línea del
   título (con su borde inferior de siempre) queda separándola de la foto.
   Esta caja pasa a vivir DENTRO de cada <div data-step="…"> (una vez por
   pestaña) en vez de una sola vez afuera — mismo look, mismo criterio de
   "no se superpone con nada" (float:right + .cmms-wo-photo-clearance en
   las filas que la rodean). */
.cmms-wo-wizard-photo-slot {
  float: right; display: flex; flex-direction: column; gap: 8px; align-items: center;
  width: 150px; margin: 0 0 14px 18px; padding: 10px;
  background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: 10px;
}
/* v97 (pedido del usuario) — un poco de margen a los costados de la foto
   dentro de su caja (antes la imagen ocupaba el ancho completo, pegada al
   borde — se notaba el fondo celeste del placeholder/objeto como una
   "línea" pegada a ambos costados). La caja se achica un poco menos que el
   ancho total de .cmms-wo-wizard-photo-slot para dejar ese aire. */
.cmms-wo-wizard-photo-slot .cmms-asset-photo-detail-box { width: 150px; }
.cmms-wo-wizard-photo-slot .cmms-asset-photo-detail-img,
.cmms-wo-wizard-photo-slot .cmms-asset-photo-detail-placeholder { width: 134px; height: 100px; margin: 0 8px; }
.cmms-wo-wizard-photo-slot .cmms-asset-photo-detail-tag { max-width: 150px; }
/* Píldora de Estado — flota arriba a la derecha del <h3> de la pestaña
   activa (es el propio <h3>, con su borde inferior, el que la separa
   visualmente de la foto de más abajo). */
.cmms-wo-status-pill { float: right; margin: 0 0 8px 12px; text-transform: uppercase; letter-spacing: .4px; }
@media (max-width: 640px) {
  .cmms-wo-wizard-photo-slot { float: none; width: 100%; flex-direction: row; margin: 0 0 14px; }
  .cmms-wo-wizard-photo-slot .cmms-asset-photo-detail-img,
  .cmms-wo-wizard-photo-slot .cmms-asset-photo-detail-placeholder { margin: 0; }
  .cmms-wo-status-pill { float: none; display: inline-block; margin: 0 0 8px; }
}
/* Se probó dejar que las filas/tarjetas normales "le hagan espacio" solo al
   float — un <div> de bloque suelto (sin establecer su propio formatting
   context) NO esquiva un float, solo el texto/inline de adentro se
   acomoda; una fila grid o una tarjeta entera se sigue extendiendo debajo
   del float y queda semi-tapada. display:flow-root fuerza a que la caja SÍ
   la esquive (mismo efecto que overflow:hidden pero sin el riesgo de
   recortar contenido que se salga del alto calculado), sin tocar el layout
   interno de lo que tiene adentro (grid/flex siguen funcionando igual). */
.cmms-wo-photo-clearance { display: flow-root; }

/* ══════════════════════════════════════════════════════════════════════
   v30 — Estilo visual "glass 3D" (glassmorfismo): paneles translúcidos con
   desenfoque de lo que queda detrás (backdrop-filter), un borde fino y
   brillante, y sombras en capas para dar sensación de relieve/profundidad.
   Va COMO OVERRIDE al final del archivo, con las MISMAS reglas/selectores
   de arriba — misma especificidad, gana por orden de aparición (el que
   está más abajo pisa background/border/box-shadow/backdrop-filter; el
   resto de cada regla original, como padding o display, sigue intacto) —
   así no hace falta duplicar/reescribir cada bloque uno por uno.
   IMPORTANTE — "manteniendo los colores" (pedido del usuario): la paleta no
   cambia, se sigue usando exactamente --c-accent / --c-panel-1 / --c-panel-2
   / etc. de cada tema (oscuro/intermedio/claro, ver :root y
   html[data-cmms-theme] arriba) — con color-mix() esos mismos colores solo
   se vuelven translúcidos, nunca se reemplazan por otros.
   Los badges de estado (pastillas vívidas — Prioridad/Estado/Tipo de OT,
   etc.) se dejan tal cual A PROPÓSITO: tienen que seguir siendo 100% opacos
   para ser legibles de un vistazo (ver el comentario junto a .cmms-badge
   más arriba). Tampoco se toca la pantalla de login ni el modal "Ingreso
   Administrador" — son claros a propósito (legibilidad de un formulario
   sensible, ver comentario junto a #cmms-login-right), aparte del resto de
   la app.
   Requiere backdrop-filter (Chrome/Edge/Safari recientes, Firefox 103+) y
   color-mix() (Chrome/Edge 111+, Safari 16.4+, Firefox 113+) — en un
   navegador sin soporte, esas propiedades puntuales se ignoran y se cae
   sola al valor opaco de la regla original de arriba (sigue viéndose bien,
   solo sin el efecto vidrio). */
:root {
  --glass-blur: 18px;
  --glass-sat: 160%;
  --glass-border: rgba(255,255,255,.14);
  --glass-highlight: rgba(255,255,255,.09);
  --glass-shine: linear-gradient(120deg, rgba(255,255,255,.16), rgba(255,255,255,0) 42%);
}
html[data-cmms-theme="light"] {
  --glass-border: rgba(255,255,255,.75);
  --glass-highlight: rgba(255,255,255,.55);
  --glass-shine: linear-gradient(120deg, rgba(255,255,255,.65), rgba(255,255,255,0) 45%);
}

/* Fondo general: antes un color plano (--c-bg) — ahora ese mismo color de
   base + unos "blobs" suaves con los MISMOS acentos del tema (--c-accent/
   --c-success), fijos al viewport, para que los paneles de vidrio de
   encima tengan algo con textura/color detrás para desenfocar (contra un
   fondo 100% liso el blur no se nota). */
.cmms-app {
  background:
    radial-gradient(1100px 650px at 8% -12%, color-mix(in srgb, var(--c-accent) 20%, transparent), transparent 60%),
    radial-gradient(900px 560px at 105% 0%, color-mix(in srgb, var(--c-success) 12%, transparent), transparent 55%),
    radial-gradient(1000px 750px at 45% 118%, color-mix(in srgb, var(--c-accent) 10%, transparent), transparent 60%),
    var(--c-bg);
  background-attachment: fixed;
}

.cmms-topbar {
  background: linear-gradient(90deg, color-mix(in srgb, var(--c-topbar-1) 80%, transparent), color-mix(in srgb, var(--c-topbar-2) 80%, transparent));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border-bottom: 1px solid var(--glass-border);
  box-shadow: 0 8px 24px -16px rgba(0,0,0,.5), inset 0 1px 0 var(--glass-highlight);
}
.cmms-sidebar {
  background: color-mix(in srgb, var(--c-sidebar-bg) 72%, transparent);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border-right: 1px solid var(--glass-border);
}

/* Tarjetas / KPIs / tiles del menú / secciones de formulario / tarjetas de
   Roles y Permisos — mismo degradé de cada una (--c-panel-1/2, --c-kpi-1/2)
   pero translúcido + desenfocado, con un brillo diagonal fino (::before)
   simulando el reflejo de una superficie de vidrio. */
.cmms-card, .cmms-menu-tile, .cmms-kpi, .cmms-form-section, .perm-role-card {
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: var(--c-panel-shadow), inset 0 1px 0 var(--glass-highlight);
}
.cmms-card, .cmms-form-section, .perm-role-card {
  background: linear-gradient(160deg, color-mix(in srgb, var(--c-panel-1) 58%, transparent), color-mix(in srgb, var(--c-panel-2) 58%, transparent));
}
.cmms-menu-tile, .cmms-kpi {
  background: linear-gradient(160deg, color-mix(in srgb, var(--c-panel-1) 58%, transparent), color-mix(in srgb, var(--c-panel-2) 58%, transparent));
}
.cmms-kpi.warn   { background: linear-gradient(160deg, color-mix(in srgb, var(--c-kpi-warn-1) 62%, transparent), color-mix(in srgb, var(--c-kpi-warn-2) 55%, transparent)); }
.cmms-kpi.danger { background: linear-gradient(160deg, color-mix(in srgb, var(--c-kpi-danger-1) 62%, transparent), color-mix(in srgb, var(--c-kpi-danger-2) 55%, transparent)); }
/* Brillo diagonal — solo en .cmms-card y .cmms-kpi/.cmms-menu-tile: son los
   únicos contenedores de esta lista de los que se confirmó que no cuelga
   ningún menú/popover posicionado en absoluto por encima del borde (ver
   comentario largo arriba) — para esos sí es seguro agregar
   overflow:hidden (necesario para que el brillo respete el borde
   redondeado) sin riesgo de recortar algo que debería sobresalir. */
.cmms-card, .cmms-menu-tile, .cmms-kpi { position: relative; overflow: hidden; }
.cmms-card::before, .cmms-menu-tile::before, .cmms-kpi::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; background: var(--glass-shine); mix-blend-mode: overlay;
}

.cmms-table thead th {
  background: color-mix(in srgb, var(--c-surface-2) 65%, transparent);
  backdrop-filter: blur(10px) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(10px) saturate(var(--glass-sat));
}

.cmms-search, .cmms-filters select, .cmms-date-label input[type="date"] {
  background: color-mix(in srgb, var(--c-surface) 72%, transparent);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.cmms-btn {
  background: color-mix(in srgb, var(--c-surface-2) 62%, transparent);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: inset 0 1px 0 var(--glass-highlight);
}
.cmms-btn:hover { background: color-mix(in srgb, var(--c-btn-hover-bg) 82%, transparent); }
/* Botones "sólidos" (primario y los dos recuadros "OT Correctiva"/
   "OT Preventiva", ver el ajuste de contraste de más arriba): se quedan
   opacos a propósito (necesitan seguir contrastando fuerte contra el
   fondo), pero suman el mismo lenguaje "vidrio" con un degradé + un brillo
   superior simulando una superficie curva/pulida, en vez del fondo plano
   de un solo color que tenían antes. */
.cmms-btn-primary, .cmms-wo-header-card {
  background: linear-gradient(160deg, var(--c-accent), var(--c-accent-h));
  border-color: color-mix(in srgb, var(--c-accent) 70%, white 10%);
  box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--c-accent) 70%, transparent), inset 0 1px 0 rgba(255,255,255,.4);
}
.cmms-btn-primary:hover, .cmms-wo-header-card:hover { background: linear-gradient(160deg, var(--c-accent-h), var(--c-accent-h)); }

.cmms-modal-overlay { backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
.cmms-modal-wide {
  background: color-mix(in srgb, var(--c-surface) 74%, transparent);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: 1px solid var(--glass-border);
  box-shadow: var(--c-panel-shadow-hover), inset 0 1px 0 var(--glass-highlight);
}

.cmms-user-pill, .cmms-topbar-btn {
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* ── Modo TV / pantalla completa del Dashboard (ver btn-tv-mode/enterTvMode
   en app.js) — cascarón propio sin sidebar ni topbar normal, pensado para
   dejarlo fijo en un monitor/TV de sala de control. Reutiliza el mismo
   dashboardView() de siempre (mismas tarjetas/gráficos/colores/tema), solo
   cambia el "marco" alrededor. */
/* height:100vh (fijo, no solo min-height) + overflow:hidden en el
   contenedor — así el modo TV nunca puede medir más que el viewport. La
   barra de scroll del NAVEGADOR que seguía apareciendo (aun con el reset
   de html/body) era el bug clásico de flexbox: un flex item con flex:1 y
   overflow-y:auto (.cmms-tv-main, más abajo) por defecto tiene
   min-height:auto, así que el navegador lo deja crecer según su
   CONTENIDO en vez de respetar el espacio que le tocaba — eso empujaba a
   este contenedor (antes solo min-height:100vh, sin techo) más alto que
   el viewport, y ahí aparecía el scroll a nivel de toda la página en vez
   de quedar contenido adentro de .cmms-tv-main. */
.cmms-tv-mode { height: 100vh; min-height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.cmms-tv-bar {
  position: relative; z-index: 950;
  background: linear-gradient(90deg, var(--c-topbar-1), var(--c-topbar-2));
  color: var(--c-topbar-text); padding: 0 20px; height: 56px;
  display: flex; align-items: center; justify-content: space-between;
  border-bottom: 1px solid var(--c-border); flex-shrink: 0;
}
.cmms-tv-bar-right { display: flex; align-items: center; gap: 14px; }
/* Reloj del modo TV, v3 (pedido del usuario) — fecha y hora en DOS chips
   separados (ver v2 más abajo en el historial) — 20px se sintió grande
   una vez separados en 2 chips propios, así que se bajó 20% (20px → 16px,
   con el resto de las medidas del chip escaladas en la misma proporción)
   manteniendo el mismo look: mismo lenguaje visual que el resto de los
   botones del topbar (variables --c-topbar-btn-*, ver .cmms-topbar-btn),
   tipografía tabular para que los dígitos no "bailen" segundo a segundo. */
.cmms-tv-clock-group { display: flex; align-items: center; gap: 8px; }
.cmms-tv-chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 16px; font-weight: 600; letter-spacing: .3px;
  font-variant-numeric: tabular-nums; color: var(--c-topbar-text);
  background: var(--c-topbar-btn-bg); border: 1px solid var(--c-topbar-btn-border);
  border-radius: 9px; padding: 5px 11px; line-height: 1.2;
}
/* Segundos: texto chico y apagado al lado de la hora — el dato está, pero
   sin competir visualmente con "3:48 PM". */
.cmms-tv-time-secs { font-size: 10px; font-weight: 500; opacity: .55; margin-left: 1px; }
/* min-height:0 — sin esto un flex item con overflow "auto"/"hidden" igual
   crece según su contenido (ver comentario en .cmms-tv-mode más arriba) en
   vez de quedarse en el alto que le toca.
   overflow: hidden (antes "overflow-y: auto") — esa barra de scroll
   "auto" era justamente la que el usuario seguía viendo y arrastrando: el
   escalado automático (tvFitToScreen() en app.js, con su colchón de
   seguridad) ya garantiza que el contenido entra completo, así que esta
   red de seguridad de scroll ya no hace falta y solo agregaba una barra
   no deseada ante cualquier diferencia mínima de redondeo. Si por algún
   motivo el cálculo de escala fallara, ahora se recorta en vez de mostrar
   scroll — se prefiere eso a la barra. */
.cmms-tv-main { flex: 1; min-height: 0; padding: 20px 26px 28px; overflow: hidden; }
/* El botón "🖥️ Pantalla completa" solo tiene sentido en el Dashboard
   normal, no una vez ya adentro del modo TV (ahí sale con "✕ Salir"). */
.cmms-tv-mode #btn-tv-mode { display: none; }

/* ── Modo TV: layout compacto (pedido del usuario) ───────────────────────
   Mismos componentes/colores/tema que el Dashboard normal, pero con
   paddings/gaps/tamaños de gráfico reducidos para que KPIs + los 2
   gráficos + criticidad/cumplimiento MP + calendario entren completos en
   el alto de un monitor típico sin necesitar scroll. #cmms-tv-scale-wrap
   es además el bloque que tvFitToScreen() (ver app.js) puede achicar con
   transform: scale si, aun así, no llega a entrar (monitor más chico, o
   apareció la tarjeta de "⚠ Choque PM/CM"). */
#cmms-tv-fit { overflow: hidden; width: 100%; }
#cmms-tv-scale-wrap { display: block; width: 100%; transform-origin: top left; }
.cmms-tv-mode .cmms-page-header { margin-bottom: 10px; }
.cmms-tv-mode .cmms-kpi-grid { gap: 10px; margin-bottom: 12px; }
.cmms-tv-mode .cmms-kpi { padding: 10px 12px; }
.cmms-tv-mode .cmms-kpi-label { margin-bottom: 4px; padding-right: 26px; }
.cmms-tv-mode .cmms-kpi-value { font-size: 22px; }
.cmms-tv-mode .cmms-charts-row { gap: 10px; margin-bottom: 10px !important; }
.cmms-tv-mode .cmms-card { padding: 10px 14px; }
.cmms-tv-mode .cmms-card h3 { margin: 0 0 8px; }
/* v70 (pedido del usuario) — los 3 cuadros de gráficos ("OTs por mes",
   "Cumplimiento de mantenimiento preventivo", "Estado de los activos")
   +20% de alto/contenido: más padding en las 3 tarjetas (gana por
   especificidad a .cmms-tv-mode .cmms-card de arriba, que sigue aplicando
   tal cual al resto de las tarjetas del modo TV — calendario/OTs
   recientes/choque PM-CM) y +20% en el alto de las 2 barras (108px→130px,
   mismo criterio "mismo alto en los dos" de siempre) y de la franja
   apilada de "Estado de los activos" (14px→17px). */
.cmms-tv-mode .cmms-charts-row .cmms-card { padding: 14px 20px; }
.cmms-tv-mode .cmms-charts-row .cmms-card h3 { margin: 0 0 12px; }
/* Mismo alto en los dos — ver comentario junto a .cmms-vbarchart más arriba
   (pedido del usuario: "Cumplimiento de mantenimiento preventivo" debe
   crecer hasta igualar a "OTs por mes", no achicarse este último).
   v71 (pedido del usuario) — otro +20% más sobre el v70 de arriba
   (130px→156px): ya van dos rondas de +20% acumuladas sobre el 108px
   original. */
.cmms-tv-mode .cmms-monthbar-wrap { height: 156px; padding-top: 26px; }
.cmms-tv-mode .cmms-vbarchart { height: 156px; padding-top: 12px; }
.cmms-tv-mode .cmms-charts-row .cmms-stackbar-track { height: 20px; }
.cmms-tv-mode .cmms-charts-row .cmms-stackbar-wrap { gap: 20px; }
.cmms-tv-mode .cmms-cal-header { margin-bottom: 4px; }
.cmms-tv-mode .cmms-cal-legend { margin-bottom: 4px; }
.cmms-tv-mode .cmms-cal-weekday { padding: 2px 0; }
/* Calendario más alto (pedido del usuario) — antes quedaba chico dejando
   espacio libre sin usar en la pantalla completa; "OTs recientes" al
   costado crece con él porque ya está pegado a su altura (ver
   .cmms-tv-wo-col .cmms-card { height:100% } más abajo). Si con esto el
   Dashboard completo ya no entra en el alto de la pantalla, el
   auto-escalado de respaldo (tvFitToScreen() en app.js) lo compensa solo. */
.cmms-tv-mode .cmms-cal-day { min-height: 37px; max-height: 73px; padding: 3px 6px; }
.cmms-tv-mode .cmms-cal-day-num { font-size: 13px; }

/* ── Modo TV: calendario + "OTs recientes" lado a lado ───────────────────
   El usuario notó que el calendario, al ocupar el ancho completo del
   Dashboard, dejaba casi todas las celdas vacías (la mayoría de los días
   no tiene ninguna OT programada). El alto de cada celda lo fija
   .cmms-tv-mode .cmms-cal-day (min/max-height, más arriba) — no depende
   del ancho (desde v49 .cmms-cal-day ya no usa aspect-ratio, ver ese
   comentario), así que angostar el calendario aquí solo libera ANCHO,
   suficiente para que "OTs recientes" (que en el Dashboard normal va
   apilada debajo, ver dashboardView()) se ubique al costado sin agregar
   una fila nueva al layout. */
.cmms-tv-cal-row { display: flex; gap: 10px; align-items: stretch; margin-top: 10px; }
.cmms-tv-cal-row .cmms-card { margin-top: 0 !important; }
.cmms-tv-cal-col { flex: 0 1 58%; min-width: 0; }
.cmms-tv-wo-col { flex: 1 1 42%; min-width: 0; }
/* overflow-x:hidden (antes "auto") — con 9 columnas la tabla se pasaba del
   ancho de esta columna y aparecía una barra de scroll horizontal (pedido
   del usuario: quitarla). table-layout:fixed + white-space:normal más
   abajo (".cmms-tv-wo-col .cmms-table th/td") hacen que las columnas se
   repartan el ancho disponible y el texto pase a la línea siguiente en vez
   de forzar overflow; el scroll vertical (overflow-y) se mantiene para
   cuando hay más filas de las que entran. */
.cmms-tv-wo-col .cmms-card { height: 100%; box-sizing: border-box; overflow-y: auto; overflow-x: hidden; }
.cmms-tv-wo-col .cmms-table { table-layout: fixed; width: 100%; }
/* white-space:normal (sin word-break/overflow-wrap:break-word) — la
   primera versión partía CUALQUIER palabra a la mitad para no desbordar
   ("ADMINIST-RADOR", "PRIORIDA-D"), ilegible (reportado por el usuario).
   Ahora el texto solo baja de línea en un espacio (ya alcanza para los
   encabezados de 2 palabras — "TAG EQUIPO", "ASIGNADO A", "DÍAS EN
   ESTADO — y para "Jacinto rodriguez"). Los anchos de columna de abajo
   están repartidos a mano (n-ésimo <th>, table-layout:fixed los hereda a
   toda la columna) para que las palabras sueltas que SÍ quedan de un tirón
   ("PRIORIDAD", fechas "2026-07-27") tengan sitio de sobra en una sola
   línea sin necesitar el corte forzado. */
.cmms-tv-wo-col .cmms-table th,
.cmms-tv-wo-col .cmms-table td { white-space: normal; }
.cmms-tv-wo-col .cmms-table th:nth-child(1) { width: 9%; }  /* N° OT */
.cmms-tv-wo-col .cmms-table th:nth-child(2) { width: 6%; }  /* Tipo */
.cmms-tv-wo-col .cmms-table th:nth-child(3) { width: 18%; } /* Título */
.cmms-tv-wo-col .cmms-table th:nth-child(4) { width: 10%; } /* TAG Equipo */
.cmms-tv-wo-col .cmms-table th:nth-child(5) { width: 9%; }  /* Prioridad */
.cmms-tv-wo-col .cmms-table th:nth-child(6) { width: 11%; } /* Estado */
.cmms-tv-wo-col .cmms-table th:nth-child(7) { width: 10%; } /* Días en estado */
.cmms-tv-wo-col .cmms-table th:nth-child(8) { width: 10%; } /* Creada */
.cmms-tv-wo-col .cmms-table th:nth-child(9) { width: 17%; } /* Asignado a */
/* v49 (pedido del usuario: "el calendario se vea del mismo tamaño que el
   listado de OTs" en modo TV) — antes la tarjeta del calendario NO estiraba
   su alto: aunque .cmms-tv-cal-row tiene align-items:stretch, eso solo
   estira el DIV contenedor .cmms-tv-cal-col (que no tiene fondo/borde
   visible); la tarjeta .cmms-card de adentro seguía con su alto natural
   (más bajo que "OTs recientes", que sí tiene height:100% desde antes) —
   quedaba un hueco invisible debajo. Ahora la tarjeta del calendario
   también ocupa el 100% de esa fila, con flex-direction:column: el
   encabezado/leyenda/días de la semana mantienen su alto natural (flex-
   shrink:0) y la grilla de días (.cmms-cal-grid que NO es la de días de la
   semana) crece con flex:1 para repartir el espacio sobrante entre sus
   filas (grid-auto-rows:1fr) — así el calendario siempre iguala el alto de
   "OTs recientes" sin importar si el mes tiene 4, 5 o 6 filas de días. Las
   celdas ya no usan un alto fijo acá (min/max-height de la regla de arriba
   quedan reemplazados por height:100% para que llenen su fila del grid). */
.cmms-tv-cal-col .cmms-card { height: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
.cmms-tv-cal-col .cmms-cal-header,
.cmms-tv-cal-col .cmms-cal-legend,
.cmms-tv-cal-col .cmms-cal-weekdays,
.cmms-tv-cal-col h3 { flex-shrink: 0; }
.cmms-tv-cal-col .cmms-cal-grid:not(.cmms-cal-weekdays) { flex: 1; grid-auto-rows: 1fr; min-height: 0; }
.cmms-tv-cal-col .cmms-cal-day { min-height: 0; max-height: none; height: 100%; }
/* v70 (bug reportado por el usuario) — desde que el calendario vive en esta
   fila lado a lado con "OTs recientes" (v49, más arriba) cada celda ya usa
   height:100% de su fila del grid en vez del min/max-height fijo de 34-
   66px de la regla ".cmms-tv-mode .cmms-cal-day" de más arriba — esa regla
   vieja es la que ocultaba la grilla de códigos de OT (".cmms-tv-mode
   .cmms-cal-day-ots { display:none }") por ser "demasiado chica". Ya no
   aplica: acá la celda tiene el alto real de la fila (ver el resto de este
   bloque), así que se vuelve a mostrar la grilla de códigos — igual que en
   el Dashboard normal (ver dashboardView() sin tv) — con las fuentes
   +10% (contenido de este bloque, pedido del usuario junto con el alto). */
.cmms-tv-cal-col .cmms-cal-day-ots { display: grid; }
.cmms-tv-cal-col .cmms-cal-day-ot { font-size: 9.5px; line-height: 14px; }
/* +10% de alto/contenido en calendario y "OTs recientes" (pedido del
   usuario): agrandar la fila de la tabla de "OTs recientes" es lo que en
   la práctica hace crecer TODA esta fila — el calendario (columna
   izquierda) sigue el mismo alto vía align-items:stretch + height:100%
   de más arriba, así que basta con esto para que ambos crezcan juntos. */
.cmms-tv-cal-row .cmms-card { padding: 11px 15px; }
.cmms-tv-wo-col .cmms-table th,
.cmms-tv-wo-col .cmms-table td { padding: 11px 13px; }
.cmms-tv-wo-col .cmms-table td { font-size: 14px; }

/* ── Simulador de roles (Configuración > Permisos > Simulador, 31-ago-2026,
   pedido explícito del usuario) ──
   Muestra en vivo, para 5 documentos (OT Correctiva catálogo/no catálogo,
   OT Inspección, RG, OT PMP), qué rol real cubre cada etapa de su flujo —
   leyendo roles.attributes de la base, no un preset hardcodeado. Los presets
   de tamaño de equipo son solo un atajo para activar/desactivar roles ya
   existentes; NO modifican los permisos guardados de ningún rol. Órdenes de
   compra queda fuera a propósito (permiso único `comprar`, sin desglose por
   etapa todavía — ver comentario en adminPermissionsSimulatorView()). */
.sim-layout { display: grid; grid-template-columns: 320px 1fr; gap: 18px; align-items: start; }
@media (max-width: 980px) { .sim-layout { grid-template-columns: 1fr; } }

.sim-presets { display: grid; gap: 8px; }
.sim-preset-btn {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; text-align: left; padding: 10px 12px; border-radius: 8px;
  border: 1px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text);
  cursor: pointer; font: inherit;
}
.sim-preset-btn:hover { border-color: var(--c-accent); }
.sim-preset-btn.active { background: rgba(59,130,246,.12); border-color: var(--c-accent); }
.sim-preset-btn .sp-name { font-weight: 600; font-size: 13px; display: block; }
.sim-preset-btn .sp-desc { font-size: 11px; color: var(--c-muted); margin-top: 1px; display: block; }
.sim-preset-count {
  font-size: 11px; font-weight: 700; color: var(--c-muted); background: var(--c-surface);
  border: 1px solid var(--c-border); border-radius: 20px; padding: 2px 8px; white-space: nowrap;
}
.sim-preset-btn.active .sim-preset-count { color: var(--c-accent); border-color: var(--c-accent); }

.sim-principle {
  margin-top: 10px; padding: 9px 11px; border-radius: 8px; background: var(--c-surface-2);
  border: 1px dashed var(--c-border); font-size: 11.5px; color: var(--c-muted); line-height: 1.5;
}

.sim-role-list { display: flex; flex-direction: column; gap: 6px; }
.sim-role-chip {
  display: flex; align-items: center; gap: 9px; padding: 8px 9px; border-radius: 8px;
  border: 1px solid var(--c-border); background: var(--c-surface-2); cursor: pointer; user-select: none;
}
.sim-role-chip.off { opacity: .42; }
.sim-role-chip-static { cursor: default; }
.sim-role-chip-static:hover { border-color: var(--c-border); }
.sim-role-chip:hover { border-color: var(--role-c, var(--c-accent)); }
.sim-role-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--role-c); flex: none; margin-top: 3px; align-self: flex-start; }
.sim-role-chip .sr-text { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.sim-role-chip .sr-name { font-size: 12.5px; font-weight: 600; }
.sim-role-chip .sr-hint { font-size: 10.5px; color: var(--c-muted); font-style: italic; line-height: 1.3; }
.sim-role-chip .sr-count { font-size: 10.5px; color: var(--c-muted); flex: none; align-self: flex-start; margin-top: 1px; }
.sim-switch { width: 30px; height: 17px; border-radius: 20px; background: var(--c-border); position: relative; flex: none; }
.sim-switch::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%;
  background: var(--c-surface); box-shadow: 0 1px 2px rgba(0,0,0,.3);
}
.sim-role-chip:not(.off) .sim-switch { background: var(--role-c); }
.sim-role-chip:not(.off) .sim-switch::after { left: 15px; }

.sim-custom-flag {
  display: flex; align-items: center; gap: 7px; margin-top: 12px; padding: 8px 10px;
  border-radius: 8px; background: rgba(245,158,11,.13); border: 1px solid var(--c-warning);
  font-size: 11.5px; color: var(--c-text); font-weight: 600;
}
.sim-custom-flag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--c-warning); flex: none; }

.sim-diagram-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px; flex-wrap: wrap; }
.sim-gap-counter { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700; color: var(--c-danger); }
.sim-gap-counter.zero { color: var(--c-success); }

.sim-doc-row { border-top: 1px solid var(--c-border); padding: 16px 0; }
.sim-doc-row:first-of-type { border-top: none; padding-top: 6px; }
.sim-doc-title { font-weight: 700; font-size: 13px; margin-bottom: 10px; }

.sim-pipeline { display: flex; align-items: flex-start; gap: 0; overflow-x: auto; padding-bottom: 4px; }
.sim-stage { display: flex; align-items: flex-start; flex: none; }
.sim-stage-node { min-width: 128px; max-width: 168px; border-radius: 9px; padding: 8px 10px; border: 1.5px solid var(--c-border); background: var(--c-surface-2); }
.sim-stage-node .sn-stage { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--c-muted); font-weight: 800; }
.sim-stage-node .sn-role { font-size: 12px; font-weight: 700; margin-top: 3px; }
.sim-stage-node.gap { border-color: var(--c-danger); border-style: dashed; background: rgba(239,68,68,.08); }
.sim-stage-node.gap .sn-role { color: var(--c-danger); }
/* Un nodo puede tener VARIOS roles cubriendo la misma etapa a la vez
   (31-ago-2026, pedido explícito del usuario: "agrega todos los roles
   asociados a cada etapa... siempre listado nunca en oración") — cada uno
   en su propia línea, con el mismo punto de color que ya usa en el panel
   "Roles activos" para poder identificarlo de un vistazo. El borde del
   nodo pasa a ser semántico (verde = cubierto) en vez de heredar el color
   de un solo rol, porque ahora puede haber más de uno. */
.sim-stage-node.ok { border-color: var(--c-success); background: rgba(34,197,94,.07); }
.sim-stage-node .sn-role-list { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.sim-stage-node .sn-role-item { display: flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 600; line-height: 1.3; color: var(--role-c); }
.sim-stage-node .sn-role-item .sn-role-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--role-c); flex: none; }
.sim-connector { width: 20px; height: 2px; background: var(--c-border); flex: none; position: relative; margin-top: 20px; }
.sim-connector::after {
  content: ""; position: absolute; right: -1px; top: 50%; transform: translateY(-50%);
  border: 4px solid transparent; border-left-color: var(--c-border);
}
.sim-gap-tooltip { position: relative; }
.sim-gap-tooltip .sgt-tip {
  display: none; position: absolute; z-index: 5; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  width: 220px; background: var(--c-text); color: var(--c-bg); font-size: 11px; line-height: 1.45;
  padding: 9px 10px; border-radius: 7px; box-shadow: 0 8px 20px rgba(0,0,0,.25); font-weight: 500;
}
.sim-gap-tooltip:hover .sgt-tip { display: block; }
