:root {
  --background: #0a0a0b;
  --foreground: #fafafa;
  --card: #18181b;
  --card-foreground: #fafafa;
  --border: #27272a;
  --input: #27272a;
  --muted: #27272a;
  --muted-foreground: #a1a1aa;
  --accent: #27272a;
  --primary: #3b82f6;
  --primary-foreground: #ffffff;
  --secondary: #27272a;
  --secondary-foreground: #e4e4e7;
  --destructive: #7f1d1d;
  --destructive-foreground: #fecaca;
  --success-bg: rgba(74, 222, 128, .15);
  --success-fg: #4ade80;
  --danger-bg: rgba(248, 113, 113, .15);
  --danger-fg: #f87171;
  --warning-bg: rgba(251, 191, 36, .15);
  --warning-fg: #fbbf24;
  --chart-down: #3b82f6;
  --chart-up: #93c5fd;
  --radius: .625rem;
}
* { box-sizing: border-box; }

.scroll-top-btn {
  position: fixed; left: 1.5rem; bottom: 1.5rem; z-index: 900;
  width: 2.75rem; height: 2.75rem; padding: 0; border-radius: 9999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--secondary); color: var(--secondary-foreground); border: 1px solid var(--border);
  box-shadow: 0 4px 12px rgba(0, 0, 0, .3);
  opacity: 0; pointer-events: none; transform: translateY(.5rem);
  transition: opacity .2s ease, transform .2s ease, background-color .15s;
}
.scroll-top-btn.visible { opacity: 1; pointer-events: auto; transform: translateY(0); }
.scroll-top-btn:hover { background: var(--accent); opacity: 1; }
body {
  font-family: system-ui, sans-serif; margin: 2rem; background: var(--background); color: var(--foreground);
  /* deja lugar abajo de todo el contenido para que el boton flotante de
     "volver arriba" (fixed, bottom-left) no quede pisando el ultimo
     parrafo de la pagina cuando se scrollea hasta el final */
  padding-bottom: 5rem;
}
/* Mapa/Mapa2 (hide_chrome, ver base.html): sin header/pestañas para
   aprovechar toda la pantalla -- pedido explicito. El margin/padding de
   arriba tambien se achica aca: esas paginas ya no scrollean como pagina
   (el mapa mide lo que sobra de alto el mismo, ver sizeMapDiagramToViewport
   en dashboard.js), asi que dejar 5rem de aire abajo "por si acaso" le
   comeria espacio de pantalla sin ninguna razon (no hay boton flotante que
   tapar: nunca se activa si no hay scroll de pagina). */
body.no-chrome { margin: .75rem 1rem; padding-bottom: 0; }
.site-header { margin-bottom: 1.25rem; display: flex; align-items: flex-start; gap: 1.5rem; }
/* header-actions va primero en el HTML (antes que la tarjeta de Trafico
   Total) y flex:0 0 auto -- asi el logo y el boton de ocultar/mostrar
   graficos quedan siempre arriba a la izquierda, sin moverse de lugar
   cuando la tarjeta de Trafico Total desaparece (display:none al ocultar
   graficos, o directamente ausente sin cache). El grafico, con
   flex:1 1 auto, ocupa lo que queda a la derecha. */
.header-actions { display: flex; flex-direction: column; align-items: flex-start; gap: .4rem; flex: 0 0 auto; }
.logo { height: 5rem; width: auto; display: block; flex: 0 0 auto; }
.provider-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.total-traffic-card { flex: 1 1 auto; min-width: 0; }
/* height fija (no solo min-height): sin una altura CSS explicita el canvas
   queda dependiendo de la relacion de ancho/alto de su propio buffer
   interno (que el JS reescribe en cada frame para el DPI), lo que
   retroalimenta un crecimiento sin control ("se ve enorme") */
.total-traffic-card canvas { height: 5.5rem; min-height: 5.5rem; }

/* grilla de la pestaña Info (address-lists/pools) -- misma disposicion que
   .provider-grid pero clase separada a proposito: no tiene que ver con los
   graficos de consumo, no debe ocultarse con el boton "Ocultar graficos" */
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }

.page-nav { margin-bottom: 1rem; display: flex; align-items: center; }
.page-nav a {
  color: var(--muted-foreground); margin-right: 1.25rem; text-decoration: none;
  font-size: .9rem; font-weight: 500; padding-bottom: .35rem; border-bottom: 2px solid transparent;
}
.page-nav a:hover { color: var(--foreground); }
.page-nav a.active { color: var(--foreground); border-bottom-color: var(--primary); }
form { margin-bottom: 1.25rem; display: flex; gap: .5rem; }

.search-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
.search-row form { margin-bottom: 0; }
.alerts-card { flex: 0 1 auto; }
.alerts-card .card-content { padding: .6rem 1rem; }
.alerts-card .card-content p { margin: .3rem 0; }
.alerts-card .card-content p:first-child { margin-top: 0; }
.alerts-card .card-content p:last-child { margin-bottom: 0; }
input[type=text], input[type=password] {
  height: 2.25rem; padding: 0 .75rem; font-size: .85rem; width: 22rem;
  border-radius: calc(var(--radius) - 2px); border: 1px solid var(--border);
  background: var(--input); color: var(--foreground);
}
input[type=text]:focus, input[type=password]:focus { outline: 2px solid var(--primary); outline-offset: 1px; }

/* boton "x" para vaciar el cuadro de busqueda (index.html y nodos.html) --
   superpuesto sobre el input, no un elemento aparte, para que quede pegado
   adentro del campo como se espera de un boton de "borrar" */
.search-input-wrap { position: relative; display: inline-flex; }
.search-input-wrap input[type=text] { padding-right: 2rem; }
.search-clear {
  position: absolute; right: .25rem; top: 50%; transform: translateY(-50%);
  width: 1.5rem; height: 1.5rem; padding: 0; border: none; background: transparent;
  color: var(--muted-foreground); font-size: 1.1rem; line-height: 1; cursor: pointer;
  border-radius: 50%;
}
.search-clear:hover { background: var(--accent); color: var(--foreground); }

