: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 (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: esa pagina ya no scrollea 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 esconde 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 le 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; }
#map-select-btn.active { background: var(--destructive); color: var(--destructive-foreground); border-color: transparent; }
/* 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);
}
/* borde punteado propio (no el del contenedor) -- marca hasta donde se
   puede arrastrar un cuadro (ver el clamp en onMapNodePointerMove) y hasta
   donde llega el scroll. */
.map-diagram-canvas {
  position: relative;
  outline: 2px dashed var(--border); outline-offset: -1px;
}
/* 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; }
/* 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; }
/* blob organico (bordes asimetricos) para diferenciarla de circulo/rectangulo
   sin tener que armar una nube "de verdad" con pseudo-elementos */
.map-shape-nube { border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%; }

/* forma switch/router: 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. */
.map-shape-switch, .map-shape-router {
  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 switch/router (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) {
  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 {
  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. */
#map-device-dialog, #map-link-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;
}
#map-device-dialog::backdrop, #map-link-dialog::backdrop { background: rgba(0, 0, 0, .5); }
#map-device-dialog h3, #map-link-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 { 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;
}
.map-device-row input[type=color] {
  width: 3rem; height: 2.1rem; padding: .15rem; border-radius: calc(var(--radius) - 2px);
  border: 1px solid var(--border); background: var(--input); cursor: pointer;
}
.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; }