/* pantalla de login (templates/login.html) -- pagina standalone, no usa
   base.html, asi que estas reglas quedan con su propio namespace .login-*
   para no pisar el resto de los estilos */
body.login-page {
  margin: 0; padding: 0; min-height: 100vh; width: 100%;
  display: flex; align-items: center; justify-content: center;
  background:
    linear-gradient(rgba(10, 10, 11, .55), rgba(10, 10, 11, .55)),
    url("background-login.png") center center / cover no-repeat fixed,
    var(--background);
}
.login-card {
  display: flex; flex-direction: column; gap: .6rem;
  width: 22rem; max-width: calc(100vw - 2.5rem);
  background: rgba(24, 24, 27, .85); backdrop-filter: blur(6px);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 2rem 1.75rem 1.75rem; box-shadow: 0 20px 50px rgba(0, 0, 0, .45);
}
.login-logo { height: 7rem; width: auto; margin: 0 auto .5rem; display: block; }
.login-subtitle { margin: 0 0 .5rem; text-align: center; color: var(--muted-foreground); font-size: .82rem; }
.login-error {
  margin: 0 0 .25rem; padding: .5rem .75rem; border-radius: calc(var(--radius) - 2px);
  background: var(--danger-bg); color: var(--danger-fg); font-size: .82rem; text-align: center;
}
.login-label { font-size: .78rem; color: var(--muted-foreground); margin-bottom: -.35rem; }
.login-card input[type=text], .login-card input[type=password] { width: 100%; }
.login-btn {
  margin-top: .5rem; height: 2.4rem; background: var(--primary); color: var(--primary-foreground);
  border: none; font-weight: 600; font-size: .88rem;
}
.login-btn:hover { opacity: .9; }
button, .btn-logout {
  display: inline-flex; align-items: center; justify-content: center;
  height: 2.25rem; padding: 0 1rem; font-size: .85rem; font-weight: 500;
  border-radius: calc(var(--radius) - 2px); border: 1px solid var(--border);
  background: var(--secondary); color: var(--secondary-foreground); cursor: pointer;
  transition: opacity .15s, background-color .15s, border-color .15s;
  text-decoration: none;
}
button:hover, .btn-logout:hover { background: var(--accent); opacity: 1; }
button:disabled { opacity: .5; cursor: not-allowed; }
table { border-collapse: collapse; width: 100%; }
.muted { color: var(--muted-foreground); font-size: .82rem; }
.text-danger { color: var(--danger-fg); font-size: .82rem; }
.text-warning { color: var(--warning-fg); font-size: .82rem; }
.text-success { color: var(--success-fg); font-size: .82rem; }
.hint { color: var(--muted-foreground); font-size: .78rem; margin: -.75rem 0 1.25rem; }
/* en Info el hint va justo debajo del nav (sin el buscador de por medio como
   en las otras pestañas), asi que el margen negativo de .hint lo deja
   demasiado pegado a las solapas -- se le da su propio margen positivo */
.info-hint { margin-top: .75rem; }

.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.provider-card { transition: background 2.5s ease; }
.provider-card.card-highlight { background: var(--warning-bg); transition: none; }
.card-header { display: flex; align-items: center; gap: .5rem; padding: .7rem 1rem; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.card-header-center { justify-content: center; }
.card-title { font-weight: 600; font-size: .92rem; }
.card-subtitle { color: var(--muted-foreground); font-size: .8rem; }
.checked-ago { font-size: .72rem; white-space: nowrap; }
.ip-link { color: var(--primary); font-size: .8rem; text-decoration: none; }
.ip-link:hover { text-decoration: underline; }

.btn-reboot, .btn-logout {
  height: 1.6rem; padding: 0 .65rem; font-size: .72rem; font-weight: 600;
  background: var(--secondary); color: var(--secondary-foreground); border: 1px solid var(--border);
}
.btn-reboot:hover, .btn-logout:hover { background: var(--accent); opacity: 1; }
.card-content { padding: .4rem 1rem .8rem; flex: 1 1 auto; }
.card-content > table td { padding: .4rem 0; border-bottom: 1px solid var(--border); font-size: .85rem; vertical-align: top; }
.card-content > table tr:last-child > td { border-bottom: none; }
.card-content > table td:first-child { color: var(--muted-foreground); width: 9rem; white-space: nowrap; }

.badge {
  display: inline-flex; align-items: center; border-radius: 9999px; padding: .15rem .55rem;
  font-size: .68rem; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; line-height: 1.4;
}
.badge-success { background: var(--success-bg); color: var(--success-fg); }
.badge-danger { background: var(--danger-bg); color: var(--danger-fg); }
.badge-warning { background: var(--warning-bg); color: var(--warning-fg); }
.badge-muted { background: var(--muted); color: var(--muted-foreground); }

.mod-bars { display: inline-flex; align-items: flex-end; gap: 2px; height: .95rem; margin-right: .4rem; vertical-align: middle; }
.mod-bar-seg { width: .22rem; background: var(--muted); border-radius: 1px; }
.mod-bar-seg.seg-1 { height: 25%; }
.mod-bar-seg.seg-2 { height: 50%; }
.mod-bar-seg.seg-3 { height: 75%; }
.mod-bar-seg.seg-4 { height: 100%; }
.mod-bar-seg.lit { background: var(--success-fg); }

/* indicador de senial estilo el que ya muestra airOS en su propio
   dashboard (mca-status/main) -- barra con gradiente fijo (rojo->naranja->
   amarillo->verde->celeste, mismos tonos que ya usan .node-card-* para
   mimosa/radio) y un marcador que se posiciona segun el dBm (ver
   _signal_gauge_pct en dashboard.py). El gradiente es siempre el mismo, lo
   que cambia es DONDE cae el marcador. */
.signal-gauge {
  position: relative; display: inline-block; width: 6rem; height: .55rem;
  margin-right: .5rem; vertical-align: middle; border-radius: 999px;
  background: linear-gradient(to right, #f87171, #fb923c, #fbbf24, #4ade80, #38bdf8);
}
.signal-gauge-marker {
  position: absolute; top: -.15rem; width: 2px; height: calc(100% + .3rem);
  background: var(--foreground); transform: translateX(-1px);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .4);
}

.client-block { display: flex; align-items: stretch; gap: 1rem; margin-bottom: 1.25rem; }
.card.client-details, .card.client-ping { display: flex; flex-direction: column; }
.client-details { flex: 0 1 28rem; max-width: 28rem; min-width: 0; }
.client-ping { flex: 1 1 auto; min-width: 24rem; }

.diag-content { display: flex; flex-direction: row; height: 100%; padding: 0; }
.diag-half { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; padding: .75rem 1rem; }
.diag-half + .diag-half { border-left: 1px solid var(--border); }
.diag-half .ping-out, .diag-half .rate-wrap { flex: 1 1 auto; min-height: 0; }

.btn-ping, .btn-scan {
  width: 100%; background: var(--secondary); color: var(--secondary-foreground); border: 1px solid var(--border);
}
.btn-ping:hover, .btn-scan:hover { background: var(--accent); opacity: 1; }
.btn-ping.active { background: var(--destructive); color: var(--destructive-foreground); border-color: transparent; }
.btn-scan { width: auto; height: 1.9rem; padding: 0 .7rem; font-size: .78rem; }

.ping-out {
  margin-top: .5rem; display: flex; flex-direction: column; overflow: hidden;
  background: var(--background); border: 1px solid var(--border); border-radius: calc(var(--radius) - 4px);
}
.ping-log {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: .5rem .6rem;
  font-family: ui-monospace, SFMono-Regular, monospace; font-size: 1.05rem; line-height: 1.6;
  white-space: pre-wrap; color: var(--muted-foreground);
}
.ping-summary {
  flex: 0 0 auto; padding: .5rem .6rem; border-top: 1px solid var(--border);
  font-size: 1.05rem; line-height: 1.6;
}
.ping-stat-row {
  display: flex; justify-content: space-between; gap: 1rem;
  font-variant-numeric: tabular-nums;
}
.ping-stat-row .ping-stat-label { color: var(--muted-foreground); }
.ping-stat-row .ping-stat-val { color: var(--foreground); font-weight: 600; }
.rate-wrap { margin-top: .6rem; display: flex; flex-direction: column; }
canvas {
  width: 100%; height: 6rem; flex: 1 1 auto; min-height: 6rem; display: block;
  background: var(--background); border: 1px solid var(--border); border-radius: calc(var(--radius) - 4px);
}
.rate-legend { display: flex; gap: 1rem; font-size: .72rem; margin-top: .5rem; color: var(--muted-foreground); }
.legend-item { display: inline-flex; align-items: center; gap: .35rem; }
.legend-dot { width: .5rem; height: .5rem; border-radius: 2px; flex: 0 0 auto; }

.chart-tooltip {
  display: none; position: fixed; z-index: 1000; pointer-events: none;
  background: var(--background); border: 1px solid var(--border); border-radius: var(--radius);
  padding: .4rem .6rem; font-size: .72rem; color: var(--foreground);
  box-shadow: 0 10px 25px rgba(0, 0, 0, .35);
}
.chart-tooltip-row { display: flex; align-items: center; gap: .4rem; white-space: nowrap; min-width: 9rem; }
.chart-tooltip-val { margin-left: auto; padding-left: .75rem; font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
.dot-down { background: var(--chart-down); }
.dot-up { background: var(--chart-up); }
.rate-now { font-size: .74rem; margin-top: .25rem; min-height: 1.1rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }

.dhcp-sub { margin-top: .3rem; }
.dhcp-sub th { text-align: left; color: var(--muted-foreground); font-size: .72rem; font-weight: 500; padding: 0 .5rem .25rem 0; }
.dhcp-sub td { padding: .3rem .5rem .3rem 0; border-bottom: 1px solid var(--border); font-size: .82rem; }
.dhcp-sub td:first-child, .dhcp-sub th:first-child { width: 11rem; max-width: 11rem; white-space: normal; word-break: break-word; padding-right: 1rem; }
.dhcp-sub td:first-child .muted { display: block; margin-top: .1rem; }

.pool-table { width: 100%; margin-top: .3rem; }
.pool-table th { text-align: left; color: var(--muted-foreground); font-size: .72rem; font-weight: 500; padding: 0 .5rem .25rem 0; }
.pool-table td { padding: .3rem .5rem .3rem 0; border-bottom: 1px solid var(--border); font-size: .82rem; }
.pool-table td:first-child { white-space: normal; word-break: break-word; }
.pool-table td:last-child, .pool-table th:last-child { text-align: right; padding-right: 0; font-variant-numeric: tabular-nums; }
.pool-table td .muted { display: block; font-size: .72rem; margin-top: .1rem; }
.hostname-link { color: var(--primary); text-decoration: none; }
.hostname-link:hover { text-decoration: underline; }
.scan-out { margin-top: .4rem; }
.log-out { margin-top: .4rem; white-space: pre-wrap; }
.diag-out { margin-top: .4rem; }
.reboot-out:empty { margin-top: 0; }
.scan-out table { margin-top: .3rem; }
.scan-out td { padding: .3rem .4rem; border-bottom: 1px solid var(--border); font-size: .82rem; }
/* cada escalon es una linea de texto corrido (no tabla) a proposito -- con
   nombres largos de cliente/nodo, una tabla de columnas fijas terminaba
   superponiendose en pantallas angostas (ver commit anterior); texto
   corrido envuelve solo con el word-wrap normal del navegador, sin pelear
   con anchos de columna. */
.diag-line { font-size: .85rem; padding: .3rem 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
.diag-line:last-child { border-bottom: none; }
tr.better td { background: rgba(74, 222, 128, .08); }

#nodos-filter { margin-bottom: 0; }

/* pestaña Nodos: una tarjeta por nodo, con un cano de color a la izquierda
   segun la marca del equipo (mismo criterio que MARCA_POR_TIPO en
   dashboard.py) -- ubiquiti/radio celeste, mikrotik/switch gris, mimosa
   naranja, para poder distinguir el tipo de un vistazo en la grilla. */
.node-grid { display: flex; flex-direction: column; gap: .75rem; }
.node-card { border-left-width: 3px; transition: background 2.5s ease; }
.node-card-radio { border-left-color: #38bdf8; }
.node-card-radio .card-header { background: rgba(56, 189, 248, .08); }
.node-card-switch { border-left-color: #a1a1aa; }
.node-card-switch .card-header { background: rgba(161, 161, 170, .08); }
.node-card-mimosa { border-left-color: #fb923c; }
.node-card-mimosa .card-header { background: rgba(251, 146, 60, .08); }

/* pestana Mapa: diagrama arrastrable tipo Dude (ver templates/mapa.html) --
   SIN lineas conectoras a proposito (pedido explicito: con ~130 equipos las
   lineas ensuciaban mas de lo que ayudaban) y SIN vista de lista, el
   diagrama es la unica vista. El canvas tiene ancho/alto fijos en pixeles
   reales -- pedido explicito: "una ventana con sus medidas, y para ver el
   resto bajar o deslizarte con el mouse o el dedo" en vez de un zoom/pan a
   mano. overflow:auto deja que el scroll NATIVO del navegador (barras,
   rueda del mouse, touch-scroll con inercia en el celular) se encargue
   solo, sin JS -- el alto es el que le calcula sizeMapDiagramToViewport()
   en dashboard.js (llena lo que queda de pantalla, para que no aparezca
   una SEGUNDA barra de scroll, la de la pagina entera); 70vh aca abajo es
   solo el valor de arranque hasta que ese JS corre. El color online/offline
   de cada cuadro se actualiza solo cada pocos segundos via /api/mapa-live
   (ver startMapaLive en dashboard.js), pingueando cada nodo en vivo. */
.map-diagram-scroll {
  position: relative; overflow: auto; user-select: none;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--card); height: 70vh; cursor: grab;
}
.map-diagram-scroll.panning { cursor: grabbing; }
/* menu contextual del clic derecho sobre el fondo del mapa (ver
   initMapContextMenu en dashboard.js) -- "+ Nuevo dispositivo" y "+ Nuevo
   enlace" vivian como botones del toolbar, ahora salen de aca porque
   pedido explicito. position:fixed + clientX/clientY directo, igual que
   .map-marquee, para no depender de cuanto este scrolleado el mapa. */
.map-context-menu {
  position: fixed; z-index: 20; list-style: none; margin: 0; padding: .3rem;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .4); min-width: 11rem;
}
.map-context-menu li { display: block; }
.map-context-menu button {
  display: block; width: 100%; text-align: left; border: none; background: none;
  padding: .5rem .7rem; border-radius: calc(var(--radius) - 4px); font-size: .85rem;
  height: auto;
}
.map-context-menu button:hover { background: var(--accent); }
/* modo seleccion multiple (ver toggleMapSelectMode en dashboard.js):
   arrastrar el fondo dibuja un lazo en vez de scrollear -- touch-action:
   none aca (y solo aca) para que un dedo arrastrando en el celular haga el
   lazo en vez de que el navegador lo interprete como touch-scroll. */
.map-diagram-scroll.select-mode { cursor: crosshair; touch-action: none; }
/* Mapa/Mapa2 esconden el header y las pestanas (hide_chrome en dashboard.py)
   para aprovechar toda la pantalla -- pedido explicito, "sacar el logo, las
   pestañas, etc" -- este es el unico link de vuelta que les queda. */
.map-back-btn { display: inline-flex; margin: 1rem 0 .75rem; }
.map-toolbar { display: flex; align-items: center; gap: .6rem; margin-bottom: .5rem; flex-wrap: wrap; }
.map-zoom-controls { display: flex; gap: .3rem; }
.map-zoom-controls .btn-scan { min-width: 2.5rem; padding-left: .5rem; padding-right: .5rem; }

/* Mapa2 (prototipo con vis-network): el propio vis-network maneja su
   zoom/pan/fisica internamente, aca solo se define el marco. */
#mapa2-network {
  height: 70vh; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--card);
}
/* mano/puntero: reemplaza el viejo boton unico "Seleccionar varios" --
   pedido explicito, dos iconos en vez de un boton que cambiaba de texto.
   Mutuamente excluyentes (ver setMapSelectMode en dashboard.js), mano
   activa por default = arrastrar el mapa; puntero activo = lazo de
   seleccion multiple. Mismo grupo visual que .map-zoom-controls (par de
   botones pegados), pero cuadrados y con icono en vez de texto. */
.map-tool-group { display: flex; gap: .3rem; }
.map-tool-btn {
  width: 1.9rem; height: 1.9rem; padding: .35rem; display: flex; align-items: center; justify-content: center;
  background: var(--secondary); color: var(--secondary-foreground); border: 1px solid var(--border); border-radius: var(--radius);
  cursor: pointer;
}
.map-tool-btn:hover { background: var(--accent); }
.map-tool-btn.active { background: var(--destructive); color: var(--destructive-foreground); border-color: transparent; }
.map-tool-icon { width: 100%; height: 100%; fill: currentColor; }
/* rectangulo del lazo -- position:fixed (no absolute) para que sus
   coordenadas sean directamente clientX/clientY sin importar cuanto este
   scrolleado el viewport (ver startMapMarquee en dashboard.js). */
.map-marquee {
  position: fixed; z-index: 5; pointer-events: none;
  border: 1px dashed #38bdf8; background: rgba(56, 189, 248, .15);
}
.map-diagram-canvas {
  position: relative;
  /* "position: relative" SOLO crea contexto de apilamiento propio si
     ademas tiene z-index numerico -- sin esto, el z-index:-1 de
     .map-draw-layer (mas abajo) no queda contenido aca adentro, se
     escapa hacia arriba en el arbol y termina pintando detras del fondo
     de .map-diagram-scroll entero (bug real: las figuras quedaban
     invisibles). isolation:isolate fuerza un contexto de apilamiento
     propio sin tener que jugar con numeros de z-index en cascada. */
  isolation: isolate;
}
/* capa de figuras de dibujo libre (boton "Dibujar" del menu contextual) --
   SIEMPRE atras de todo (z-index -1, mas atras que .map-links-layer en 0
   y .map-diagram-node en 1) asi un rectangulo grande marcando un galpon
   nunca tapa un dispositivo o enlace real. Cada figura es un <g> con
   transform="translate(x,y)" (ver index.html) para poder tener todos sus
   elementos internos en coordenadas LOCALES (0,0 al ancho/alto real) --
   mover la figura es solo cambiar el transform del <g>, redimensionarla
   es solo cambiar width/height/points de los hijos, sin recalcular nada
   absoluto (ver dashboard.js: onMapShapePointerDown/Move/Up). */
.map-draw-layer { position: absolute; top: 0; left: 0; z-index: -1; pointer-events: none; }
.map-draw-fill { pointer-events: fill; cursor: grab; fill-opacity: .18; stroke-width: 2; }
.map-draw-fill-linea { fill-opacity: 0; stroke-width: 3; pointer-events: stroke; }
.map-draw-text {
  font-size: .85rem; font-weight: 700; text-anchor: middle; dominant-baseline: middle;
  pointer-events: fill; cursor: grab;
}
/* cuadradito en la esquina inferior derecha para redimensionar -- discreto
   (opacity baja) hasta que se le pasa el mouse por arriba, para no llenar
   el mapa de cuadraditos si hay muchas figuras. */
.map-draw-handle {
  fill: var(--foreground); opacity: .25; cursor: nwse-resize; pointer-events: fill;
}
.map-draw-handle:hover { opacity: .8; }
/* figura seleccionada (modo puntero, ver toggleMapSelected en
   dashboard.js) -- mismo azul que .map-diagram-selected para los
   dispositivos, pero via filter:drop-shadow en vez de box-shadow (eso no
   sirve para un <g> con hijos de forma irregular como el triangulo o la
   linea; drop-shadow si sigue el contorno real, duplicado para que se
   note mas como anillo que como sombra). */
.map-draw-shape.map-draw-selected { filter: drop-shadow(0 0 3px #38bdf8) drop-shadow(0 0 3px #38bdf8); }
/* capa de lineas manuales entre dispositivos ("+ Nuevo enlace") -- primer
   hijo del canvas (z-index 0, ver .map-diagram-node mas abajo que es 1) asi
   las lineas quedan SIEMPRE detras de los cuadros, nunca tapandolos. Mismo
   sistema de coordenadas "de autor" que node.style.left/top (ver
   dashboard.py:mapa, arma x1/y1/x2/y2 con la posicion de cada extremo), asi
   que zoom/scroll les afecta igual que a los cuadros, sin cuentas aparte. */
.map-links-layer { position: absolute; top: 0; left: 0; z-index: 0; pointer-events: none; }
.map-links-layer line { pointer-events: stroke; cursor: pointer; }
/* puntos intermedios (waypoints) de un enlace -- pedido explicito: "que
   las lineas tengan puntos donde corregir el recorrido, no una linea
   recta", para poder esquivar cuando dos enlaces se superponen.
   Arrastrar un segmento (la linea de arriba) donde todavia no hay un
   punto crea uno ahi mismo y lo arrastra al toque (ver
   handleLinksLayerPointerDown en dashboard.js, mismo gesto que "arrastrar
   la ruta" de Google Maps); clic derecho sobre un punto ya puesto lo
   saca. Radio grande a proposito (mas que el ancho de la linea, 2px) para
   que sea facil de agarrar con el mouse. */
.map-link-point {
  fill: var(--card); stroke: #38bdf8; stroke-width: 2px;
  pointer-events: fill; cursor: move;
}
.map-link-point:hover { fill: #38bdf8; }
/* "ancla" pegada a un dispositivo, solo se dibuja mientras el enlace no
   tiene NINGUN waypoint propio (ver rebuildMapLinkGroup) -- pedido
   explicito: "poder agregar waypoints en los dispositivos para elegir de
   donde sale la linea". Hueca/punteada para distinguirla a simple vista
   de un waypoint ya puesto (.map-link-point, solida); al arrastrarla se
   convierte en un waypoint de verdad y deja de dibujarse. */
.map-link-anchor {
  fill: var(--card); stroke: #38bdf8; stroke-width: 1.5px; stroke-dasharray: 2,2;
  opacity: 0.8; pointer-events: fill; cursor: move;
}
.map-link-anchor:hover { fill: #38bdf8; opacity: 1; }
/* trafico en vivo de un enlace con interfaz cargada (ver startMapaLive en
   dashboard.js, pide /api/mapa-link-traffic cada 2s junto con el ping) --
   un cartelito solido (mismo look que .chart-tooltip: fondo + borde del
   tema) para que se lea con contraste encima de cualquier color de linea
   o de fondo del mapa (ahora personalizable), en vez de flotar suelto
   arriba de la linea. El tamano del rect se recalcula en JS a partir del
   getBBox() del texto (ver positionMapLinkTrafficBox en dashboard.js)
   porque el ancho cambia con cada numero nuevo cada 2s. */
/* pointer-events: fill/auto explicito -- si no, heredan el "none" de
   .map-links-layer de arriba y los clics les pasan de largo. Sin esto,
   el cartelito tapaba visualmente el tramo de la linea que queda debajo
   (es mas ancho que el stroke de 2px) SIN volverse clickeable el mismo,
   dejando una zona muerta: no se podia editar el enlace clickeando ahi
   (bug real, reportado). */
.map-link-traffic-bg { fill: var(--background); stroke: var(--border); stroke-width: 1px; pointer-events: fill; cursor: pointer; }
.map-link-traffic { font-size: .65rem; font-weight: 600; fill: var(--foreground); dominant-baseline: middle; pointer-events: fill; cursor: pointer; }
/* dispositivo ya elegido como "primer extremo" del enlace en curso (ver
   handleMapLinkNodeClick en dashboard.js) -- mismo anillo que la seleccion
   multiple, para no inventar un segundo lenguaje visual. */
.map-link-picking { z-index: 2; box-shadow: 0 0 0 3px #fbbf24; }
.map-diagram-scroll.link-mode { cursor: crosshair; }

/* cada nodo es una cajita HTML (no SVG) para que el nombre se vea bien y
   arrastrar sea simple -- x/y representan el CENTRO (translate -50%/-50%),
   asi el calculo de arrastre es directo, sin depender de cuanto mide la
   caja segun el largo del nombre. */
.map-diagram-node {
  position: absolute; z-index: 1; transform: translate(-50%, -50%);
  touch-action: none; user-select: none; -webkit-user-drag: none; cursor: grab;
  padding: .25rem .5rem; border-radius: calc(var(--radius) - 4px);
  border: 2px solid; font-size: .7rem; font-weight: 600; line-height: 1.2;
  /* nombre completo SIEMPRE visible (pedido explicito, ya no se trunca) --
     white-space: normal + un max-width moderado lo envuelve en un par de
     lineas en vez de cortarlo con "...". DIAGRAM_SPACING_X/Y en dashboard.py
     esta calibrado para que ni siquiera un cuadro de 2 lineas se pise con
     el de al lado en el layout default. */
  white-space: normal; max-width: 11rem; overflow-wrap: break-word;
  text-align: center; text-decoration: none; display: inline-block;
}
.map-diagram-node:active { cursor: grabbing; }
/* mismo tratamiento que los dispositivos manuales (ver mas abajo): todo el
   rectangulo solido de un color segun el tipo, no solo el borde -- se pone
   rojo entero si no responde el ping (.map-diagram-offline, mas abajo),
   asi que este color de aca es en los hechos "el color mientras esta
   online". */
.map-diagram-node-radio { background-color: #38bdf8; border-color: #38bdf8; color: #000000; }
.map-diagram-node-switch { background-color: #a1a1aa; border-color: #a1a1aa; color: #000000; }
.map-diagram-node-mimosa { background-color: #fb923c; border-color: #fb923c; color: #000000; }
.map-diagram-ccr { background: var(--muted); color: var(--foreground); border-color: var(--muted-foreground); }
/* z-index arriba de los demas cuadros para que el anillo no quede tapado
   por el vecino de al lado cuando estan pegados. */
.map-diagram-selected { z-index: 2; box-shadow: 0 0 0 3px #38bdf8; }
/* dispositivos agregados a mano (boton "+ Nuevo dispositivo") -- todo el
   cuadro (fondo, borde y texto) es del color que el usuario eligio, viene
   inline desde el template/JS; solo el nombre va adentro, sin icono. */
.map-diagram-manual { cursor: grab; }
.map-diagram-manual:active { cursor: grabbing; }
.map-shape-rectangulo { border-radius: calc(var(--radius) - 4px); }
.map-shape-circulo { border-radius: 999px; padding: .4rem 1rem; }

/* formas "solo icono" (switch/router/antena/torre/servidor/camara/nube):
   sin fondo de color, un iconito + el nombre debajo (como un pictograma
   de diagrama de red) en vez de un cuadro solido -- el color elegido lo
   lleva el icono (fill: currentColor en el svg), el nombre se queda
   siempre en el color de texto normal del tema para que sea legible sin
   importar que color se haya elegido. OJO: esta lista tiene que
   coincidir exactamente con MAP_ICON_SHAPES en dashboard.js y con el
   {% if %} de index.html -- un forma que quede afuera de las tres partes
   sale con una caja de color de fondo en vez del icono solo (bug real,
   le paso a "nube": el border-radius de arriba no alcanzaba para que se
   viera como nube de verdad, quedaba un ovalo). */
.map-shape-switch, .map-shape-router, .map-shape-antena, .map-shape-torre,
.map-shape-servidor, .map-shape-camara, .map-shape-nube {
  background: none; border: none; padding: 0; max-width: 5rem;
  display: flex; flex-direction: column; align-items: center; gap: .2rem;
}
.map-shape-icon { width: 1.9rem; height: 1.9rem; flex: 0 0 auto; }
.map-shape-icon svg { width: 100%; height: 100%; display: block; fill: currentColor; }
.map-shape-label { color: var(--foreground); font-size: .7rem; font-weight: 600; }

/* si no responde el ping, el cuadro se pone ROJO SOLIDO entero -- pisa el
   color de tipo/elegido de arriba (real o manual, misma regla para los
   dos) sin importar cual sea. Rojo solido de verdad (--danger-fg, no el
   --danger-bg translucido que se usa para badges chicos en otras partes
   del dashboard) porque fue pedido explicito: "se ponga en color ROJO",
   no un tinte. !important porque le tiene que ganar a un inline-style (el
   color elegido de un dispositivo manual). Para las formas "solo icono"
   (sin fondo, ver mas arriba) solo el icono se pone rojo, el nombre abajo
   se queda legible como siempre. */
.map-diagram-node.map-diagram-offline:not(.map-shape-switch):not(.map-shape-router):not(.map-shape-antena):not(.map-shape-torre):not(.map-shape-servidor):not(.map-shape-camara):not(.map-shape-nube) {
  background-color: var(--danger-fg) !important; border-color: var(--danger-fg) !important;
  color: #ffffff !important;
}
.map-diagram-manual.map-diagram-offline.map-shape-switch .map-shape-icon,
.map-diagram-manual.map-diagram-offline.map-shape-router .map-shape-icon,
.map-diagram-manual.map-diagram-offline.map-shape-antena .map-shape-icon,
.map-diagram-manual.map-diagram-offline.map-shape-torre .map-shape-icon,
.map-diagram-manual.map-diagram-offline.map-shape-servidor .map-shape-icon,
.map-diagram-manual.map-diagram-offline.map-shape-camara .map-shape-icon,
.map-diagram-manual.map-diagram-offline.map-shape-nube .map-shape-icon {
  color: var(--danger-fg) !important;
}

/* dialogo "Nuevo dispositivo" -- <dialog> nativo, sin libreria de modal.
   position:fixed + inset:0 + margin:auto centra SIEMPRE contra el
   viewport visible, sin depender del centrado automatico del navegador
   para <dialog> -- ese calculaba el centro contra el ancho total de la
   pagina (que con el canvas del mapa de miles de px de ancho quedaba
   larguisimo) en vez del viewport, y el dialogo aparecia lejos a la
   derecha, con una barra de scroll horizontal para poder verlo. */
/* OJO: #map-shape-dialog (el dialogo "Dibujar") tiene que estar en TODAS
   estas listas de selectores -- se quedo afuera al agregarlo (bug real,
   encontrado despues): sin esto usa el estilo nativo default de
   <dialog> del navegador, sin ancho/centrado/tema propios, mas angosto
   de lo que parece a simple vista. */
#map-device-dialog, #map-link-dialog, #map-shape-dialog {
  position: fixed; inset: 0; margin: auto;
  background: var(--card); color: var(--foreground); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1.25rem 1.5rem; width: min(22rem, 90vw);
  max-height: 90vh; overflow: auto; height: fit-content;
}
/* el dialogo de Dibujar necesita mas ancho que los demas -- pedido
   explicito: forma/etiqueta/color en UNA sola fila horizontal (como una
   barra de herramientas de Paint), no una fila por campo como el resto
   de los dialogos. Con el ancho angosto de arriba, los 9 cuadraditos de
   color solos ya no entran en una linea. */
#map-shape-dialog { width: min(30rem, 92vw); }
#map-device-dialog::backdrop, #map-link-dialog::backdrop, #map-shape-dialog::backdrop { background: rgba(0, 0, 0, .5); }
#map-device-dialog h3, #map-link-dialog h3, #map-shape-dialog h3 { margin: 0 0 1rem; font-size: 1rem; }
/* una fila por campo, cada una un flex container aparte (nada de grid +
   display:contents -- con eso el label de una fila terminaba pareado con
   el input de la fila siguiente, quedaba todo "corrido" un lugar) */
#map-device-form, #map-link-form, #map-shape-form { display: flex; flex-direction: column; gap: .7rem; }
.map-device-row { display: flex; align-items: center; gap: .8rem; }
.map-device-row label { flex: 0 0 6.5rem; font-size: .82rem; color: var(--muted-foreground); }
.map-device-row input[type=text], .map-device-row select {
  flex: 1 1 auto; min-width: 0;
  height: 2.1rem; padding: 0 .6rem; border-radius: calc(var(--radius) - 2px);
  border: 1px solid var(--border); background: var(--input); color: var(--foreground); font-size: .88rem;
}
/* paleta fija de colores (cuadraditos) en vez de un <input type=color>
   libre -- pedido explicito para que los dispositivos no terminen todos
   de tonos ligeramente distintos e imposibles de igualar a ojo. */
.color-swatches { display: flex; flex-wrap: wrap; gap: .4rem; flex: 1 1 auto; }
.color-swatch {
  width: 1.6rem; height: 1.6rem; padding: 0; border-radius: calc(var(--radius) - 4px);
  border: 1px solid var(--border); cursor: pointer;
}
.color-swatch-selected { box-shadow: 0 0 0 2px var(--card), 0 0 0 4px #38bdf8; }
/* dialogo de Dibujar -- forma/etiqueta/color en UNA fila horizontal en
   vez de una fila por campo (pedido explicito, como una barra de
   herramientas de Paint). Cada campo es una columnita chica con su label
   arriba (no al costado como .map-device-row, no entrarian los tres en
   el ancho del dialogo con el label largo al costado). wrap por si
   alguna vez el dialogo queda mas angosto que los tres juntos (celular). */
.map-shape-row { display: flex; align-items: flex-start; gap: .9rem; flex-wrap: wrap; }
.map-shape-field { display: flex; flex-direction: column; gap: .35rem; min-width: 0; }
.map-shape-field label { font-size: .72rem; color: var(--muted-foreground); white-space: nowrap; }
.map-shape-field-grow { flex: 1 1 8rem; }
.map-shape-field select, .map-shape-field input[type=text] {
  height: 2.1rem; padding: 0 .6rem; border-radius: calc(var(--radius) - 2px);
  border: 1px solid var(--border); background: var(--input); color: var(--foreground); font-size: .88rem;
  width: 100%; min-width: 0;
}
.map-device-actions { display: flex; justify-content: flex-end; gap: .6rem; margin-top: .6rem; }
.map-device-delete { margin-right: auto; }

/* pestana Alertas: log de lan10_auto_reboot.py (ver dashboard.py:alertas). */
.alertas-escalados { display: flex; flex-wrap: wrap; gap: .6rem; margin-bottom: 1.2rem; }
.alertas-escalado-card {
  display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem;
  border: 1px solid var(--danger-bg); border-radius: var(--radius);
  background: var(--card); text-decoration: none; color: inherit;
}
.alertas-escalado-card:hover { background: var(--accent); }
.alertas-escalado-name { font-weight: 600; }
.alertas-log { display: flex; flex-direction: column; gap: .4rem; }
.alertas-log-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  padding: .4rem .6rem; border-radius: calc(var(--radius) - 2px); background: var(--card);
}
.alertas-log-ts { font-size: .78rem; color: var(--muted-foreground); min-width: 9.5rem; }

.node-card .card-content { padding-bottom: .6rem; }
.node-card-body { display: flex; gap: 1rem; align-items: stretch; }
.node-card-body table { flex: 1 1 auto; min-width: 0; }
/* fotos de producto bajadas de los sitios de los fabricantes -- fondo blanco
   fijo (no var(--card)) a proposito: son fotos de catalogo pensadas para
   fondo claro, se ven mal recortadas sobre el tema oscuro del dashboard.
   align-items:stretch (arriba) hace que la imagen tome la misma altura que
   la tabla de al lado -- se define solo el ancho, la altura queda en 100%. */
.node-image { flex: 0 0 9rem; width: 9rem; height: 100%; min-height: 5.5rem; object-fit: contain; background: #fff; border-radius: var(--radius); padding: .3rem; }
.node-actions { margin-top: .5rem; }

/* pantallas angostas (celulares) -- .client-ping tiene min-width:24rem que en
   escritorio evita que el panel de diagnostico quede too angosto, pero en un
   celular ese minimo por si solo ya ocupa casi todo el viewport y empuja
   .client-details a 0 (queda invisible). Se apilan verticalmente y se sacan
   los anchos minimos/fijos pensados para escritorio. */
@media (max-width: 768px) {
  body { margin: 1rem; padding-bottom: 4rem; }
  /* Safari/Chrome mobile hacen zoom automatico al enfocar un input con
     font-size menor a 16px -- se sube ahi nomas (no en escritorio, donde
     no molesta y .85rem se ve mejor con el resto de la UI) */
  input[type=text], input[type=password] { font-size: 16px; }
  .client-block { flex-direction: column; }
  .client-details { max-width: none; flex: 1 1 auto; }
  .client-ping { min-width: 0; flex: 1 1 auto; }
  .diag-content { flex-direction: column; height: auto; }
  .diag-half { flex: none; }
  .diag-half + .diag-half { border-left: none; border-top: 1px solid var(--border); }
  /* altura fija (no min-height) para que .ping-log haga scroll interno en vez de
     crecer sin techo con cada ping. El resumen (minimo/maximo/promedio/perdidos)
     ahora se renderiza siempre, con "-" antes del primer resultado (ver
     statsSummaryHtml en dashboard.js), asi que .ping-out ya no salta de
     tamano al llegar el primer ping -- el contenido es igual de alto desde
     el arranque, no hace falta adivinarle un min-height aparte. */
  .ping-log { height: 8rem; }
  form { flex-wrap: wrap; }
  input[type=text], input[type=password] { width: 100%; }
  /* .card recorta (overflow:hidden) lo que no entra -- en escritorio el
     ancho sobra, pero en un celular un texto largo sin espacios (hostname,
     nombre de fabricante, etc.) empuja la tabla mas ancha que la tarjeta y
     el sobrante queda invisible, cortado a la mitad de una palabra. */
  .card-content > table td, .dhcp-sub td, .dhcp-sub th, .pool-table td {
    overflow-wrap: anywhere;
  }
  /* 9rem (etiqueta) + 11rem (columna MAC) pensados para escritorio no
     entran juntos en el ancho real de un celular -- a la columna Hostname
     casi no le queda lugar y el overflow-wrap de arriba la parte letra por
     letra. Se angostan ambas para que quede espacio utilizable. */
  .card-content > table td:first-child { width: 7rem; white-space: normal; }
  .dhcp-sub td:first-child, .dhcp-sub th:first-child { width: auto; max-width: 45%; }
  .node-card-body { flex-direction: column; align-items: center; }
  .node-image { width: 9rem; height: 9rem; }
  /* .card-header es una fila flex sin wrap -- con un nombre de cliente/nodo
     largo en un celular angosto, empuja el badge/boton de reboot fuera del
     ancho visible y .card los recorta (overflow:hidden). Se deja pasar a
     una segunda linea, y se corta el titulo si es una palabra sin espacios
     (nombres pegados tipo "JuanPerezCasa2") para que no vuelva a pasar lo
     mismo dentro del propio titulo. */
  .card-header { flex-wrap: wrap; }
  .card-title { overflow-wrap: anywhere; }
}
.node-card.row-highlight { background: var(--warning-bg); transition: none; }

/* estados que calcula el monitor del cerebro (ver applyMapaLiveStatus en
   dashboard.js). down, inalcanzable y probable_caido llevan .map-diagram-offline
   (rojo, reglas de arriba); la causa se distingue por el anillo. */
.map-diagram-node.map-estado-down { outline: 3px solid var(--danger-fg); outline-offset: 4px; border-radius: 4px; }
.map-diagram-node.map-estado-pendiente { opacity: .6; }
.map-diagram-node.map-estado-probable_caido { outline: 3px dashed var(--danger-fg); outline-offset: 4px; border-radius: 4px; }
.map-diagram-manual.map-estado-probable_caido .map-shape-icon { color: var(--danger-fg) !important; }

/* leyenda fija abajo a la derecha (abajo a la izquierda esta el boton de
   volver arriba) */
.map-leyenda {
  position: fixed; right: 1rem; bottom: 1rem; z-index: 900; max-width: 19rem;
  background: var(--card); color: var(--card-foreground); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .5rem .75rem; font-size: .8rem;
}
.map-leyenda summary { cursor: pointer; font-weight: 600; }
.map-leyenda h4 { margin: .6rem 0 .3rem; font-size: .75rem; color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .04em; }
.map-leyenda ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; }
.map-leyenda li { display: flex; align-items: center; gap: .5rem; }
.map-leyenda-muestra { flex: none; width: .9rem; height: .9rem; border-radius: 3px; border: 1px solid var(--border); }
.map-leyenda-muestra.caido { background: var(--danger-fg); }
.map-leyenda-muestra.anillo { background: var(--danger-fg); outline: 2px solid var(--danger-fg); outline-offset: 2px; }
.map-leyenda-muestra.punteado { border: 2px dashed var(--danger-fg); }
.map-leyenda-muestra.midiendo { background: var(--muted-foreground); opacity: .6; }

/* Registro de acciones (abajo a la izquierda) */
.map-acciones {
  position: fixed; left: 1rem; bottom: 1rem; z-index: 900; width: 22rem; max-width: calc(100vw - 2rem);
  background: var(--card); color: var(--card-foreground); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .5rem .75rem; font-size: .8rem;
}
.map-acciones summary { cursor: pointer; font-weight: 600; }
.map-acciones-n { color: var(--muted-foreground); font-weight: 400; }
.map-acciones ul { list-style: none; margin: .4rem 0 0; padding: 0; display: grid; gap: .35rem; max-height: 14rem; overflow-y: auto; }
.map-acciones li { display: grid; grid-template-columns: auto 1fr; column-gap: .5rem; padding: .25rem .35rem; border-radius: 4px; }
.map-acciones li time { color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
.map-acciones li small { grid-column: 2; color: var(--muted-foreground); }
.map-acciones li.nueva { animation: accion-nueva 4s ease-out; }
@keyframes accion-nueva { from { background: color-mix(in srgb, var(--primary, #3b82f6) 35%, transparent); } to { background: transparent; } }
