/* sip-monitor - foglio di stile unico (login + pannelli).
   Tema scuro da strumento di osservabilità SIP: fondo blu-nero, tutto in
   monospace, accento ciano per gli stati attivi e viola per un host del
   ladder. I messaggi SIP sono colorati per classe (--sip-*). */

/* JetBrains Mono self-hosted (subset latin): se i file non ci sono si
   ricade sullo stack di sistema senza altri effetti. */
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-400.woff2") format("woff2");
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-600.woff2") format("woff2");
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-700.woff2") format("woff2");
}

:root {
  color-scheme: dark;

  --bg: #0a0e17;
  --panel: #0e1420;
  --panel-2: #121a28;
  --inset: #0b1018;
  --border: #1c2434;
  --border-strong: #2a3446;

  --text: #e6edf3;
  --text-muted: #8492a6;
  --text-dim: #5b6675;

  --accent: #5ec6e6; /* ciano: stati attivi, selezione, link */
  --accent-strong: #7dd3ec; /* fill bottoni */
  --on-accent: #06131b; /* testo su fill ciano */
  --accent-soft: rgba(94, 198, 230, 0.12);
  --violet: #a78bfa; /* host secondario nel ladder */

  /* Classi dei messaggi SIP: ladder, lista e cella CAUSE. */
  --sip-req: #6cc7e8;
  --sip-ack: #9aa7b8;
  --sip-1xx: #e0b23f;
  --sip-2xx: #4fd08a;
  --sip-3xx: #e0b23f;
  --sip-err: #f0555b; /* >=4xx e CANCEL */

  /* Fondi tenui derivati, per i pochi elementi a pillola colorata. */
  --tint-ok: rgba(79, 208, 138, 0.12);
  --tint-warn: rgba(224, 178, 63, 0.12);
  --tint-err: rgba(240, 85, 91, 0.12);
  --tint-info: rgba(108, 199, 232, 0.12);

  --mono: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
    "Cascadia Code", Menlo, Consolas, monospace;
  --radius: 12px;
  --radius-sm: 6px;
  --pill: 999px;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}

* {
  box-sizing: border-box;
}

/* Scrollbar sottile e scura, sia Firefox sia WebKit. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: var(--pill);
}

::-webkit-scrollbar-thumb:hover {
  background: #3a465c;
  background-clip: padding-box;
}

::selection {
  color: var(--text);
  background: rgba(94, 198, 230, 0.28);
}

body {
  margin: 0;
  font-family: var(--mono);
  font-size: 13.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: var(--bg);
}

button {
  font: inherit;
  cursor: pointer;
}

/* ------------------------------------------------------------- login */

body.login-page {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: 1.5rem;
}

.login-card {
  width: 21rem;
  max-width: 100%;
  padding: 1.75rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.login-card h1 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--text);
}

.login-card .subtitle {
  margin: 0.3rem 0 1.5rem;
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--text-muted);
}

.field {
  display: block;
  margin-bottom: 0.9rem;
}

.field > span {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

input[type="text"],
input[type="password"],
input[type="search"] {
  width: 100%;
  padding: 0.45rem 0.7rem;
  font: inherit;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

input::placeholder {
  color: var(--text-dim);
}

input[type="search"]::-webkit-search-cancel-button {
  filter: invert(1) opacity(0.45);
}

input:hover {
  border-color: var(--border-strong);
}

input:focus {
  border-color: var(--accent);
  outline: 0;
  box-shadow: 0 0 0 2px var(--accent-soft);
}

/* Focus sempre visibile, anche dove il :focus non basta (righe, bottoni). */
input:focus-visible,
button:focus-visible,
summary:focus-visible,
tr:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.btn-primary {
  width: 100%;
  margin-top: 0.6rem;
  padding: 0.5rem;
  font-weight: 700;
  color: var(--on-accent);
  background: var(--accent-strong);
  border: 1px solid var(--accent-strong);
  border-radius: var(--pill);
}

.btn-primary:hover:not(:disabled) {
  background: #98dff2;
  border-color: #98dff2;
}

.btn-primary:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Riservato sempre, così il form non salta quando compare l'errore. */
.form-error {
  min-height: 1.15rem;
  margin: 0.8rem 0 0;
  font-size: 0.78rem;
  color: var(--sip-err);
}

.form-error:not(:empty) {
  padding: 0.45rem 0.7rem;
  background: var(--tint-err);
  border: 1px solid rgba(240, 85, 91, 0.35);
  border-radius: var(--radius-sm);
}

/* ------------------------------------------------- struttura app */

/* Sidebar fissa a sinistra, il resto dell'app nella colonna accanto. */
body.app-page {
  display: flex;
  height: 100vh;
}

.shell {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

/* ------------------------------------------------------------ sidebar */

.sidebar {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  gap: 0.5rem;
  width: 13rem;
  padding: 0.75rem 0.6rem;
  background: var(--panel);
  border-right: 1px solid var(--border);
}

.brand {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  align-items: flex-start;
  padding: 0.4rem 0.5rem 0.9rem;
}

/* "VIPER": monospace bold, la V in ciano e il resto nel testo normale. */
.brand-name {
  font-family: var(--mono);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--text);
}

.brand-accent {
  color: var(--accent);
}

/* Sottotitolo minuto, sotto il nome: nella sidebar e' una didascalia
   ("advanced monitoring tools"), sulla scheda di accesso e' la sigla
   sciolta. Sono due testi diversi di proposito, e la sidebar tiene i
   valori originali perche' la sua stringa e' corta e ci sta su una riga:
   la colonna e' larga 13rem, che al corpo del sottotitolo fa 28 caratteri
   scarsi, e la didascalia ne occupa 25. */
.brand-sub {
  font-size: 0.6rem;
  font-weight: 400;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

/* Le iniziali della sigla sulla scheda di accesso. Il colore arriva da
   `.brand-accent`, la stessa classe della V del wordmark: qui si aggiunge
   solo il peso, cosi' le cinque lettere si staccano come nel marchio.
   Non e' scritta per `.brand-sub`: nella sidebar la sigla non c'e'. */
.login-card .subtitle .brand-accent {
  font-weight: 700;
}

.nav {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

/* La barra ciano a sinistra sta sempre lì, trasparente finché la voce non è
   quella corrente: così il testo non si sposta al cambio di vista. */
.nav-item {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  padding: 0.45rem 0.65rem;
  font-size: 0.78rem;
  color: var(--text-muted);
  text-decoration: none;
  border-left: 2px solid transparent;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

/* Il `display: flex` qui sopra batte il `display: none` che il browser darebbe
   da solo all'attributo hidden: senza questa regola la voce IMPOSTAZIONI
   resterebbe visibile ai non-admin, cioe' esattamente cio' che deve evitare. */
.nav-item[hidden] {
  display: none;
}

.nav-item:hover {
  color: var(--text);
  background: var(--panel-2);
}

.nav-item.is-current {
  font-weight: 700;
  color: var(--accent);
  background: var(--accent-soft);
  border-left-color: var(--accent);
}

/* Maiuscolo solo tipografico, come i titoli dei pannelli: nel DOM il testo
   resta "Chiamate attive", così gli screen reader lo leggono come parola. */
.nav-label {
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.nav-icon {
  font-size: 0.7rem;
  line-height: 1;
}

.topbar {
  display: flex;
  flex-shrink: 0;
  gap: 0.75rem;
  align-items: center;
  padding: 0.55rem 1rem;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}

.topbar h1 {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text);
}

.topbar .spacer {
  flex: 1;
}

.topbar .who {
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* Stato del canale realtime: presente ma senza rubare attenzione. */
.conn {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  font-size: 0.74rem;
  color: var(--text-dim);
}

.conn-dot {
  width: 0.45rem;
  height: 0.45rem;
  background: var(--text-dim);
  border-radius: 50%;
}

.conn[data-state="live"] {
  color: var(--sip-2xx);
}

.conn[data-state="live"] .conn-dot {
  background: var(--sip-2xx);
  box-shadow: 0 0 6px rgba(79, 208, 138, 0.6);
}

.conn[data-state="reconnecting"] {
  color: var(--sip-1xx);
}

.conn[data-state="reconnecting"] .conn-dot {
  background: var(--sip-1xx);
  animation: pulse 1.2s ease-in-out infinite;
}

@keyframes pulse {
  50% {
    opacity: 0.25;
  }
}

/* Bottone di servizio (Riprova): pillola con solo il bordo. */
.btn-quiet {
  padding: 0.3rem 0.85rem;
  font-size: 0.78rem;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

.btn-quiet:hover {
  color: var(--text);
  background: var(--panel-2);
  border-color: var(--border-strong);
}

/* Azione principale della topbar. */
.btn-accent {
  padding: 0.3rem 0.9rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--on-accent);
  background: var(--accent-strong);
  border: 1px solid var(--accent-strong);
  border-radius: var(--pill);
}

.btn-accent:hover:not(:disabled) {
  background: #98dff2;
  border-color: #98dff2;
}

.btn-accent:disabled {
  opacity: 0.5;
  cursor: default;
}

.workspace {
  display: grid;
  flex: 1;
  gap: 0.75rem;
  min-height: 0; /* senza questo i pannelli non scrollano dentro il grid */
  padding: 0.75rem;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
}

.panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.panel-head {
  display: flex;
  flex-shrink: 0;
  gap: 0.6rem;
  align-items: baseline;
  padding: 0.6rem 0.85rem;
  background: var(--panel-2);
  border-bottom: 1px solid var(--border);
}

.panel-head h2 {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text);
}

.panel-head .count {
  font-size: 0.74rem;
  color: var(--text-muted);
}

.panel-head .spacer {
  flex: 1;
}

/* Il range attivo, accanto al titolo della vista Chiamate. */
.range-label {
  font-size: 0.72rem;
  color: var(--text-dim);
}

/* Le due viste del pannello di sinistra: una sola visibile per volta, e
   quella visibile occupa tutto il pannello. */
.view {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
}

.view[hidden] {
  display: none;
}

.panel-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
}

/* -------------------------------------------------------- filtri */

.filters {
  display: flex;
  flex-shrink: 0;
  flex-wrap: wrap;
  gap: 0.45rem 0.6rem;
  align-items: center;
  padding: 0.55rem 0.85rem;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}

/* Toggle finestra temporale: due pillole mutuamente esclusive. */
.segmented {
  display: inline-flex;
  gap: 0.35rem;
}

.segmented button {
  padding: 0.26rem 0.8rem;
  font-size: 0.76rem;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

.segmented button:hover {
  color: var(--text);
  background: var(--panel-2);
  border-color: var(--border-strong);
}

.segmented button[aria-pressed="true"] {
  font-weight: 700;
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
}

/* Campi del range data/ora: etichetta minuta sopra il controllo nativo. */
.range-field {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.range-field input {
  width: auto;
  padding: 0.22rem 0.55rem;
  font-size: 0.74rem;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

.range-field input:hover {
  border-color: var(--border-strong);
}

/* L'icona nativa del picker è nera su fondo scuro: va schiarita. */
.range-field input::-webkit-calendar-picker-indicator {
  filter: invert(1) opacity(0.5);
  cursor: pointer;
}

.range-field input::-webkit-calendar-picker-indicator:hover {
  filter: invert(1) opacity(0.9);
}

/* Filtri per numero (chiamante / chiamato): stessa etichetta minuta dei campi
   data, ma con un campo di testo stretto. Restano distinti dalla ricerca
   generica, che sta in fondo alla barra e pesca anche nel Call-ID. */
.party-field input {
  width: 7.5rem;
  min-width: 0;
  font-variant-numeric: tabular-nums;
}

/* Il campo attivo si accende come le pillole dei filtri booleani: chi guarda
   la barra vede subito che c'è un filtro in vigore. */
.party-field:has(input:not(:placeholder-shown)) {
  color: var(--accent);
}

.party-field input:not(:placeholder-shown) {
  color: var(--accent);
  border-color: var(--accent);
}

.party-field:focus-within {
  color: var(--text);
}

/* I due campi IP. Ereditano tutto da .party-field -- accensione, tetto sotto
   i 900px, allineamento -- e cambiano solo la larghezza: un IPv4 completo e'
   quindici caratteri, piu' dei numeri accanto, e con 7,5rem si leggeva a meta'.
   Non di piu' di cosi': la riga dei filtri e' gia' affollata. */
.ip-field input {
  width: 9.5rem;
}

/* Sotto i 900px la barra va a capo: i due campi si dividono la riga invece di
   lasciare un buco, ma con un tetto -- allargati oltre non si leggono meglio,
   e un numero non ha bisogno di 400px. */
@media (max-width: 900px) {
  .party-field {
    flex: 1 1 8rem;
    max-width: 15rem;
  }

  .party-field input {
    flex: 1;
    width: auto;
    min-width: 0;
  }
}

/* Filtri booleani: stessa pillola, lo stato lo dà la checkbox nativa. */
.check {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  padding: 0.26rem 0.8rem;
  font-size: 0.76rem;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--pill);
  cursor: pointer;
  user-select: none;
}

.check:hover {
  color: var(--text);
  background: var(--panel-2);
  border-color: var(--border-strong);
}

.check input {
  width: 0.8rem;
  height: 0.8rem;
  margin: 0;
  accent-color: var(--accent);
}

.check:has(input:checked) {
  font-weight: 700;
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
}

.check:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.filters .search {
  flex: 1;
  min-width: 11rem;
  max-width: 22rem;
  margin-left: auto;
  padding: 0.26rem 0.8rem;
  font-size: 0.76rem;
}

/* -------------------------------------------------------- tabella */

.calls-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.78rem;
}

.calls-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 0.45rem 0.7rem;
  font-size: 0.68rem;
  font-weight: 600;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-dim);
  background: var(--panel-2);
  box-shadow: inset 0 -1px 0 var(--border);
}

.calls-table tbody td {
  padding: 0.38rem 0.7rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.calls-table tbody tr {
  cursor: pointer;
}

.calls-table tbody tr:hover > td {
  background: var(--panel-2);
}

/* Riga selezionata: due linee ciano sopra e sotto, fondo appena tintato. */
.calls-table tbody tr.is-selected > td,
.calls-table tbody tr.is-selected:hover > td {
  background: var(--accent-soft);
  border-top: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
}

/* Arrivata con l'ultimo evento realtime: si accende e sfuma da sola. */
@keyframes row-arrived {
  from {
    background: var(--accent-soft);
  }
  to {
    background: transparent;
  }
}

.calls-table tbody tr.is-new > td {
  animation: row-arrived 2.5s ease-out;
}

.col-time,
.col-dur,
.col-pdd,
.col-cause {
  font-variant-numeric: tabular-nums;
}

.col-time {
  color: var(--text-muted);
}

.col-dur,
.col-pdd {
  color: var(--text);
  text-align: right;
}

/* A→B può essere lungo: è l'unica colonna elastica.

   Era al 40%. Le altre quattro (Time, Dur, PDD, Cause) portano numeri di
   larghezza nota — `10:59:52`, `00:53`, `410 ms`, `200` — e a 1440 px si
   prendevano 339 px per mostrarne poco più di 160: la percentuale in più la
   pagano loro, e non la stavano usando. */
.col-party {
  max-width: 0;
  width: 62%;
  overflow: hidden;
  white-space: nowrap;
}

/* Il contatore scorre ogni secondo: senza larghezza fissa la colonna
   ballerebbe passando da 09:59 a 10:00.

   STA QUI, e non più in fondo con le altre colonne della vista Utenti, perché
   sotto i 700 px viene revocata: una `@media` non aggiunge specificità, decide
   l'ordine di scrittura. Finché questa regola stava dopo il blocco stretto lo
   vinceva in silenzio, e la colonna teneva i suoi 72 px anche a 375 px, dove
   li paga la colonna che porta i numeri. Ogni override responsive di
   `.active-table .col-dur` deve stare DOPO questa riga. */
.active-table .col-dur {
  min-width: 4.5rem;
}

/* --------------------------------- perché la cella NON tronca più in blocco

   Prima qui c'era `text-overflow: ellipsis` sul <td>: quando il contenuto
   eccedeva, la cella tagliava IN FONDO. Con due soli pezzi (A → B) era quasi
   innocuo; da quando l'utenza di consegna si è aggiunta come terzo pezzo
   (c7c3072) il contenuto è diventato più lungo e a essere tagliato era
   sistematicamente l'ULTIMO — cioè il numero chiamato, che è la risposta alla
   domanda della colonna. Si leggeva `+393477933492 →…`: il chiamante intero e
   il chiamato scomparso.

   Tagliare in fondo è la politica sbagliata per una cella che contiene più
   valori di importanza diversa. Adesso i pezzi stanno in un contenitore flex
   (.party-cell) e lo spazio si RIPARTISCE: ognuno si accorcia per conto suo,
   con i puntini dentro il proprio riquadro, e l'ordine in cui cedono è
   dichiarato invece che casuale.

   Chi cede per primo, e perché (i `flex-shrink` sono 1200 / 8 / 1):
     utenza    contorno: compare solo quando aggiunge qualcosa, e se non c'è
               spazio il suo posto è il tooltip
     chiamante importante, ma è il pezzo che l'utente sta spesso già filtrando
     chiamato  ULTIMO: è il dato principale della colonna e non può sparire

   I tre numeri sono distanti apposta. Flexbox non toglie spazio a uno per
   volta in ordine: lo toglie a tutti insieme, in proporzione al fattore. Con
   1200 contro 8 l'utenza assorbe centocinquanta volte più restringimento del
   chiamante, che è quanto basta perché il chiamante resti intero finché
   l'utenza non è ridotta quasi a niente. Con 200 contro 20 — la prima scelta,
   misurata — il chiamante perdeva già una cifra a 1440 px.

   Il pavimento (`min-width` in ch) fa sì che nessuno dei due numeri si riduca
   ai soli puntini: meglio due monconi leggibili che uno intero e uno sparito.
   Il tetto (`max-width` in rem) resta quello di prima ed è la difesa contro
   l'input ostile: vedi la nota qui sotto. Il valore intero è comunque nel
   tooltip della cella, sempre. */
.party-cell {
  display: flex;
  align-items: baseline;
  min-width: 0;
}

.party-a,
.party-b,
.party-utenza {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.party-a {
  flex: 0 8 auto;
  min-width: 4ch;
}

.party-b {
  flex: 0 1 auto;
  min-width: 9ch;
}

.party-utenza {
  flex: 0 1200 auto;
  margin-left: 0.3rem;
}

/* La freccia non si accorcia: è un simbolo, non un valore, e mezza freccia
   non vuol dire niente. */
.party-arrow {
  flex: 0 0 auto;
}

/* ------------------------------------------- contenimento dell'input ostile
   Il troncamento in JS (format.js) accorcia i valori a 24-40 caratteri, ma non
   può essere l'unica difesa: basta un punto di rendering dimenticato, o un
   valore che arriva da una strada nuova, e la colonna torna a sfondare.

   `max-width: 0` sul <td> non basta da solo, perché in una tabella a layout
   automatico la larghezza minima del contenuto viene comunque dal testo: uno
   <span> inline con 1024 caratteri e `white-space: nowrap` richiede la sua
   riga intera, la tabella la concede e il pannello si mette a scorrere in
   orizzontale. Il tetto va messo sul contenuto, non sulla cella: un
   inline-block con `max-width` HA una larghezza definita, quindi la larghezza
   minima della cella diventa finita e la tabella non si allarga più.

   Misurato con un valore di 500 caratteri a 1440, 800 e 375 px: nessuno
   scroll orizzontale, colonne allineate. Da flex il contenimento è anche più
   solido: un flex item con `min-width: 0` contribuisce ZERO alla larghezza
   minima della cella, quindi un From user da 1024 caratteri non ha più modo
   di allargare la tabella nemmeno in teoria. Il tetto resta comunque, perché
   è la difesa che non dipende dal contesto di layout. */
.party-a,
.party-b {
  max-width: 14rem;
}

.party-utenza {
  max-width: 11rem;
}

/* Sotto i 900px la colonna è più stretta: il tetto scende con lei. */
@media (max-width: 900px) {
  .party-a,
  .party-b {
    max-width: 8rem;
  }

  /* 8rem come gli altri due, e non 6: a 6rem un'utenza di undici cifre fra
     parentesi restava fuori per UN pixel e si mangiava la parentesi chiusa
     con i puntini. Il tetto è una difesa contro i valori assurdi, non deve
     scattare su quelli ordinari. */
  .party-utenza {
    max-width: 8rem;
  }
}

/* ------------------------------------------- schermi stretti: chi cede, e in
   che ordine

   `width: 62%` sulla colonna A→B qui non vale niente, e va detto perché: in
   una tabella a layout automatico la percentuale è un'INDICAZIONE, e perde
   contro la larghezza minima delle altre colonne. Time, Dur, PDD e Cause
   portano numeri che non vanno a capo, e con 0.7rem di respiro per lato si
   prendevano 269 px dei 349 disponibili a 375 px — 88 px dei quali di solo
   padding, per mostrare 160 px di cifre. Alla colonna elastica ne restavano
   80, e il numero chiamato spariva su TUTTE le righe.

   L'ORDINE IN CUI SI CEDE, dal meno al più doloroso:

     1. l'utenza di consegna si accorcia   (sempre, via flex-shrink)
     2. A→B va su due righe                (sotto i 700 px, qui)
        e le etichette di stato si abbreviano, che è lo stesso gradino
     3. la colonna del nodo sparisce       (sotto i 560 px, più giù)
     4. e con lei `Start`, nella sola vista attivi

   La soglia è 700 e non 560 perché le colonne adesso sono SEI: misurato a
   600 px con la vecchia soglia, ad A→B restavano 202 px e il numero chiamato
   era leggibile su 2 righe su 12. Il respiro si stringe dove lo spazio non
   c'è; sopra i 700 px non cambia nulla. */
@media (max-width: 700px) {
  .calls-table tbody td,
  .calls-table thead th {
    padding-right: 0.3rem;
    padding-left: 0.3rem;
  }

  /* Le colonne numeriche si stringono sul contenuto e non un pixel di più:
     così la percentuale della colonna elastica torna a contare. */
  .calls-table .col-time,
  .calls-table .col-start,
  .calls-table .col-dur,
  .calls-table .col-pdd,
  .calls-table .col-cause {
    width: 1%;
  }

  /* La vista Chiamate attive ha lo stesso problema in forma peggiore: al posto
     dell'ora c'è la pastiglia di stato, che porta una PAROLA («In corso»,
     «Squillo») e si prendeva 109 px dei 349 disponibili. Alla coppia A→B ne
     restavano 30, e il numero chiamato spariva su tutte le righe.

     È un difetto PREESISTENTE — misurato uguale sul codice di prima — e qui
     è solo ATTENUATO, non chiuso: la pastiglia più stretta porta la colonna
     da 30 a 75 px, abbastanza perché il numero chiamato non sparisca del
     tutto, non abbastanza perché si legga intero. Chiuderlo vuol dire
     togliere una colonna o accorciare le etichette di stato a 375 px, che è
     una decisione di prodotto: sta fra le cose aperte. La vista Chiamate,
     quella della segnalazione, a 375 px è invece a posto (vedi sotto). */
  .active-table .st {
    gap: 0.25rem;
    padding-right: 0.3rem;
    padding-left: 0.3rem;
    font-size: 0.62rem;
  }

  /* La parola per esteso lascia il posto alla sua abbreviazione: «In corso»
     diventa «LIVE», «Squillo» «RING», «Setup» «SETUP».

     Scatta a 700 e non a 560 perché è lo stesso gradino in cui A→B va su due
     righe: sotto quella soglia la tabella è in modo stretto, e una pastiglia
     lunga lì dentro toglie spazio alla colonna che porta i numeri. A 600 px
     vale 18 px in più per A→B — non indispensabili a quella larghezza, ma è
     un gradino solo invece di tre, e la parola intera non si perde.

     Il nome accessibile della pastiglia resta quello lungo (`aria-label` in
     active.js) e così il tooltip: chi legge con lo schermo sente «Squillo» a
     qualunque larghezza, e chi passa il puntatore lo vede. */
  .active-table .st-lungo {
    display: none;
  }

  .active-table .st-corto {
    display: inline;
  }

  /* `min-width: 4.5rem` teneva ferma la colonna del contatore che scorre: a
     375 px quel respiro lo paga la colonna che porta i numeri, e il
     tremolio di una cifra è il male minore. */
  .active-table .col-dur {
    min-width: 0;
  }

  /* E anche così restano 131 px, che per quattro valori su una riga non
     bastano: misurato, il numero chiamato si leggeva per un quarto. A questa
     larghezza la coppia va su DUE righe — il chiamante sopra, la freccia e il
     chiamato sotto — e ognuno ha la larghezza intera invece di un quarto.

     Il salto di riga si ottiene dalla BASE del chiamante: al 100% è lui a
     riempire la sua riga, e tutto il resto va a capo. Nessun elemento in più
     nel DOM, e sopra i 560 px la cella resta su una riga sola.

     Così la pastiglia del nodo si prende una riga tutta sua, ed è il costo di
     questa scelta: la cella ne occupa quattro (87 px di riga) quando c'è sia
     il nodo sia l'utenza, tre altrimenti. Si può far entrare la pastiglia
     accanto al chiamante abbassandogli la base — provato al 60% e al 50% —
     ma a 131 px di colonna il chiamante non ci sta più intero: 48 px di
     pastiglia e 7 di freccia gliene lasciano 70 contro i 103 che gli servono.
     Una riga in meno contro un numero tagliato, e vince il numero: qui la
     lista si legge, non si conta a colpo d'occhio.

     La pastiglia del nodo compare solo quando i nodi noti sono più di uno e
     non se n'è già scelto uno, quindi la riga a quattro linee non è il caso
     ordinario. */
  .party-cell {
    flex-wrap: wrap;
    row-gap: 0.1rem;
  }

  .party-a {
    flex-basis: 100%;
  }

  /* Ognuno ha la sua riga: i tetti pensati per stare in tre su una sola non
     servono più, e a 6rem l'utenza si tagliava per UN pixel. */
  .party-a,
  .party-b,
  .party-utenza {
    max-width: 100%;
  }

}

/* ------------------------------------ IL TERZO GRADINO: cede il nodo

   Sotto i 560 px nemmeno le due righe bastano. Su un telefono la tabella ha
   349 px per sei colonne: misurato con la colonna del nodo presente, ad A→B
   ne restavano 49 nella vista Chiamate e **17** in quella degli attivi, che
   per giunta si metteva a scorrere in orizzontale. Nessuno dei due numeri
   della chiamata era leggibile. Tolta la colonna, A→B torna a 131 px e
   chiamante, chiamato e utenza si leggono per intero su tutte le righe.

   Cede il nodo, e non è una scelta di comodo: è l'unica colonna che dice dove
   abbiamo GUARDATO invece di cosa è successo, ed è l'unica il cui valore resta
   scritto altrove anche quando la colonna non c'è — nel selettore «Nodo» della
   barra dei filtri e nel piede del pannello, che nomina il nodo attivo. A→B,
   durata e causa non hanno un altrove.

   Sopra i 560 px la colonna c'è SEMPRE, anche con un nodo solo e anche con il
   filtro già stretto su uno: è la richiesta, e vale ovunque ci sia lo spazio
   per rispettarla.

   Si nascondono intestazione e celle con la stessa regola — per questo il
   `<th>` porta la classe: nascondere solo le celle disallineerebbe la
   tabella. */
@media (max-width: 560px) {
  .calls-table .col-node {
    display: none;
  }

  /* E con lei la colonna della direzione, per la stessa ragione e con la
     stessa misura. A 375 px la pastiglia costa 55 px e ad A -> B ne restavano
     75: il chiamante si leggeva su 4 righe su 16 e il chiamato su nessuna.
     Provate le vie di mezzo, e nessuna basta -- misurato: con la sola freccia
     (32 px) A -> B arriva a 99 e il chiamato resta leggibile su 1 riga su 16;
     togliendo la gamba non cambia niente. Solo togliendo la colonna A -> B
     torna ai suoi 131 px e si legge tutto.

     Come per il nodo, cio' che cede e' una CLASSIFICAZIONE e non una misura:
     entrata o uscita si ricava anche dal filtro attivo, che resta nella barra,
     mentre A -> B, durata, PDD e causa non hanno un altrove. */
  .calls-table .col-dir {
    display: none;
  }

  /* ------------------ e nella vista attivi cede anche `Start`

     Le etichette di stato accorciate («In corso» → «LIVE») portano la colonna
     dello stato da 78 a 60 px, e ad A→B da 75 a 95: un miglioramento vero, ma
     NON abbastanza — il chiamante si leggeva ancora su 7 righe su 16 e il
     chiamato su 1. Misurato cosa serve per arrivare ai 131 px con cui la vista
     Chiamate legge tutto:

       niente                 A→B  95 px   chiamante  7/16   chiamato  1/16
       senza PDD              A→B 118 px   chiamante 16/16   chiamato 13/16
       senza Start            A→B 140 px   chiamante 16/16   chiamato 16/16
       senza PDD e Start      A→B 140 px   (identico: il secondo non aggiunge)

     Cede `Start`, e per due ragioni. La prima è che basta da solo, e togliere
     anche PDD non guadagna un pixel. La seconda è che su QUESTA vista è la
     colonna che ripete: il contatore di `Duration` scorre da `activeSince` —
     l'INVITE per chi squilla o sta instaurando, quindi `Start` è esattamente
     «adesso meno Duration»; per chi è in conversazione i due differiscono solo
     del tempo di squillo. PDD invece è un dato che non sta da nessun'altra
     parte nella riga.

     Sopra i 560 px `Start` c'è. E in ogni caso il valore per esteso resta nel
     pannello di dettaglio, che si apre da qualunque riga. */
  .active-table .col-start {
    display: none;
  }
}

/* --------------------------------------------- la colonna della direzione

   Entrata o uscita, prima colonna, larga due caratteri. Sta in testa perche'
   e' la domanda che ci si fa per prima davanti a una lista di chiamate — «di
   queste, quali entravano?» — e perche' costa pochissimo: la pastiglia e' una
   freccia piu' due o tre lettere.

   `width: 1%` con `white-space: nowrap` e' il solito modo di dire a una
   tabella a layout automatico «stringiti sul contenuto». */
.calls-table .col-dir {
  width: 1%;
  white-space: nowrap;
}

/* IL COLORE NON E' L'UNICO SEGNALE. Verde-in / blu-out e' il quarto segnale,
   non il primo: prima vengono il VERSO della freccia (← / →), poi la SIGLA
   scritta (IN / OUT), poi il nome accessibile in parole («In entrata, gamba
   A», messo da renderDirection). Chi non distingue verde e blu legge
   comunque tutto, e chi legge con lo schermo sente una frase, non tre
   simboli. */
.dir {
  display: inline-flex;
  gap: 0.25rem;
  align-items: baseline;
  padding: 0.02rem 0.35rem;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  border-radius: var(--pill);
  white-space: nowrap;
}

.dir-arrow {
  font-size: 0.8rem;
  line-height: 1;
}

.dir-in {
  color: var(--sip-2xx);
  background: var(--tint-ok);
  border: 1px solid rgba(79, 208, 138, 0.35);
}

.dir-out {
  color: var(--sip-req);
  background: var(--tint-info);
  border: 1px solid rgba(108, 199, 232, 0.35);
}

/* La gamba, dentro la stessa pastiglia e in corpo minore: e' una precisazione
   di «in entrata», non un'informazione di pari rango. Una colonna sua sarebbe
   larga un carattere e vuota su ogni riga che non sia un'entrante. */
.dir-leg {
  margin-left: 0.1rem;
  font-size: 0.82em;
  vertical-align: super;
  opacity: 0.85;
}

/* L'intestazione della colonna e' un'icona muta (⇆) con il nome vero in
   .sr-only: due caratteri di testata su una colonna larga due caratteri
   allargherebbero la colonna piu' del suo contenuto. */
.calls-table .col-dir[scope="col"],
.calls-table th.col-dir {
  color: var(--text-dim);
}

/* ------------------------------------------------- la colonna del nodo

   PERCHÉ STA FRA IL QUANDO E IL CHI.

   Il nodo non è un dato della chiamata: è da dove l'abbiamo VISTA. Le colonne
   di queste liste si leggono in due gruppi — prima il contesto
   dell'osservazione (l'ora nella vista storica, lo stato in quella degli
   attivi), poi la chiamata (A→B, durata, PDD, causa) — e la provenienza
   appartiene al primo. Sta in fondo a quel gruppo, cioè appoggiata ad A→B,
   per due motivi: è la coppia che qualifica, e fino a ieri ci stava dentro
   come pastiglia, quindi l'occhio la cerca lì. Metterla in testa avrebbe
   spostato la chiave con cui si scorre la lista — l'ora, lo stato — dalla
   prima colonna, che è dove la si cerca.

   È SEMPRE PRESENTE, anche con un nodo solo o con il filtro già stretto su
   uno: una colonna che va e viene fa chiedere ogni volta perché non c'è.

   `width: 1%` con `white-space: nowrap` è il modo di dire a una tabella a
   layout automatico «stringiti sul contenuto»: i nomi sono corti (sosw,
   soswmi, astx-svil) e ogni pixel in più lo pagherebbe A→B, che è la colonna
   che il contenuto ce l'ha davvero. */
.calls-table .col-node {
  width: 1%;
  white-space: nowrap;
}

/* La pastiglia di PROVENIENZA: il contenuto della colonna qui sopra, e la
   stessa forma di prima — non se n'è inventata una nuova.

   Il nome del nodo viene dai pacchetti, quindi vale la stessa regola di
   .party-a / .party-b: il tetto sta sul CONTENUTO (inline-block + max-width),
   non sulla cella, altrimenti in una tabella a layout automatico la larghezza
   minima tornerebbe a venire dal testo e la colonna sfonderebbe. */
.node-chip {
  display: inline-block;
  /* 5rem = 80 px. Il nome vero più lungo di questo impianto, `astx-svil`,
     misura 72 px; `soswmi` 53, `sosw` 40. Il tetto sta appena sopra il più
     lungo che esiste, e serve a garantire che un captureId assurdo — arriva
     dai pacchetti come tutto il resto — non possa allargare la colonna oltre
     ciò di cui un nome vero ha bisogno. Senza, la riga di prova da 1024
     caratteri portava la colonna da 94 a 113 px, rubandoli ad A→B. */
  max-width: 5rem;
  padding: 0.02rem 0.4rem;
  overflow: hidden;
  font-size: 0.66rem;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
  color: var(--text-muted);
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

/* Il selettore del nodo nella barra filtri: stessa altezza dei campi accanto,
   e una larghezza che non insegue il nome più lungo (che arriva dai dati). */
.node-field select {
  max-width: 11rem;
}

/* Le altre colonne che mostrano testo di provenienza SIP (vista
   Registrazioni). Qui il testo sta direttamente nel <td>, quindi il tetto si
   può mettere sulla cella. */
.col-user,
.col-contact,
.col-device,
.col-transport {
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Call-ID nell'intestazione del pannello destro: può essere lungo a piacere e
   non deve spingere il pannello oltre la sua colonna. */
#detail-sid {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Etichette dentro il ladder e la lista messaggi: la reason phrase è testo
   libero scelto dall'altro capo. */
.hop-label,
.msg-label {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* A brillante, B spento sotto: come nel riferimento restano su due livelli
   di lettura anche stando sulla stessa riga. */
.party-a {
  font-weight: 700;
  color: var(--text);
}

/* L'utenza di consegna, fra parentesi accanto al numero chiamato. Piu' spenta
   del numero perche' e' un contorno: la domanda "chi e' stato chiamato" ha per
   risposta il numero composto, l'utenza dice a chi e' stato consegnato. Compare
   solo quando i due valori sono davvero diversi (vedi lib/calls.js), quindi il
   fatto stesso che ci sia e' informazione. */
.party-utenza {
  color: var(--text-dim);
}

.party-arrow {
  margin: 0 0.35rem;
  color: var(--text-dim);
}

/* Durata sconosciuta: risposta catturata, chiusura no. Non è un trattino
   (che vuol dire "non c'è nulla da misurare", ed è il caso delle chiamate
   mai risposte) e non è un contatore che scorre (che direbbe "sta
   succedendo adesso"). È una domanda senza risposta, e si vede che lo è.
   Il motivo per esteso sta nel tooltip della cella. */
.dur-unknown {
  display: inline-block;
  min-width: 1.2rem;
  color: var(--text-dim);
  text-align: center;
  cursor: help;
}

.party-b {
  color: var(--text-dim);
}

.muted {
  color: var(--text-dim);
}

.badge {
  display: inline-block;
  padding: 0.02rem 0.4rem;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  border-radius: var(--pill);
}

.badge-live {
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid rgba(94, 198, 230, 0.35);
}

/* Cella CAUSE colorata per classe di risposta. */
.cause {
  display: inline-block;
  min-width: 2.4rem;
  padding: 0.02rem 0.4rem;
  font-weight: 700;
  text-align: center;
  border-radius: var(--radius-sm);
}

.cause-2xx {
  color: var(--sip-2xx);
  background: var(--tint-ok);
}

.cause-3xx {
  color: var(--sip-3xx);
  background: var(--tint-warn);
}

.cause-4xx {
  color: var(--sip-err);
  background: var(--tint-err);
}

/* ----------------------------------------- vista Chiamate attive */

/* Riepilogo per stato sopra la tabella: stessi colori dei badge, così il
   conteggio e le righe si leggono insieme. */
.active-summary {
  display: flex;
  flex-shrink: 0;
  flex-wrap: wrap;
  gap: 0.3rem 1rem;
  align-items: center;
  min-height: 2.1rem;
  padding: 0.5rem 0.85rem;
  font-size: 0.75rem;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}

.active-summary .hint {
  font-size: 0.75rem;
  color: var(--text-dim);
}

.sum {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  font-weight: 700;
}

.sum::before {
  width: 0.42rem;
  height: 0.42rem;
  content: "";
  background: currentColor;
  border-radius: 50%;
}

/* Badge di stato: pallino + etichetta nel colore della classe SIP
   corrispondente, sopra il fondo tenue già usato dalle celle CAUSE. */
.st {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  padding: 0.08rem 0.6rem;
  font-size: 0.7rem;
  font-weight: 700;
  border-radius: var(--pill);
}

/* Delle due forme dello stato se ne vede una sola: quella lunga di regola,
   quella corta sotto i 560 px (vedi il blocco stretto). */
.st-corto {
  display: none;
}

.st::before {
  width: 0.45rem;
  height: 0.45rem;
  content: "";
  background: currentColor;
  border-radius: 50%;
}

/* Risposta, in conversazione. */
.sum-live,
.st-live {
  color: var(--sip-2xx);
}

.st-live {
  background: var(--tint-ok);
}

/* In squillo: il pallino pulsa finché non risponde qualcuno. */
.sum-ringing,
.st-ringing {
  color: var(--sip-1xx);
}

.st-ringing {
  background: var(--tint-warn);
}

.st-ringing::before {
  animation: pulse 1.2s ease-in-out infinite;
}

/* In instaurazione: nessun 18x ancora, si sta cercando la via. */
.sum-setup,
.st-setup {
  color: var(--sip-req);
}

.st-setup {
  background: var(--tint-info);
}

.col-state {
  width: 1%; /* la colonna si stringe sul badge */
}

/* --------------------------------------------- vista Utenti */

/* Stati della registrazione: stesse pill+pallino della vista Chiamate attive.
   Online verde, offline grigio spento, unreachable ambra (registrato ma il
   qualify non risponde). */
.sum-online,
.st-online {
  color: var(--sip-2xx);
}

.st-online {
  background: var(--tint-ok);
}

.sum-unreach,
.st-unreach {
  color: var(--sip-1xx);
}

.st-unreach {
  background: var(--tint-warn);
}

/* Il TOTALE dei registrati (online + non risponde). Non e' uno stato, quindi
   non prende il pallino colorato degli stati: un pallino accanto lo metterebbe
   sulla stessa riga logica dei suoi addendi, che e' esattamente l'equivoco da
   cui nasce questa modifica (vedi HANDOFF §5.19). */
.sum-registered {
  color: var(--text-muted);
}

.sum-registered::before {
  display: none;
}

.sum-offline,
.st-offline {
  color: var(--text-dim);
}

.st-offline {
  background: rgba(139, 148, 166, 0.1);
}

/* Riepilogo: i due tipi di stack riprendono i colori delle pill Kind. */
.sum-pjsip {
  color: var(--accent);
}

.sum-sip {
  color: var(--violet);
}

/* Pill del tipo di stack: PJSIP ciano, chan_sip viola, altro spento. Distinte
   a colpo d'occhio come chiede la colonna KIND. */
.kind {
  display: inline-block;
  padding: 0.05rem 0.5rem;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: var(--pill);
}

.kind-pjsip {
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid rgba(94, 198, 230, 0.35);
}

.kind-sip {
  color: var(--violet);
  background: rgba(167, 139, 250, 0.12);
  border: 1px solid rgba(167, 139, 250, 0.35);
}

.kind-other {
  color: var(--text-dim);
  background: rgba(139, 148, 166, 0.1);
  border: 1px solid var(--border-strong);
}

/* Colonne della tabella Utenti: nove colonne, quindi la tabella può eccedere
   la larghezza del pannello e scorre dentro .panel-body (overflow:auto). User
   e Device sono le uniche elastiche; il resto si stringe sul contenuto. */
.col-user {
  max-width: 10rem;
  overflow: hidden;
  font-weight: 700;
  color: var(--text);
  text-overflow: ellipsis;
}

.col-kind,
.col-transport {
  width: 1%;
}

.col-contact {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.col-device {
  max-width: 12rem;
  overflow: hidden;
  color: var(--text-muted);
  text-overflow: ellipsis;
}

.col-expires,
.col-lastreg,
.col-lastseen {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.col-start {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------ la riga del dettaglio: schede + azione

   Ladder e Lista sono un selettore a stati; PCAP e' un'azione che si compie.
   Stanno sulla stessa riga perche' riguardano la stessa cosa, ma non devono
   sembrare tre linguette: la separa uno spazio elastico, e il pulsante ha una
   forma sua -- contorno leggero, nessuno stato "attivo" da accendere. */
.detail-bar {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  padding-right: 0.9rem;
}

.detail-bar .tabs {
  flex: 0 1 auto;
}

/* Come in .topbar e .panel-head: lo spaziatore elastico si dichiara dove
   serve, non globalmente. E' quello che spinge l'azione all'altro capo della
   riga, che e' la separazione vera fra "scegli una vista" e "fai una cosa". */
.detail-bar .spacer {
  flex: 1;
}

/* Il messaggio di esito sta ACCANTO al pulsante e non in cima al pannello:
   e' li' che l'utente sta guardando quando preme. */
.detail-bar-msg {
  /* Un errore lungo non deve schiacciare il pulsante che l'ha prodotto. */
  max-width: 22rem;
  font-size: 0.72rem;
  color: var(--text-dim);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.detail-bar-msg.is-error {
  color: var(--sip-err);
}

.btn-download {
  flex: 0 0 auto;
  padding: 0.18rem 0.7rem;
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--pill);
  cursor: pointer;
}

.btn-download:hover:not(:disabled) {
  color: var(--text);
  border-color: var(--accent);
}

.btn-download:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.btn-download:disabled {
  opacity: 0.45;
  cursor: default;
}

/* --------------------------------------------- stati della lista */

.state {
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--text-muted);
}

.state strong {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.88rem;
  color: var(--text);
}

.state.state-error strong {
  color: var(--sip-err);
}

/* Il suggerimento puo' contenere un valore arrivato dai pacchetti -- il nome
   di un nodo che non esiste, per dire -- e quelli non hanno spazi in cui
   andare a capo: senza questo, su uno schermo stretto escono di lato. */
.state .hint {
  font-size: 0.78rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.state button {
  margin-top: 0.9rem;
}

/* Barra sottile durante il ricarico, per non far sparire la lista. */
.loading-bar {
  flex-shrink: 0;
  height: 2px;
  background: var(--accent-soft);
  overflow: hidden;
}

.loading-bar.is-active::after {
  display: block;
  width: 40%;
  height: 100%;
  content: "";
  background: var(--accent);
  animation: slide 0.9s linear infinite;
}

@keyframes slide {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(350%);
  }
}

/* -------------------------------------------- il seguito della lista

   Sta FUORI dal contenitore che scorre, fra la tabella e il piede: dentro,
   bisognerebbe scendere in fondo a settecento righe per trovarlo. Le varianti
   stanno sotto la base, come sempre (HANDOFF §5.13). */
.more {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.75rem;
  align-items: baseline;
  padding: 0.5rem 0.9rem;
  border-top: 1px solid var(--border);
}

.more-btn {
  padding: 0.3rem 0.75rem;
  font: inherit;
  font-size: 0.78rem;
  color: var(--accent);
  cursor: pointer;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

.more-btn:hover:not(:disabled) {
  color: var(--accent-strong);
  border-color: var(--accent);
}

.more-btn:disabled {
  color: var(--text-dim);
  cursor: default;
}

/* Quante ne sono state caricate, e -- quando l'utente sta sfogliando -- che
   l'aggiornamento automatico e' sospeso. Detto, non lasciato intuire dal fatto
   che la lista ha smesso di muoversi. */
.more-note {
  font-size: 0.7rem;
  color: var(--text-dim);
}

.panel-foot {
  flex-shrink: 0;
  padding: 0.4rem 0.85rem;
  font-size: 0.72rem;
  color: var(--text-muted);
  background: var(--panel-2);
  border-top: 1px solid var(--border);
}

.notice {
  flex-shrink: 0;
  padding: 0.5rem 0.85rem;
  font-size: 0.76rem;
  color: var(--sip-1xx);
  background: var(--tint-warn);
  border-bottom: 1px solid var(--border);
}

/* ------------------------------------------------ pannello destro */

.sid-chip {
  max-width: 60%;
  overflow: hidden;
  font-size: 0.7rem;
  color: var(--text-dim);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tabs {
  display: flex;
  flex-shrink: 0;
  gap: 0.35rem;
  padding: 0.45rem 0.85rem;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}

.tabs button {
  padding: 0.26rem 0.9rem;
  font-size: 0.76rem;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

.tabs button:hover {
  color: var(--text);
  background: var(--panel-2);
  border-color: var(--border-strong);
}

.tabs button[aria-selected="true"] {
  font-weight: 700;
  color: var(--on-accent);
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

/* Colori condivisi da ladder e lista: ogni classe di messaggio definisce il
   proprio primo piano, freccia ed etichetta lo ereditano da --c-fg. */
.sip-req {
  --c-fg: var(--sip-req);
}

.sip-ack {
  --c-fg: var(--sip-ack);
}

.sip-cancel {
  --c-fg: var(--sip-err);
}

.sip-1xx {
  --c-fg: var(--sip-1xx);
}

.sip-2xx {
  --c-fg: var(--sip-2xx);
}

.sip-3xx {
  --c-fg: var(--sip-3xx);
}

.sip-4xx {
  --c-fg: var(--sip-err);
}

.sip-unknown {
  --c-fg: var(--text-muted);
}

/* ---------------------------------------------------- SIP ladder */

.ladder {
  --off-w: 5.5rem;
}

.ladder-head,
.ladder-row {
  display: grid;
  grid-template-columns: var(--off-w) minmax(0, 1fr);
}

.ladder-head {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--panel-2);
  border-bottom: 1px solid var(--border);
}

.ladder-lanes {
  display: grid;
  grid-template-columns: repeat(var(--hosts), minmax(0, 1fr));
  align-items: center;
}

.ladder-host {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.45rem 0.3rem;
  text-align: center;
}

.host-ip {
  font-size: 0.73rem;
  font-weight: 700;
  color: var(--text);
}

.host-port {
  font-size: 0.65rem;
  color: var(--text-muted);
}

/* Un solo host porta l'accento viola (quello centrale, scelta stabile):
   dà un punto di riferimento nel ladder senza aggiungere dati. */
.ladder-host.is-accent .host-port {
  color: var(--violet);
}

.ladder-rows {
  padding-bottom: 1rem;
}

.ladder-row {
  align-items: stretch;
}

.ladder-row:hover {
  background: var(--panel-2);
}

/* Colonna degli scostamenti: numeri spenti, non devono competere con le
   frecce colorate. */
.ladder-off {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 0.6rem;
  font-size: 0.68rem;
  color: var(--text-dim);
}

.ladder-row .ladder-lanes {
  min-height: 2.7rem;
}

/* Linea di vita dell'host: un segmento per riga, così restano allineate
   alle colonne senza doverle disegnare in overlay. */
.lane {
  position: relative;
  grid-row: 1;
  align-self: stretch;
}

.lane::before {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  content: "";
  border-left: 1px dashed var(--border-strong);
}

/* La freccia occupa le colonne fra sorgente e destinazione; il padding
   laterale di mezza colonna la fa partire e arrivare al centro delle due
   linee di vita invece che ai bordi. */
.hop {
  display: flex;
  flex-direction: column;
  grid-row: 1;
  gap: 0.15rem;
  min-width: 0;
  padding: 0.3rem 0;
  padding-right: calc(50% / var(--span));
  padding-left: calc(50% / var(--span));
  color: var(--c-fg, var(--text-muted));
  background: none;
  border: 0;
}

/* Etichetta nello stesso colore della freccia; il fondo pieno serve solo a
   non farla attraversare dalle linee di vita. */
.hop-label {
  overflow: hidden;
  padding: 0.05rem 0.4rem;
  font-size: 0.71rem;
  font-weight: 700;
  color: var(--c-fg, var(--text));
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: var(--panel);
  border-radius: var(--radius-sm);
}

.ladder-row:hover .hop-label {
  background: var(--panel-2);
}

/* La deviazione nel ladder: chi ha girato la chiamata, accanto alla freccia.
   Spenta e piccola apposta -- non e' l'informazione principale della riga, e
   deve poter essere ignorata da chi sta leggendo l'esito. Non allarga niente:
   sta dentro il bottone che c'e' gia' e si tronca come l'etichetta. */
.hop-dev {
  display: inline-block;
  max-width: 9ch;
  overflow: hidden;
  margin-left: 0.3rem;
  padding: 0.05rem 0.25rem;
  font-size: 0.64rem;
  font-weight: 600;
  color: var(--text-muted);
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.hop:hover .hop-label {
  box-shadow: inset 0 0 0 1px currentColor;
}

.hop-arrow {
  position: relative;
  height: 2px;
  background: currentColor;
}

.hop-arrow::after {
  position: absolute;
  top: 50%;
  width: 0;
  height: 0;
  content: "";
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transform: translateY(-50%);
}

.dir-right .hop-arrow::after {
  right: 0;
  border-left: 7px solid currentColor;
}

.dir-left .hop-arrow::after {
  left: 0;
  border-right: 7px solid currentColor;
}

/* Messaggio che parte e arriva sullo stesso host: cappio invece di freccia. */
.hop-self {
  padding-right: 0;
  padding-left: 0;
}

.hop-self .hop-arrow {
  width: 55%;
  height: 9px;
  margin: 0 auto;
  background: none;
  border: 1px solid currentColor;
  border-top: 0;
  border-radius: 0 0 7px 7px;
}

.hop-self .hop-arrow::after {
  display: none;
}

/* ------------------------------------------------------ tab Lista */

/* Timeline: una linea verticale continua e un pallino per messaggio. */
.msg-list {
  --rail-x: 2.05rem;
  padding: 0.2rem 0 0.6rem;
}

.msg {
  position: relative;
}

.msg::before {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--rail-x);
  width: 1px;
  content: "";
  background: var(--border);
}

.msg:first-child::before {
  top: 1.05rem;
}

.msg:last-child::before {
  bottom: auto;
  height: 1.05rem;
}

.msg-head {
  display: grid;
  column-gap: 0.5rem;
  row-gap: 0.1rem;
  align-items: center;
  padding: 0.4rem 0.85rem;
  font-size: 0.75rem;
  cursor: pointer;
  list-style: none;
  grid-template-columns: 0.8rem 1rem 1.6rem 4.8rem minmax(0, 1fr) auto;
  grid-template-areas:
    "caret rail seq off label clock"
    "caret rail seq off route route";
}

.msg-head::-webkit-details-marker {
  display: none;
}

.msg-head::before {
  grid-area: caret;
  content: "\25B8";
  color: var(--text-dim);
}

.msg[open] .msg-head::before {
  content: "\25BE";
  color: var(--accent);
}

/* Il pallino della timeline: l'anello lo stacca dalla linea sottostante. */
.msg-head::after {
  position: absolute;
  top: 1.05rem;
  left: var(--rail-x);
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  content: "";
  background: var(--accent);
  border-radius: 50%;
  box-shadow: 0 0 0 3px var(--panel);
}

.msg-head:hover {
  background: var(--panel-2);
}

.msg-head:hover::after {
  box-shadow: 0 0 0 3px var(--panel-2);
}

.msg-seq {
  grid-area: seq;
  font-size: 0.68rem;
  color: var(--text-dim);
  text-align: right;
}

.msg-off {
  grid-area: off;
  font-size: 0.68rem;
  color: var(--text-dim);
  text-align: right;
}

.msg-label {
  grid-area: label;
  justify-self: start;
  max-width: 100%;
  overflow: hidden;
  font-weight: 700;
  color: var(--c-fg, var(--text));
  text-overflow: ellipsis;
  white-space: nowrap;
}

.msg-route {
  grid-area: route;
  overflow: hidden;
  font-size: 0.67rem;
  color: var(--text-muted);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.msg-clock {
  grid-area: clock;
  font-size: 0.67rem;
  color: var(--text-dim);
}

/* Payload SIP: riquadro incassato, allineato alla timeline. */
.msg-raw {
  margin: 0 0.85rem 0.7rem calc(var(--rail-x) + 0.6rem);
  padding: 0.6rem 0.75rem;
  overflow-x: auto;
  /* <pre> non eredita il font dal body: senza questo il payload uscirebbe
     dal monospace dell'app. */
  font-family: var(--mono);
  font-size: 0.71rem;
  line-height: 1.5;
  color: var(--text-muted);
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* Prima riga (request-line / status-line) e nomi header: giusto quel tanto
   che basta a orientarsi dentro il payload. */
.msg-raw .raw-start {
  color: var(--text);
}

.msg-raw .raw-header {
  color: rgba(94, 198, 230, 0.75);
}

/* Arrivo dal ladder: un lampo per far capire quale riga si è aperta. */
@keyframes target-flash {
  from {
    background: var(--accent-soft);
  }
  to {
    background: transparent;
  }
}

.msg.is-target {
  animation: target-flash 1.4s ease-out;
}

/* ------------------------------------- footer con le metriche */

.panel-foot.metrics {
  display: flex;
  flex-wrap: wrap;
  gap: 0.1rem 1.1rem;
}

.metric {
  display: inline-flex;
  gap: 0.4rem;
  align-items: baseline;
}

.metric-key {
  font-size: 0.66rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.metric-value {
  font-weight: 700;
  color: var(--text);
}

@media (max-width: 900px) {
  /* La sidebar diventa una barra superiore: brand a sinistra, voci in
     fila. Il resto dell'app scorre sotto. */
  body.app-page {
    flex-direction: column;
  }

  .sidebar {
    flex-direction: row;
    gap: 0.75rem;
    align-items: center;
    width: auto;
    padding: 0.4rem 0.7rem;
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }

  .brand {
    padding: 0;
  }

  .nav {
    flex-direction: row;
    gap: 0.35rem;
  }

  /* In orizzontale la barra a sinistra non ha piu' senso: torna una pillola
     come gli altri controlli del tema. */
  .nav-item {
    padding: 0.28rem 0.7rem;
    border-left: 0;
    border-radius: var(--pill);
  }

  .nav-item.is-current {
    border-left: 0;
  }

  .workspace {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 2fr) minmax(0, 1fr);
  }
}

/* Molto stretto: le voci si riducono alla sola icona. La label resta nel DOM
   (serve agli screen reader), ma esce dal flusso. */
@media (max-width: 560px) {
  /* Il wordmark "VIPER" resta (è compatto); via il sottotitolo e le label. */
  .brand-sub,
  .nav-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .nav-icon {
    font-size: 0.85rem;
  }

  /* Senza testo la pillola si stringe sull'icona: qui si tocca col dito,
     quindi il bersaglio va tenuto grande a mano. */
  .nav-item {
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ================================== viste a pannello unico (a tutta pagina) */
/*
 * Stato nodi non sta accanto agli altri due pannelli: quando è la vista
 * corrente prende tutta la workspace e lista e dettaglio spariscono. La griglia
 * torna a una colonna sola, così il contenuto non resta schiacciato nel 1.45fr
 * del pannello di sinistra.
 *
 * Era così anche per Impostazioni, finché è stata una vista dell'app: dall'
 * 11/08/2026 è una sezione di /admin, e qui resta il solo caso Nodi.
 */

.panel[hidden] {
  display: none;
}

.workspace.is-full {
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
}

.workspace.is-full > .panel:not(.panel-report) {
  display: none;
}

/* Il contenuto scorre dentro il pannello; la colonna ha un tetto perché una
   riga di testo lunga quanto un monitor da 1440px non si legge. Le classi
   .settings-* restano il vestito comune dei form: le usa /admin, che è l'unico
   posto dove sono rimaste. */
.settings-content {
  max-width: 62rem;
  padding: 1.1rem 1.15rem 2rem;
}

.settings-section + .settings-section {
  margin-top: 2.2rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--border);
}

.settings-section h3 {
  margin: 0 0 0.35rem;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}

.settings-subhead {
  margin: 1.6rem 0 0.6rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.settings-desc {
  max-width: 52rem;
  margin: 0 0 1rem;
  font-size: 0.78rem;
  line-height: 1.55;
  color: var(--text-muted);
}

.settings-hint {
  margin: 0.45rem 0 0;
  font-size: 0.72rem;
  color: var(--text-dim);
}

/* Stato dell'ingest. Solo "fermo" prende un fondo colorato: e' l'unico dei
   quattro stati su cui bisogna intervenire, e se si evidenziassero anche gli
   altri quello che conta non si distinguerebbe piu' dal resto della pagina. */
.settings-desc[data-ingest] {
  padding: 0.6rem 0.8rem;
  border-left: 3px solid transparent;
  border-radius: var(--radius-sm);
}

.settings-desc[data-ingest='ok'] {
  border-left-color: var(--sip-2xx);
}

.settings-desc[data-ingest='stalled'] {
  border-left-color: var(--sip-err);
  background: var(--tint-err);
  color: var(--text);
  font-weight: 500;
}

.settings-desc[data-ingest='idle'],
.settings-desc[data-ingest='unknown'] {
  border-left-color: var(--border-strong);
}

/* Riga di controlli: campo(i) + azione + messaggio, che va a capo da solo
   quando la larghezza non basta. */
.settings-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  align-items: center;
}

.settings-form .range-field input[type="number"] {
  width: 5rem;
  font-variant-numeric: tabular-nums;
}

.settings-form .range-field input[type="text"],
.settings-form .range-field input[type="password"] {
  width: 12rem;
}

/* Il campo nuovo utente e quello password stanno su una riga sola finché
   c'è spazio: sotto, ognuno si prende la sua. */
.settings-form-new .range-field {
  flex: 0 1 auto;
}

/* Select nativa vestita come gli input a pillola. */
select {
  padding: 0.22rem 0.55rem;
  font: inherit;
  font-size: 0.74rem;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

select:hover {
  border-color: var(--border-strong);
}

select:focus {
  border-color: var(--accent);
  outline: 0;
  box-shadow: 0 0 0 2px var(--accent-soft);
}

select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Esito di un salvataggio, accanto al bottone che l'ha scatenato. */
.form-msg {
  font-size: 0.74rem;
  color: var(--text-muted);
}

.form-msg[data-kind="ok"] {
  color: var(--sip-2xx);
}

.form-msg[data-kind="error"] {
  color: var(--sip-err);
}

.form-msg:empty {
  display: none;
}

/* --------------------------------- fatti sui dati SIP in tabella */

.settings-facts {
  display: grid;
  gap: 0.35rem 1.4rem;
  margin: 1.2rem 0 0;
  padding: 0.75rem 0.9rem;
  font-size: 0.76rem;
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  grid-template-columns: max-content 1fr;
}

.settings-facts dt {
  color: var(--text-dim);
}

.settings-facts dd {
  margin: 0;
  color: var(--text);
}

/* ------------------------------------- tabella degli utenti app */

/* La tabella non deve poter allargare il pannello: se non ci sta, scorre lei. */
.table-scroll {
  overflow-x: auto;
}

.settings-table {
  width: 100%;
  border-collapse: collapse;
}

/* Qui le righe non si selezionano: niente cursore a manina né hover invitante
   come nelle liste di chiamate. */
.settings-table tbody tr {
  cursor: default;
}

.settings-table tbody tr:hover > td {
  background: transparent;
}

.settings-table .col-username {
  font-weight: 600;
}

.settings-table .col-created {
  color: var(--text-muted);
}

.settings-table .col-actions {
  text-align: right;
  white-space: nowrap;
}

.settings-table .col-actions button + button {
  margin-left: 0.35rem;
}

/* "tu": la riga dell'utente collegato, che ha meno azioni delle altre. */
.self-tag {
  padding: 0.05rem 0.45rem;
  font-size: 0.64rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-soft);
  border-radius: var(--pill);
}

.role-pill {
  padding: 0.1rem 0.6rem;
  font-size: 0.7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--pill);
}

.role-pill.role-admin {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
}

.role-pill.role-user {
  color: var(--text-muted);
}

.role-select {
  font-size: 0.72rem;
}

.btn-row {
  padding: 0.16rem 0.7rem;
  font-size: 0.72rem;
}

.btn-danger:hover:not(:disabled) {
  color: var(--sip-err);
  border-color: var(--sip-err);
  background: var(--tint-err);
}

.btn-quiet:disabled {
  opacity: 0.4;
  cursor: default;
}

.btn-danger-solid {
  color: #1b0709;
  background: var(--sip-err);
  border-color: var(--sip-err);
}

.btn-danger-solid:hover:not(:disabled) {
  background: #f47076;
  border-color: #f47076;
}

/* --------------------- riga-editor (password / conferma eliminazione) */

/* Sostituisce prompt() e confirm(), che non seguirebbero il tema: si apre
   sotto la riga dell'utente e ne resta aperta una sola per volta. */
.row-editor > td {
  padding: 0.6rem 0.7rem;
  background: var(--inset);
  border-bottom: 1px solid var(--border);
}

.row-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.7rem;
  align-items: center;
}

.row-confirm .confirm-text {
  flex: 1 1 18rem;
  font-size: 0.76rem;
  color: var(--text);
  white-space: normal;
}

/* Solo per gli screen reader: intestazione della colonna azioni. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (max-width: 900px) {
  /* La griglia a due righe della workspace non vale per le viste a pannello
     unico: senza questo resterebbero alte un terzo dello schermo. */
  .workspace.is-full {
    grid-template-rows: minmax(0, 1fr);
  }

  .settings-content {
    padding: 0.9rem 0.85rem 1.5rem;
  }
}

@media (max-width: 560px) {
  /* A questa larghezza etichetta e campo in fila non ci stanno: l'etichetta
     va sopra, come nel form di login. */
  .settings-form .range-field {
    flex: 1 1 100%;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.2rem;
  }

  .settings-form .range-field input,
  .settings-form .range-field select {
    width: 100%;
  }

  .settings-facts {
    grid-template-columns: minmax(0, 1fr);
    gap: 0 0;
  }

  .settings-facts dd {
    margin-bottom: 0.55rem;
  }
}

/* Conferma dell'esecuzione manuale della pulizia: stessa logica della
   riga-editor degli utenti, ma in linea sotto i due pulsanti. */
.run-confirm {
  display: flex;
  flex: 1 1 100%;
  flex-wrap: wrap;
  gap: 0.5rem 0.7rem;
  align-items: center;
  padding: 0.6rem 0.75rem;
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.run-confirm .confirm-text {
  flex: 1 1 20rem;
  font-size: 0.76rem;
  color: var(--text);
}

/* ========================================================== pagina Nodi */
/*
 * Due livelli, un pannello solo (data-mode su #view-report):
 *
 *   overview  una RIGA per nodo. Vincolo di progetto: con due nodi la pagina
 *             sta tutta in uno schermo 1920x1080 senza scorrere, e regge fino
 *             a sei righe. L'altezza e' la risorsa scarsa di questa vista:
 *             ogni voce in piu' per riga si paga in pixel, ed e' la ragione
 *             per cui qui c'e' solo l'essenziale e tutto il resto sta nel
 *             dettaglio.
 *   detail    un nodo solo, a tutta larghezza. Qui lo spazio c'e', e i
 *             contenuti che non entravano in panoramica si distendono.
 *
 * La pagina si guarda, non si legge: il primo segnale e' il semaforo a
 * sinistra della riga, il secondo e' la barra legittimo/scanner. Tutto il
 * resto e' dettaglio che si va a cercare dopo, e sta scritto in grigio
 * apposta.
 */

.report-content {
  padding: 0.7rem 0.9rem 0.9rem;
}

/* Nel dettaglio il respiro se lo puo' permettere IN LARGHEZZA: a 1920 px il
   contenuto non arriva a riempirla. In ALTEZZA no: l'obiettivo dichiarato e'
   che il dettaglio entri in uno schermo Full HD senza scorrere, e il margine
   in fondo era 26 px di puro vuoto sotto l'ultima riga. */
.panel-report[data-mode="detail"] .report-content {
  padding: 0.9rem 1.1rem 0.9rem;
}

.report-foot {
  margin: 0.7rem 0 0;
  font-size: 0.72rem;
  color: var(--text-dim);
}

.report-foot.is-error {
  color: var(--sip-1xx);
}

/* Ritorno dal dettaglio alla panoramica. Sta in intestazione, prima del
   titolo, e in panoramica e' semplicemente assente. */
.btn-back {
  flex-shrink: 0;
  align-self: center;
  padding: 0.14rem 0.6rem;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-muted);
  text-decoration: none;
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

.btn-back:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

.btn-back:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.btn-back[hidden] {
  display: none;
}

/* Nel dettaglio il titolo E' il nome del nodo, cioe' un valore che arriva dai
   pacchetti: report.js lo accorcia gia' a 40 caratteri, ma quaranta lettere
   senza spazi sono comunque piu' larghe di un telefono. Qui si accorcia con
   l'ellissi tipografica invece di spingere fuori il resto della barra. */
.panel-report .panel-head h2 {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* ============================================ livello 1: la panoramica */
/*
 * Una SCHEDA BASSA per nodo, impilate una sotto l'altra. Sono impilate e non
 * affiancate apposta: tutte hanno la stessa griglia interna, quindi le celle
 * si incolonnano da una scheda all'altra e due nodi si confrontano con un
 * movimento dell'occhio invece che rileggendo le etichette.
 *
 * Il tetto e' TRE nodi, e con ~900 px utili a 1080p ognuno ha circa 220 px --
 * spazio che si e' scelto di riempire con cio' che prima stava solo nel
 * dettaglio (sparkline grande, tutti i motivi, qualita', top 3 cause e IP).
 * Oltre i tre nodi la pagina non si rompe: `data-dense="1"` stringe, e piu' in
 * la' si scorre.
 */

.report-cards {
  display: grid;
  gap: 0.55rem;
  /* Tutte le schede alte uguale, anche se una ha tre motivi e l'altra
     nessuno: e' cio' che tiene le celle incolonnate fra una scheda e la
     successiva, che e' l'unico motivo per cui sono impilate. */
  grid-auto-rows: 1fr;
}

.report-cards[hidden] {
  display: none;
}

/* La scheda e' un <a>: il cursore, il focus da tastiera, Invio e il tasto
   Indietro arrivano dal browser invece che da un gestore scritto a mano (la
   barra spaziatrice la aggiunge report.js, perche' sui link scorre invece di
   attivare). */
.ncard2 {
  display: flex;
  min-width: 0;
  flex-direction: column;
  padding: 0.5rem 0.75rem 0.6rem;
  color: inherit;
  text-decoration: none;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
}

/* Il bordo sinistro ripete il semaforo: da lontano si vede il colore della
   scheda prima ancora di mettere a fuoco il pallino. */
.ncard2[data-health="ok"] {
  border-left-color: var(--sip-2xx);
}

.ncard2[data-health="warn"] {
  border-left-color: var(--sip-1xx);
}

.ncard2[data-health="bad"] {
  border-left-color: var(--sip-err);
}

/* Regolare, con note: un attacco che qualcuno ha gia' fermato. Ciano e non
   giallo, perche' in questa pagina il giallo vuol dire "guarda adesso" e qui
   non c'e' niente da guardare adesso -- e' informazione, come i rimandi. */
.ncard2[data-health="note"] {
  border-left-color: var(--accent);
}

.ncard2[data-health="unknown"] {
  border-left-color: var(--border-strong);
}

/* Nodo sotto attacco: bordo piu' marcato. Nessuna animazione, come sul resto
   della pagina: una cosa che lampeggia si guarda una volta e poi si copre con
   una finestra. */
.ncard2[data-attack="1"] {
  border-left-width: 5px;
  border-color: rgba(240, 85, 91, 0.4);
  border-left-color: var(--sip-err);
}

/* Che la scheda sia cliccabile si deve vedere prima di provarci: il fondo si
   schiarisce e la freccia si accende. */
.ncard2:hover {
  background: var(--panel);
  border-color: var(--border-strong);
}

.ncard2:hover .ncard2-go {
  color: var(--accent);
}

.ncard2:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ------------------------------------------- intestazione della scheda */
/* Chi e', come sta e da quanto tace: le tre cose che si leggono per prime,
   su una riga sola e sempre nello stesso posto. */

.ncard2-head {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 0.55rem;
  align-items: center;
  padding-bottom: 0.45rem;
  border-bottom: 1px solid var(--border);
}

.ncard2-head .spacer {
  flex: 1;
}

/* Il nome del nodo e' la cosa piu' grande della pagina: e' quello che si cerca
   guardando il monitor da tre metri. */
.ncard2-name {
  min-width: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text);
  overflow-wrap: anywhere;
}

.ncard2-last {
  font-size: 0.72rem;
  color: var(--text-dim);
  white-space: nowrap;
}

.ncard2-go {
  font-size: 1.15rem;
  line-height: 1;
  color: var(--text-dim);
}

/* --------------------------------------------- il corpo, sei colonne */
/*
 * Le larghezze sono in `fr` e non in rem: a 1920 px la sparkline si prende
 * tutto lo spazio che avanza, che e' esattamente cio' che serve a un grafico.
 * `minmax(0, ...)` su ognuna perche' senza, un contenuto lungo (un IPv6, un
 * nome di nodo ostile) allargherebbe la colonna oltre il contenitore.
 */

.ncard2-body {
  display: grid;
  min-width: 0;
  gap: 0.5rem 1.1rem;
  padding-top: 0.5rem;
  grid-template-columns:
    minmax(0, 1.5fr) /* raccolta dati: la sparkline */
    minmax(0, 1.5fr) /* salute: tutti i motivi */
    minmax(0, 1.15fr) /* traffico e qualita' */
    minmax(0, 0.9fr) /* cause di fallimento */
    minmax(0, 1.1fr) /* sicurezza */
    minmax(0, 0.75fr); /* registrazioni */
}

.ncell {
  min-width: 0;
}

/* L'intestazione di una cella: minuscola e spenta, si legge la prima volta e
   poi sparisce dall'attenzione, che e' quello che deve fare. */
.nrow-group-h {
  display: block;
  margin: 0 0 0.3rem;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* ------------------------------------------------- 1. raccolta dati */

/* Il ritmo in cifre sopra il grafico: il numero dice quanto, il disegno dice
   come. Nessuno dei due basta da solo. */
.ncell-lead {
  margin: 0 0 0.3rem;
  font-size: 0.72rem;
  color: var(--text-muted);
}

.ncell-lead b {
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.ncell-lead-sub {
  color: var(--text-dim);
}

/* L'altezza della sparkline della scheda (`.spark-md`) sta piu' in basso,
   accanto a `.spark`: e' una variante della stessa classe base e a parita' di
   specificita' vince l'ultima scritta, quindi le due DEVONO stare vicine. */

/* ------------------------------------------------------- 2. salute */

.ncell-reasons {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* I motivi in panoramica: stesse pastiglie del dettaglio, ma senza il fondo
   incassato e con il testo fermo a due righe. L'intero sta nel tooltip, e
   l'elenco completo nel dettaglio. */
.nreasons-brief {
  padding: 0;
  background: none;
  border-bottom: 0;
}

.nreasons-brief .nreason {
  flex-wrap: nowrap;
  padding: 0.1rem 0;
  font-size: 0.73rem;
  line-height: 1.35;
}

.nreasons-brief .nreason + .nreason {
  border-top: 0;
}

.nreasons-brief .nreason-kind {
  min-width: 4.2rem;
  align-self: flex-start;
}

/* Due righe al massimo. Un motivo che va a capo tre volte alzerebbe la scheda,
   e con tre schede l'altezza e' l'unico vincolo che conta. */
.nreasons-brief .nreason-text {
  display: -webkit-box;
  flex: 1 1 auto;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

/* Nessun allarme, o sezione mancante: si dice, spento. Uno spazio vuoto
   farebbe pensare a qualcosa che non ha caricato. */
.nrow-quiet {
  margin: 0;
  font-size: 0.73rem;
  line-height: 1.4;
  color: var(--text-dim);
}

/* ------------------------------------------- 3. traffico e qualita' */

/* Tre colonne invece delle due del dettaglio: sei metriche stanno in due
   righe, e la cella non diventa la piu' alta della scheda. */
.ncell-quality .nm-grid {
  gap: 0.4rem 0.6rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.ncell-quality .nm-v {
  font-size: 0.88rem;
}

.ncell-quality .nm-k {
  font-size: 0.62rem;
}

/* ------------------------------------------------------ 5. sicurezza */

/* La barra sta sopra la classifica ed e' gia' sotto l'intestazione della
   cella: il margine che serve nel dettaglio, qui, e' spazio sprecato. */
.ncell-security .nbar-wrap {
  margin: 0 0 0.45rem;
}

.ncell-security .nbar-legend {
  gap: 0.3rem 0.6rem;
  margin-top: 0.3rem;
  font-size: 0.62rem;
}

.ncell-security .nbar-key b {
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------- 6. registrazioni */

.nrow-group-line {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.85rem;
  align-items: baseline;
}

/* In colonna e non in fila: la cella e' la piu' stretta, e tre numeri
   affiancati ci starebbero solo rimpicciolendoli. */
.nrow-group-line.is-stacked {
  flex-direction: column;
  gap: 0.25rem;
  align-items: stretch;
}

/* Valore e etichetta: il numero e' il contenuto, l'etichetta la didascalia.
   Tabellari, cosi' due schede si confrontano incolonnate. */
.nrow-stat {
  display: flex;
  min-width: 0;
  flex-direction: column;
  line-height: 1.15;
}

.is-stacked .nrow-stat {
  flex-direction: row;
  gap: 0.4rem;
  align-items: baseline;
  justify-content: space-between;
}

.nrow-stat-v {
  font-size: 0.95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.is-stacked .nrow-stat-v {
  font-size: 0.88rem;
  order: 2;
}

.nrow-stat-k {
  font-size: 0.62rem;
  color: var(--text-dim);
  white-space: nowrap;
}

.is-stacked .nrow-stat-k {
  font-size: 0.68rem;
  order: 1;
}

/* --------------------------- schede compatte, da quattro nodi in su */
/*
 * Non e' il caso per cui si progetta -- i nodi saranno tre -- ma non deve
 * rompersi se capita. Si stringe, non si toglie: sparkline piu' bassa, motivi
 * a una riga, meno respiro. Da cinque in su la pagina scorre, e va bene cosi'.
 */
.report-cards[data-dense="1"] {
  gap: 0.4rem;
  grid-auto-rows: auto;
}

.report-cards[data-dense="1"] .ncard2 {
  padding: 0.4rem 0.7rem 0.5rem;
}

.report-cards[data-dense="1"] .ncard2-name {
  font-size: 0.9rem;
}

/* Da quattro nodi in su la didascalia del grafico e' la prima cosa che se ne
   va: dice sempre la stessa frase, e chi e' arrivato a guardare quattro nodi
   insieme l'ha gia' letta. I riferimenti orari invece restano -- sono il
   motivo per cui il grafico si legge. */
.report-cards[data-dense="1"] .nchart-cap {
  display: none;
}

.report-cards[data-dense="1"] .spark-md {
  height: 2.6rem;
}

.report-cards[data-dense="1"] .nreasons-brief .nreason-text {
  -webkit-line-clamp: 1;
  line-clamp: 1;
}

/* ============================================== livello 2: il dettaglio */
/*
 * Quattro colonne a tutta larghezza. A 1920 px una colonna sola sarebbe uno
 * spreco: i blocchi si prendono una o due colonne a seconda di quanto hanno
 * da dire (`.nblock-span-2`), e le classifiche lunghe -- i top IP -- ne
 * prendono due perche' un IPv6 in una colonna stretta va a capo.
 */

/*
 * `stretch` e non `start`: i blocchi di una stessa riga si pareggiano in
 * altezza. La pagina NON diventa piu' alta -- l'altezza di una riga era gia'
 * quella del blocco piu' alto -- ma lo spazio che prima restava fuori dai
 * riquadri adesso e' dentro, e chi ne sa fare qualcosa se lo prende: e' cosi'
 * che il grafico della raccolta si allunga fino a pareggiare la valutazione di
 * salute che gli sta accanto, invece di lasciare un vuoto sotto di se'.
 */
/* Lo spazio fra le righe e' 0,5rem e non 0,85 per la stessa ragione del
   margine qui sopra: sono tre intervalli, e 0,35rem l'uno fanno 17 px sul
   totale della pagina. In larghezza non cambia niente, le colonne hanno spazio
   da vendere. */
.report-detail {
  display: grid;
  gap: 0.5rem 0.85rem;
  align-items: stretch;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.report-detail[hidden] {
  display: none;
}

/* Imbottitura verticale ridotta (era 0,8 sopra e 0,9 sotto): 9 px per riga,
   27 sul totale. Quella laterale resta com'era -- il problema e' l'altezza. */
.report-detail .nblock {
  display: flex;
  flex-direction: column;
  padding: 0.55rem 0.95rem 0.6rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* La riga del verdetto: semaforo, giudizio, eventuale attacco e da quanto il
   nodo tace. Prende tutta la larghezza e sta sopra ogni altra cosa -- il nome
   del nodo e' gia' in intestazione, questo e' cio' che il nome non dice. */
.ndetail-head {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.7rem;
  align-items: center;
  padding: 0.5rem 0.9rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
  grid-column: 1 / -1;
}

.ndetail-head[data-health="ok"] {
  border-left-color: var(--sip-2xx);
}

.ndetail-head[data-health="warn"] {
  border-left-color: var(--sip-1xx);
}

.ndetail-head[data-health="bad"] {
  border-left-color: var(--sip-err);
}

.ndetail-head[data-health="note"] {
  border-left-color: var(--accent);
}

.ndetail-head .spacer {
  flex: 1;
}

.ndetail-last {
  font-size: 0.74rem;
  color: var(--text-dim);
}

.nblock-span-2 {
  grid-column: span 2;
}

.nblock-span-4 {
  grid-column: 1 / -1;
}

/* I motivi, nel dettaglio, non hanno piu' il fondo incassato della vecchia
   scheda: sono dentro un blocco come gli altri. */
.report-detail .nreasons {
  padding: 0;
  background: none;
  border-bottom: 0;
}

.ncard-dot {
  width: 0.5rem;
  height: 0.5rem;
  align-self: center;
  background: currentColor;
  border-radius: 50%;
}

.ncard-health {
  flex-shrink: 0;
  padding: 0.06rem 0.55rem;
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: var(--pill);
}

/* Le tre tinte del semaforo, condivise da pallino e pastiglia. */
.ncard-ok {
  color: var(--sip-2xx);
}

.ncard-health.ncard-ok {
  background: var(--tint-ok);
}

.ncard-warn {
  color: var(--sip-1xx);
}

.ncard-health.ncard-warn {
  background: var(--tint-warn);
}

.ncard-bad {
  color: var(--sip-err);
}

.ncard-health.ncard-bad {
  background: var(--tint-err);
}

.ncard-note {
  color: var(--accent);
}

.ncard-health.ncard-note {
  background: var(--tint-info);
}

/* Nodo noto ma muto nella finestra: si spiega, invece di mostrare quattro
   blocchi di zeri, che si leggono come "tutto a posto". */
.ncard-quiet {
  padding: 1.6rem 1rem 1.8rem;
  color: var(--text-muted);
  text-align: center;
}

.ncard-quiet strong {
  display: block;
  margin-bottom: 0.4rem;
  font-size: 0.84rem;
  color: var(--text);
}

.ncard-quiet p {
  max-width: 30rem;
  margin: 0 auto 0.45rem;
  font-size: 0.76rem;
  line-height: 1.5;
}

.ncard-quiet .nhint {
  font-size: 0.72rem;
  color: var(--text-dim);
}

/* ------------------------------------------------------- i blocchi */
/* Usati solo dal dettaglio: la panoramica non ha blocchi, ha celle. */

.nblock {
  min-width: 0;
}

.nblock-h {
  margin: 0 0 0.6rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent);
}

/* ------------------------------------------------------- metriche */

.nm-grid {
  display: grid;
  gap: 0.5rem 0.9rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.nm {
  min-width: 0;
}

.nm-k {
  display: block;
  font-size: 0.66rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/* I numeri sono il contenuto della pagina: piu' grandi delle etichette e
   tabellari, cosi' due blocchi affiancati si confrontano incolonnati. */
.nm-v {
  display: block;
  font-size: 0.95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  overflow-wrap: anywhere;
}

.nm-ok {
  color: var(--sip-2xx);
}

.nm-warn {
  color: var(--sip-1xx);
}

.nm-bad {
  color: var(--sip-err);
}

/* Le quattro metriche della raccolta stanno su una riga sola: il blocco e'
   largo mezza pagina, e due righe da due occupavano in altezza uno spazio che
   c'era in larghezza. Quel che si risparmia lo prende il grafico sotto. Sotto
   i 560 px si torna a due colonne insieme a tutte le altre griglie -- ed e'
   per questo che il selettore compare anche laggiu': a parita' di media query
   vince la specificita', e `.nblock-collect .nm-grid` batte `.nm-grid`. */
.nblock-collect .nm-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.nnote {
  margin: 0.7rem 0 0;
  font-size: 0.68rem;
  line-height: 1.45;
  color: var(--text-dim);
}

.nempty {
  margin: 0.2rem 0 0;
  font-size: 0.72rem;
  color: var(--text-dim);
}

/* -------------------------------------------- grafico della raccolta */
/*
 * Quattro pezzi: didascalia, scala verticale, disegno e asse orario. Il
 * disegno e' SVG scritto a mano; TUTTO IL TESTO E' HTML e sta attorno, mai
 * dentro. La ragione e' `preserveAspectRatio: none`: quello stiramento e' cio'
 * che permette al grafico di prendersi la larghezza disponibile, ma
 * deformerebbe qualunque lettera scritta nell'SVG -- a 1920 px un "14:35"
 * diventerebbe largo il triplo e alto uguale.
 *
 * La griglia a due colonne tiene incolonnati il disegno e le sue etichette
 * orarie: la scala verticale occupa la prima colonna in tutte e due le righe,
 * quindi l'asse comincia esattamente dove comincia il disegno. Con due
 * elementi indipendenti si sarebbe disallineato al primo numero a quattro
 * cifre.
 */

/* Nel dettaglio il grafico e' l'ULTIMO figlio del suo blocco, ed e' l'unico
   pezzo che sa cosa fare di altezza in piu': `flex: 1` glielo assegna, e
   `min-height: 0` gli impedisce di rifiutarla (un elemento flessibile non
   scende sotto la propria altezza di contenuto senza questa riga). */
.nspark-wrap {
  display: flex;
  flex: 1;
  min-height: 0;
  margin-top: 0.6rem;
}

/* Tre righe: didascalia, disegno, asse orario. Solo quella di mezzo si
   allunga -- il testo sopra e sotto vale sempre la sua riga e basta. */
.nchart {
  display: grid;
  flex: 1;
  min-width: 0;
  margin: 0;
  column-gap: 0.4rem;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
}

/* Cosa si sta contando e con quale passo. Senza, il disegno e' una linea che
   sale: non un dato. */
.nchart-cap {
  margin-bottom: 0.25rem;
  font-size: 0.64rem;
  line-height: 1.3;
  color: var(--text-dim);
  grid-column: 1 / -1;
}

.nchart-cap b {
  font-weight: 600;
  color: var(--text-muted);
}

/* La scala verticale: picco, zero e livelli tondi in mezzo. Le etichette sono
   posizionate in percentuale sull'altezza del disegno -- non distribuite in
   colonna -- perche' devono cadere ESATTAMENTE sulla loro linea di griglia:
   una scala che indica un valore due pixel sopra la sua riga e' peggio che
   nessuna scala. */
.nchart-scale {
  position: relative;
  min-width: 1.6rem;
  font-size: 0.62rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-align: right;
}

/* Il contrasto e' il punto della modifica: prima erano `--text-dim`, cioe' lo
   stesso grigio delle didascalie di contorno, e a 0,6rem sparivano. I NUMERI
   si leggono (`--text-muted`), le LINEE restano discrete: sono due ruoli
   diversi e non devono avere lo stesso peso. */
.nchart-y {
  position: absolute;
  right: 0;
  color: var(--text-muted);
  white-space: nowrap;
  transform: translateY(50%);
}

/* Il picco e' il numero che si va a cercare per primo: pieno contrasto. */
.nchart-ymax {
  font-weight: 600;
  color: var(--text);
}

/* Lo zero e' un riferimento, non un dato: resta spento. */
.nchart-y0 {
  color: var(--text-dim);
}

/* L'asse orario. Le etichette sono posizionate in percentuale sulla stessa
   scala del disegno; i due estremi sono ancorati ai bordi invece che centrati
   sulla loro tacca, altrimenti la prima uscirebbe di mezzo carattere a
   sinistra e l'ultima altrettanto a destra. */
.nchart-x {
  position: relative;
  height: 0.95rem;
  margin-top: 0.2rem;
  font-size: 0.6rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text-dim);
  grid-column: 2;
}

.nchart-t {
  position: absolute;
  top: 0;
  white-space: nowrap;
  transform: translateX(-50%);
}

.nchart-t.is-first {
  left: 0;
  transform: none;
}

.nchart-t.is-last {
  right: 0;
  transform: none;
}

/* Altezza fissa e larghezza libera: il disegno si allunga con il contenitore.
   Lo stiramento non deforma la linea grazie a `non-scaling-stroke` (in
   report.js), che e' l'unica ragione per cui si puo' usare
   `preserveAspectRatio: none` senza che si veda -- ed e' anche il motivo per
   cui lo STESSO disegno serve il grafico basso della panoramica e quello alto
   del dettaglio (`.spark-lg`): cambia solo l'altezza. */
.spark {
  display: block;
  width: 100%;
  height: 2.6rem;
  overflow: visible;
  grid-column: 2;
}

/* Le due varianti alte: la scheda di panoramica e il blocco del dettaglio. In
   tutti e due i posti il grafico non e' piu' un segno in fondo a una cella ma
   IL grafico, ed e' il primo posto dove si vede un buco o una raffica.
   Vanno DOPO `.spark`, che ha la stessa specificita': a parita' vince
   l'ultima scritta, ed e' un errore che non si vede leggendo il codice --
   le classi stanno su elementi diversi e sembrano indipendenti. */
.spark-md {
  height: 5rem;
}

/* Nel dettaglio l'altezza non e' un numero fisso ma quella che avanza nel
   blocco (vedi `.nspark-wrap`), fra un minimo sotto il quale il disegno
   smetterebbe di dire qualcosa e un TETTO. E' l'unica variante che si comporta
   cosi': in panoramica l'altezza E' il vincolo, e va tenuta ferma.

   Il tetto c'e' perche' `.nspark-wrap` e' `flex: 1` e il disegno si allunga per
   riempire la riga: misurato a 1920 px arrivava a ~200 px, cioe' meta' in piu'
   del minimo, senza dire niente di piu' -- la curva e' la stessa, solo piu'
   alta. Con il tetto la riga della griglia puo' scendere, che e' l'unico modo
   di far entrare il dettaglio in uno schermo Full HD senza togliere dati.

   10rem e non meno: sotto i 160 px le quattro tacche della scala verticale
   (picco, due livelli tondi, zero) si accavallano. */
.spark-lg {
  height: 100%;
  min-height: 7rem;
  max-height: 10rem;
}

.spark-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linejoin: round;
}

.spark-area {
  fill: var(--accent-soft);
  stroke: none;
}

/* La base dello zero e la linea del massimo: senza, una spezzata bassa e una
   alta si leggono uguali. Spente entrambe -- sono riferimenti, non dati. */
.spark-zero {
  stroke: var(--border-strong);
  stroke-width: 1;
}

.spark-max {
  stroke: var(--border);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}

/* Le tacche orarie, sotto il disegno per ordine di scrittura: si vedono
   attraverso l'area, che e' semitrasparente. */
.spark-tick {
  stroke: var(--border);
  stroke-width: 1;
}

/* Le linee dei livelli intermedi. Piu' spente delle tacche orarie: sono tante
   di piu' e attraversano tutto il disegno, quindi devono farsi guardare solo
   quando le si cerca. Il dato e' la serie, non la griglia. */
.spark-grid {
  stroke: var(--border);
  stroke-width: 1;
  opacity: 0.55;
}

/* Finestra senza traffico: una riga piatta e spenta. Un riquadro vuoto si
   confonderebbe con un caricamento non riuscito. */
.spark-flat {
  stroke: var(--border-strong);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

/* Un rettangolo trasparente per intervallo: serve solo a dare al browser
   qualcosa su cui mostrare il <title> con orario e valore. `fill:transparent`
   e non `fill:none`, che non riceve il puntatore. */
.spark-hit {
  fill: transparent;
}

.spark-hit:hover {
  fill: var(--accent-soft);
}

/* ------------------------------------------- barra legittimo/scanner */

.nbar-wrap {
  margin-top: 0.8rem;
}

.nbar {
  display: flex;
  height: 0.55rem;
  overflow: hidden;
  background: var(--inset);
  border-radius: var(--pill);
}

.nbar.is-empty {
  background: repeating-linear-gradient(90deg, var(--inset) 0 6px, transparent 6px 12px);
}

.nbar-legit {
  background: var(--sip-2xx);
}

.nbar-scanner {
  background: var(--sip-err);
}

.nbar-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1rem;
  margin-top: 0.4rem;
  font-size: 0.7rem;
}

.nbar-key {
  display: inline-flex;
  gap: 0.35rem;
  align-items: center;
  color: var(--text-muted);
}

.nbar-key::before {
  width: 0.42rem;
  height: 0.42rem;
  content: "";
  background: currentColor;
  border-radius: 2px;
}

.nbar-key-legit {
  color: var(--sip-2xx);
}

.nbar-key-scanner {
  color: var(--sip-err);
}

/* La forma compatta della legenda (una quota sola, la percentuale di
   scansione; i due conteggi nel tooltip) si veste dentro la cella Sicurezza
   della panoramica -- vedi `.ncell-security` piu' sopra. */

/* ---------------------------------------------------- classifiche */

.nrank {
  display: grid;
  gap: 0.28rem 0.55rem;
  margin: 0;
  padding: 0;
  list-style: none;
  grid-template-columns: minmax(0, auto) minmax(2.5rem, 1fr) max-content;
}

.nrank-row {
  display: grid;
  gap: inherit;
  align-items: center;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
}

.nrank-k {
  min-width: 0;
  font-size: 0.72rem;
  color: var(--text);
  overflow-wrap: anywhere;
}

.nrank-v {
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  text-align: right;
}

/* La barra e' quel che rende leggibile "il primo vale dieci volte il
   secondo" senza doverlo calcolare a mente. */
.nrank-track {
  height: 0.35rem;
  overflow: hidden;
  background: var(--inset);
  border-radius: var(--pill);
}

.nrank-fill {
  display: block;
  height: 100%;
  background: var(--text-dim);
  border-radius: var(--pill);
}

.nrank-bad .nrank-fill {
  background: var(--sip-err);
  opacity: 0.75;
}

.nrank-warn .nrank-fill {
  background: var(--sip-1xx);
  opacity: 0.7;
}

/* Un browser senza `subgrid` mette comunque le tre parti in fila: peggio
   incolonnate, mai rotte. */
@supports not (grid-template-columns: subgrid) {
  .nrank-row {
    display: flex;
    gap: 0.55rem;
  }

  .nrank-k {
    flex: 0 1 auto;
  }

  .nrank-track {
    flex: 1 1 2.5rem;
  }
}

/* -------------------------------------------- rimando a un'altra vista */

/* `align-self` perche' nel dettaglio il blocco che lo contiene e' una colonna
   flessibile (vedi `.report-detail .nblock`): senza, il pulsante si stirerebbe
   per tutta la larghezza del blocco e il suo testo finirebbe al centro. */
.nlink {
  align-self: flex-start;
  margin-top: 0.8rem;
  padding: 0;
  font-size: 0.72rem;
  color: var(--accent);
  cursor: pointer;
  background: none;
  border: 0;
}

.nlink:hover {
  color: var(--accent-strong);
  text-decoration: underline;
}

.nlink:focus-visible {
  border-radius: var(--radius-sm);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --------------------------------------------------------- stretto */
/*
 * Il vincolo di stare in uno schermo senza scorrere vale a 1920x1080. Sotto
 * quella misura si scorre, e va bene: quello che NON deve succedere e' che
 * qualcosa esca di lato. Le soglie qui sotto sono i punti in cui il corpo
 * della scheda smette di starci in orizzontale, e a ognuno si perde una
 * colonna invece di stringere tutto -- stringere tutto e' il modo di ottenere
 * sei colonne illeggibili invece di tre leggibili.
 *
 * La scheda cresce in altezza a ogni passaggio, e va bene: sotto i 1080 px non
 * c'e' piu' nessuna promessa di stare in uno schermo.
 */

/* Sotto i 1500 px le sei colonne non ci stanno piu' in fila: tre e tre. */
@media (max-width: 1500px) {
  .ncard2-body {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 1080px) {
  /* Due e due. Sotto questa larghezza la sparkline in un sesto di riga
     sarebbe un francobollo. */
  .ncard2-body {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* Il dettaglio scende a due colonne: quattro, a questa larghezza, sarebbero
     larghe quanto le etichette che contengono. */
  .report-detail {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .nblock-span-2 {
    grid-column: 1 / -1;
  }
}

@media (max-width: 900px) {
  .report-content,
  .panel-report[data-mode="detail"] .report-content {
    padding: 0.7rem 0.7rem 1.2rem;
  }

  /* Le schede non sono piu' tutte alte uguale: a questa larghezza si scorre
     comunque, e pareggiarle vorrebbe dire solo allungare le piu' corte. */
  .report-cards {
    grid-auto-rows: auto;
  }
}

/* Telefono: tutto in colonna. */
@media (max-width: 560px) {
  /* Ritorno, titolo, selettore della finestra e orologio non ci stanno su una
     riga sola: si va a capo invece di spingersi fuori a vicenda. Il titolo
     tiene un minimo perche' e' il nome del nodo, non un'etichetta fissa: se lo
     si lascia comprimere, sparisce del tutto dietro l'ellissi. */
  .panel-report .panel-head {
    flex-wrap: wrap;
    row-gap: 0.4rem;
  }

  .panel-report .panel-head h2 {
    flex: 1 1 8rem;
  }

  .ncard2-body,
  .report-detail {
    grid-template-columns: minmax(0, 1fr);
  }

  /* La freccia sparisce: a questa larghezza la scheda e' un blocco intero, e
     che sia toccabile si vede da solo. */
  .ncard2-go {
    display: none;
  }

  .nm-grid,
  .ncell-quality .nm-grid,
  .nblock-collect .nm-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* -------------------------------------------- valutazione di salute (Nodi) */
/*
 * Il semaforo della scheda riflette il giudizio completo, non piu' la sola
 * raccolta dati, e sotto l'intestazione compare l'elenco dei motivi. Regola
 * che vale per tutto questo blocco: il colore da' l'urgenza, il testo dice
 * cosa fare. Se il colore dovesse portare da solo l'informazione, vorrebbe
 * dire che il testo non e' scritto bene.
 */

/* Riga di sintesi. Sta fuori dal corpo che scorre, quindi resta visibile
   anche scorrendo un elenco lungo di nodi. */
.report-summary {
  display: flex;
  flex-shrink: 0;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  align-items: center;
  padding: 0.5rem 1.15rem;
  font-size: 0.74rem;
  background: var(--panel-2);
  border-bottom: 1px solid var(--border);
}

.report-summary[hidden] {
  display: none;
}

.nsum-total {
  font-weight: 700;
  color: var(--text-muted);
}

.nsum-ok {
  color: var(--sip-2xx);
}

.nsum-warn {
  color: var(--sip-1xx);
}

.nsum-bad {
  color: var(--sip-err);
}

.nsum-note {
  color: var(--accent);
}

.nsum-unknown {
  color: var(--text-dim);
}

/* "N nodi sotto attacco": e' la voce che deve restare in testa allo sguardo
   mentre si legge il resto della riga, quindi ha un fondo suo. Nessuna
   animazione: una cosa che lampeggia si guarda una volta e poi si copre. */
.nsum-attack {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  padding: 0.1rem 0.65rem;
  font-weight: 700;
  color: var(--sip-err);
  background: var(--tint-err);
  border: 1px solid rgba(240, 85, 91, 0.35);
  border-radius: var(--pill);
}

.nsum-attack-mark {
  font-size: 0.7em;
}

/* ------------------------------------------------------- il giudizio */

/* Il quarto stato: la valutazione non e' riuscita. Non e' verde e non e'
   rosso -- dire "va tutto bene" quando non lo si sa e' il difetto peggiore. */
.ncard-unknown {
  color: var(--text-dim);
}

.ncard-health.ncard-unknown {
  background: rgba(139, 148, 166, 0.1);
}

/* La pastiglia "sotto attacco", uguale nei due livelli: in panoramica sta
   accanto al motivo, nel dettaglio in intestazione. Si vede da lontano senza
   muoversi, e senza lampeggiare. */
.ncard-attack {
  display: inline-flex;
  gap: 0.35rem;
  align-items: center;
  padding: 0.08rem 0.55rem;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sip-err);
  background: var(--tint-err);
  border: 1px solid rgba(240, 85, 91, 0.35);
  border-radius: var(--pill);
}

.ncard-attack-mark {
  font-size: 0.75em;
}

/* -------------------------------------------------- elenco dei motivi */

.nreasons {
  margin: 0;
  padding: 0.55rem 0.9rem 0.7rem;
  list-style: none;
  background: var(--inset);
  border-bottom: 1px solid var(--border);
}

.nreason {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.5rem;
  align-items: baseline;
  padding: 0.22rem 0;
  font-size: 0.75rem;
  line-height: 1.45;
  color: var(--text);
}

.nreason + .nreason {
  border-top: 1px solid var(--border);
}

.nreason-text {
  flex: 1 1 12rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* La natura del motivo: attacco / servizio / raccolta. Serve a non confondere
   "qualcuno sta bussando" con "il centralino risponde male", che si risolvono
   in modi diversi e con due fretta diverse. */
.nreason-kind {
  flex: 0 0 auto;
  min-width: 4.6rem;
  padding: 0.02rem 0.45rem;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-align: center;
  text-transform: uppercase;
  border-radius: var(--pill);
}

.nreason-attacco .nreason-kind {
  color: var(--sip-err);
  background: var(--tint-err);
}

/* Un attacco RIUSCITO. E' l'unica riga della pagina che dice "qualcuno e'
   dentro", e non puo' somigliare a quelle che dicono "qualcuno prova":
   pastiglia piena invece che tinta. */
.nreason-conseguenza .nreason-kind {
  color: var(--bg);
  background: var(--sip-err);
}

/* Un attacco contenuto: la pastiglia resta quella dell'attacco (perche' di
   quello si tratta) ma spenta, come il testo qui sotto. */
.nreason-nota .nreason-kind {
  color: var(--text-muted);
  background: var(--tint-info);
}

.nreason-servizio .nreason-kind {
  color: var(--sip-1xx);
  background: var(--tint-warn);
}

.nreason-raccolta .nreason-kind {
  color: var(--accent);
  background: var(--tint-info);
}

/* La gravita' si legge dal testo, non dalla pastiglia: un motivo critico ha
   il testo pieno, uno di attenzione resta piu' spento. */
.nreason-critico .nreason-text {
  font-weight: 700;
}

.nreason-attenzione .nreason-text {
  color: var(--text);
}

/* Contenuto: si legge, ma non chiama. E' la gravita' piu' quieta che esista
   sopra "niente", e deve stare sotto l'attenzione anche a colpo d'occhio. */
.nreason-nota .nreason-text {
  color: var(--text-muted);
  font-weight: 400;
}

/* Nessun allarme, oppure valutazione mancata: righe di servizio, spente. */
.nreason-quiet,
.nreason-unknown {
  color: var(--text-dim);
  font-size: 0.73rem;
}

.nreason-unknown {
  color: var(--sip-1xx);
}

/* ===================================================== le prove di un motivo

   Il pannello che sta sotto un motivo d'allarme e dice CHE COSA l'ha causato e
   QUANDO e' successo l'ultimo episodio. Sta nel dettaglio del nodo -- la
   panoramica non mostra motivi, quindi non cresce di un pixel -- ed e' chiuso
   finche' non lo si apre.

   La gerarchia e' voluta e ha una ragione sola: la prima riga e' l'ORA
   DELL'ULTIMO TENTATIVO, perche' e' con quella che si giudica se la cosa sta
   ancora succedendo. Tutto il resto -- utenze, sorgenti, numeri -- risponde a
   "che cos'era", che e' la seconda domanda, non la prima. */

.nreason-prove {
  flex: 0 0 auto;
  padding: 0;
  font-size: 0.7rem;
  color: var(--text-muted);
  cursor: pointer;
  background: none;
  border: 0;
  border-bottom: 1px dotted currentcolor;
}

.nreason-prove:hover {
  color: var(--text);
}

.nprove {
  flex: 1 1 100%;
  margin: 0.4rem 0 0.2rem;
  padding: 0.5rem 0.65rem 0.6rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius, 4px);
}

/* L'ultimo tentativo, che e' il dato del giudizio: piu' grande di tutto il
   resto del pannello, e con l'ora assoluta accanto per chi deve annotarla. */
.nprove-last {
  margin: 0 0 0.3rem;
  font-size: 0.86rem;
  line-height: 1.3;
}

.nprove-last-value {
  font-weight: 700;
}

/* Caldo se e' dentro la finestra di "adesso", spento se e' passato. La soglia
   e' quella della valutazione (HEALTH_RECENT_MINUTES), letta dalla risposta:
   il colore e il verdetto non possono dire due cose diverse. */
.nprove-last[data-tone="live"] .nprove-last-value {
  color: var(--sip-err);
}

.nprove-last[data-tone="quiet"] .nprove-last-value {
  color: var(--text-muted);
}

.nprove-last-abs {
  margin-left: 0.4rem;
  font-size: 0.7rem;
  color: var(--text-dim);
}

.nprove-rates {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.9rem;
  margin: 0 0 0.35rem;
  font-size: 0.72rem;
  color: var(--text-muted);
}

.nprove-rate[data-tone="live"] {
  font-weight: 700;
  color: var(--sip-err);
}

/* ------------------------------------------ l'andamento dell'ultima ora

   Dodici barre da cinque minuti, con didascalia sopra e asse orario sotto. La
   prima versione erano rettangoli grigi senza titolo ne' unita' ne' orari, e
   un utente ha dovuto chiedere cosa fossero: senza un'unita' di misura un
   disegno non e' un dato (§5.19). Le lettere stanno in HTML attorno al
   disegno, mai dentro: l'SVG e' stirato in larghezza e le deformerebbe
   (§5.18).

   ATTENZIONE ALL'ORDINE (§5.13): le varianti stanno TUTTE sotto la loro base,
   nello stesso blocco. A parita' di specificita' vince l'ultima regola del
   file, e una variante scritta a monte della base non ha alcun effetto --
   errore gia' pagato due volte con `.spark-lg` e `.spark-md`. */

.nprove-chart {
  margin: 0 0 0.5rem;
}

/* Cosa si conta e con che passo, piu' il massimo su cui le barre sono in
   scala: senza, una barra alta puo' essere tre tentativi o tremila. */
.nprove-cap {
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.3rem;
  align-items: baseline;
  margin-bottom: 0.2rem;
  font-size: 0.64rem;
  line-height: 1.3;
  color: var(--text-dim);
}

.nprove-cap b {
  font-weight: 600;
  color: var(--text-muted);
}

/* Il massimo si stacca a destra: e' un numero, non parte della frase. */
.nprove-max {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

/* Altezza fissa, larghezza libera. Dentro ci sono SOLO rettangoli pieni:
   nessun tratto, quindi lo stiramento non deforma niente e non serve
   `non-scaling-stroke`. */
.nprove-bars {
  display: block;
  width: 100%;
  height: 30px;
  background: var(--inset);
}

/* La casella di un intervallo, disegnata SEMPRE, anche vuota. E' il pezzo che
   mancava: con le sole barre piene, un intervallo solo su dodici era un
   rettangolo isolato in mezzo al nulla e si leggeva come un segnaposto. Con la
   griglia si legge "uno su dodici", che e' l'informazione. */
.nprove-slot {
  fill: var(--border);
}

/* La barra di un valore. Il tono lo decide lo STATO DEL MOTIVO, non il
   disegno, e la variante sta sotto la base (vedi la nota sull'ordine):

     in corso   il rosso dell'allarme: sta succedendo adesso
     contenuto  il ciano dei dati: e' successo, e non c'e' niente da fare

   Rosso pieno anche da contenuto farebbe leggere come allarme un attacco che
   fail2ban ha gia' chiuso -- cioe' la stanchezza da allarme di §5.12 rimessa
   dentro dalla porta del colore, subito dopo che §5.36 l'aveva tolta dalla
   gravita'. Il ciano e' lo stesso che la pagina usa per la gravita' `nota`:
   il disegno e la pastiglia del motivo dicono la stessa cosa. */
.nprove-bar {
  fill: var(--text-muted);
}

.nprove-chart[data-stato="in-corso"] .nprove-bar {
  fill: var(--sip-err);
}

.nprove-chart[data-stato="contenuto"] .nprove-bar {
  fill: var(--accent);
}

/* L'ultimo intervallo e' quello su cui si sta decidendo: si distingue con la
   pienezza, non con un colore diverso -- due colori nello stesso disegno
   direbbero che sono due grandezze. */
.nprove-bar.is-now {
  filter: brightness(1.25);
}

/* Un rettangolo trasparente per intervallo: serve solo a dare al browser
   qualcosa su cui mostrare il <title> con orario e conteggio. `transparent` e
   non `none`, che il puntatore non lo riceve. */
.nprove-hit {
  fill: transparent;
}

.nprove-hit:hover {
  fill: var(--tint-info);
}

/* L'asse orario: inizio a sinistra, "adesso" a destra. Gli estremi sono
   ancorati ai bordi e non centrati sulla loro tacca, altrimenti uscirebbero di
   mezzo carattere fuori dal disegno. */
.nprove-x {
  position: relative;
  height: 0.9rem;
  margin-top: 0.15rem;
  font-size: 0.6rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text-dim);
}

.nprove-t {
  position: absolute;
  top: 0;
  white-space: nowrap;
}

.nprove-t.is-first {
  left: 0;
}

/* "adesso" e' il lato che risponde alla domanda del pannello, quindi si legge
   un gradino piu' acceso del resto dell'asse. */
.nprove-t.is-last {
  right: 0;
  font-weight: 600;
  color: var(--text-muted);
}

/* Due colonne su schermo largo, una sola quando non ci stanno: le prove sono
   elenchi corti e non devono mai far comparire una barra di scorrimento. */
.nprove-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.4rem 1rem;
}

.nprove-h {
  margin: 0 0 0.15rem;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  color: var(--text-dim);
  text-transform: uppercase;
}

.nprove-list {
  margin: 0;
  padding: 0;
  font-size: 0.73rem;
  list-style: none;
}

.nprove-list li {
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
  padding: 0.05rem 0;
}

.nprove-who {
  min-width: 0;
  overflow-wrap: anywhere;
}

.nprove-n {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* "mai autenticata", "ha ottenuto risposta": la qualifica che cambia il senso
   della riga. Spenta, perche' e' un contorno del numero. */
.nprove-tag {
  flex: 0 0 auto;
  font-size: 0.62rem;
  color: var(--text-dim);
}

.nprove-empty,
.nprove-window {
  margin: 0.15rem 0 0;
  font-size: 0.68rem;
  color: var(--text-dim);
}

/* Il rimando alla vista corrispondente, gia' filtrata su questo nodo: e' la
   differenza fra sapere che c'e' un problema e poterlo andare a guardare. */
.nreason-go {
  flex: 0 0 auto;
  padding: 0;
  font-size: 0.7rem;
  color: var(--accent);
  cursor: pointer;
  background: none;
  border: 0;
}

.nreason-go::after {
  content: " →";
}

.nreason-go:hover {
  color: var(--accent-strong);
  text-decoration: underline;
}

.nreason-go:focus-visible {
  border-radius: var(--radius-sm);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (max-width: 560px) {
  .report-summary {
    padding: 0.5rem 0.85rem;
  }

  /* A questa larghezza la pastiglia e il testo in fila non ci stanno: la
     pastiglia va sopra, e il testo si prende la riga intera. */
  .nreason-kind {
    min-width: 0;
  }

  .nreason-text {
    flex: 1 1 100%;
  }
}

/* ============================================ pannello di amministrazione
   Pagina a se' (/admin), ma con la STESSA STRUTTURA dell'app: `body` porta
   `app-page` insieme ad `admin-page`, quindi sidebar a sinistra, colonna a
   destra e i due breakpoint che la piegano arrivano gia' fatti da lassu'.
   Qui sotto ci sono solo le differenze: la barra in alto, la colonna che
   scorre, e i pochi elementi che l'app non ha (pastiglie dei permessi, stato
   dei nodi). Sezioni, tabelle, bottoni, righe-editor e messaggi sono quelli di
   sempre.

   PERCHE' UN MENU E NON DELLE SCHEDE. Le sezioni cresceranno (Notifiche e' gia'
   prevista): in colonna una voce in piu' e' una riga in piu', mentre in
   orizzontale si contenderebbe la barra con titolo, ritorno all'app, nome
   utente ed Esci -- e sotto i 900px quella riga andrebbe a capo. In piu' la
   voce accesa resta visibile mentre si scorre una tabella lunga, cosa che una
   scheda in cima alla pagina smette di fare al primo giro di rotella. */

/* Stessa barra dell'app (.topbar, da cui eredita fondo, filo e spaziature):
   qui dentro cambia solo cosa ci sta sopra. */
.admin-top {
  flex-wrap: wrap;
}

/* Il titolo dice a chiare lettere che questa NON e' l'applicazione: e'
   l'unica differenza che conta fra le due pagine, e va vista subito. */
.admin-title {
  margin: 0;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

/* La sezione corrente accanto al titolo. Serve davvero solo sotto i 560px,
   dove il menu si riduce alle icone, ma tenerla sempre costa una parola e
   toglie l'unico momento in cui la pagina non dice dove si e'. */
.admin-where {
  padding-left: 0.9rem;
  font-size: 0.78rem;
  color: var(--text-muted);
  border-left: 1px solid var(--border-strong);
}

.admin-top .spacer {
  flex: 1;
}

.admin-top .who {
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* Il ritorno all'app e' un link, e si vede che lo e'. */
.admin-top a.btn-quiet {
  text-decoration: none;
}

/* Colonna unica con un tetto: una riga di testo lunga quanto un monitor da
   1920px non si legge, e qui di testo da leggere ce n'e'. Scorre da sola,
   dentro la sua colonna: senza `min-height: 0` un figlio flex non si stringe
   sotto il proprio contenuto e a scorrere finirebbe la pagina intera, portandosi
   via anche la barra in alto. */
.admin-main {
  flex: 1;
  min-height: 0;
  width: 100%;
  max-width: 72rem;
  padding: 1.2rem 1.15rem 3rem;
  overflow-y: auto;
}

/* Le sezioni si alternano: ne e' visibile una sola per volta (l'attributo
   hidden lo mette il router in js/admin.js). */
.admin-section[hidden] {
  display: none;
}

/* La prima sezione di una pagina non ha il filo sopra ne' il margine: sono
   pensati per separare due sezioni, e in cima separerebbero dal nulla. */
.admin-section > .settings-section:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Impostazioni ha una riga sua sopra il contenuto, per la data dell'ultima
   modifica: sta a destra e in tono minore, com'e' nelle teste dei pannelli
   dell'app. Il titolo della sezione e' li' per gli screen reader (.sr-only) e
   non si vede: a schermo lo dice gia' il menu, e ripeterlo aggiungerebbe una
   riga di rumore in cima a ogni sezione. */
.admin-section-head {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  justify-content: flex-end;
}

.admin-section-head .count {
  font-size: 0.74rem;
  color: var(--text-muted);
}

/* Il contenuto di Impostazioni qui non sta dentro un pannello con la sua
   imbottitura: la mette gia' .admin-main, e sommarle sposterebbe la colonna
   verso destra rispetto alle altre due sezioni. */
.admin-settings-content {
  max-width: none;
  padding: 0;
}

/* ------------------------------------------------ notifiche (SMTP) */

/* La nota dentro una sezione, senza il margine di quella in cima alla pagina:
   qui apre il contenuto invece di staccarsene. */
.admin-note-plain {
  margin: 0 0 1.4rem;
}

/* Il suggerimento sotto il campo password. In rosso quando dice che la
   password c'e' ma non si decifra piu': non e' un dettaglio, e' una cosa da
   fare prima del prossimo invio. */
.settings-hint[data-kind='error'] {
  color: var(--sip-err);
}

/* Esito dell'invio di prova: il testo del server va a capo dove l'ha messo il
   server, e le risposte SMTP lunghe non devono allargare la colonna. */
#smtp-test-result .hint {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Le pastiglie del registro: stessa forma dei ruoli, due colori soli. */
.role-pill.esito-ok {
  color: var(--sip-2xx);
  background: var(--tint-ok);
}

.role-pill.esito-ko {
  color: var(--sip-err);
  background: var(--tint-err);
}

/* Le pastiglie della gravita' di un avviso. Gli stessi due colori del resto
   dell'interfaccia: il giallo dell'attenzione e il rosso del critico sono gia'
   quelli della pagina Nodi, e un terzo vocabolario cromatico per dire la stessa
   cosa costringerebbe a impararlo. */
.role-pill.severita-attenzione {
  color: var(--sip-3xx);
  background: var(--tint-warn);
}

.role-pill.severita-critico {
  color: var(--sip-err);
  background: var(--tint-err);
}

/* "risolto" nel registro: la condizione e' rientrata, e si legge verde come
   ogni altro esito buono. */
.role-pill.severita-risolto {
  color: var(--sip-2xx);
  background: var(--tint-ok);
}

/* La riga in cima alla sezione Notifiche: dice se gli avvisi stanno davvero
   funzionando. Gialla quando non funzionano e non rossa, perche' gli invii
   sospesi sono una scelta legittima, non un guasto. */
#alerts-banner[data-kind='ok'] {
  border-left: 3px solid var(--sip-2xx);
  background: var(--tint-ok);
  padding: 0.7rem 0.9rem;
}

#alerts-banner[data-kind='warn'] {
  border-left: 3px solid var(--sip-3xx);
  background: var(--tint-warn);
  padding: 0.7rem 0.9rem;
}

/* Nella rubrica la gravita' si cambia da un menu dentro la cella: senza questo
   il select prenderebbe la larghezza di default e allargherebbe la colonna. */
#alerts-rec-table select {
  max-width: 14rem;
}

/* La colonna del dettaglio e' l'unica che puo' contenere una frase lunga: le
   si da' un tetto e la si lascia andare a capo, invece di far scorrere tutta
   la tabella per una riga sola. */
.settings-table .col-dettaglio {
  max-width: 32rem;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* La nota in cima: due cose che la pagina da sola non dice (le modifiche
   valgono subito; agli admin i permessi non si applicano). Fondo tenue e barra
   ciano: si legge una volta e poi non disturba piu'. */
.admin-note {
  margin: 0 0 1.8rem;
  padding: 0.7rem 0.9rem;
  font-size: 0.78rem;
  line-height: 1.6;
  color: var(--text-muted);
  background: var(--accent-soft);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
}

.admin-note strong {
  color: var(--text);
}

/* Avvertenza dentro una sezione: stessa forma della nota ma in giallo, perche'
   descrive una situazione da sistemare (i nodi non classificati). */
.settings-desc.admin-warn {
  padding: 0.6rem 0.8rem;
  color: var(--text-muted);
  background: var(--tint-warn);
  border-left: 3px solid var(--sip-1xx);
  border-radius: var(--radius-sm);
}

.settings-desc code {
  padding: 0.05rem 0.3rem;
  font-family: var(--mono);
  font-size: 0.95em;
  color: var(--text);
  background: var(--inset);
  border-radius: var(--radius-sm);
}

/* La prima sezione non ha il filo sopra: sotto la nota ce ne sarebbero due. */
.admin-main .settings-section:first-of-type {
  margin-top: 0;
}

/* ------------------------------------- gruppi di caselle dei permessi */

/* Un fieldset per asse (ambienti, viste). Le caselle sono le stesse pillole
   dei filtri (.check): si accendono in ciano quando sono spuntate, e a colpo
   d'occhio si vede cosa e' concesso senza leggere le etichette una per una. */
.perm-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.4rem;
  align-items: center;
  margin: 0;
  padding: 0.45rem 0.6rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.perm-group legend {
  padding: 0 0.35rem;
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.perm-check {
  padding: 0.22rem 0.7rem;
  font-size: 0.74rem;
}

/* Avviso "nessuna vista / nessun ambiente": non blocca il salvataggio, dice
   solo cosa comporta. Va a capo per conto suo, sotto i controlli. */
.form-msg.perm-warn,
.form-msg.perm-note {
  flex: 1 1 100%;
  line-height: 1.5;
  white-space: normal;
}

.form-msg.perm-warn {
  color: var(--sip-1xx);
}

/* ------------------------------------------- pastiglie dei permessi */

/* In tabella i permessi sono elenchi corti: pastiglie invece di testo separato
   da virgole, cosi' due righe si confrontano guardando le forme. */
.perm-chip {
  display: inline-block;
  margin: 0.1rem 0.25rem 0.1rem 0;
  padding: 0.08rem 0.55rem;
  font-size: 0.7rem;
  color: var(--text-muted);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

/* "tutto": e' un amministratore, e i permessi memorizzati non contano. */
.perm-chip-all {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
  cursor: help;
}

/* "nessuno" non e' un errore -- e' un modo di sospendere un accesso senza
   cancellarlo -- ma deve saltare all'occhio, perche' spiega da solo le
   segnalazioni del tipo "non vedo piu' niente". */
.perm-chip-none {
  color: var(--sip-1xx);
  background: var(--tint-warn);
  border-color: rgba(224, 178, 63, 0.4);
}

/* ------------------------------------------------ tabelle del pannello */

/* Le due tabelle non devono allargare la pagina: se non ci stanno, scorrono
   loro dentro .table-scroll (che e' gia' definito con Impostazioni). */
.admin-users-table,
.admin-nodes-table {
  min-width: 44rem;
}

.admin-users-table .col-perms {
  max-width: 16rem;
}

.admin-nodes-table {
  min-width: 34rem;
}

.admin-nodes-table .col-node {
  font-weight: 600;
}

/* La riga di un nodo da classificare: filo giallo a sinistra. E' l'unica su
   cui c'e' da fare qualcosa, e in una tabella di tre righe si perde in fretta. */
.admin-nodes-table tr.is-unclassified > td:first-child {
  box-shadow: inset 3px 0 0 var(--sip-1xx);
}

/* Pastiglia di STATO dei nodi, e vale solo nella tabella del pannello admin:
   dice «da classificare» / «classificato» / «non nei dati», che è un'altra
   cosa dalla pastiglia di PROVENIENZA delle liste di chiamate.

   Lo scope non è cosmetico. Le due regole si chiamavano tutte e due
   `.node-chip` senza selettore contenitore, quindi questa — che viene dopo —
   vinceva anche nelle liste, e la pastiglia della lista prendeva in silenzio
   imbottitura, corpo e fondo pensati per un'altra cosa. Finché era un dettaglio
   dentro un'altra cella nessuno se n'era accorto; adesso è il contenuto di una
   colonna e la sua larghezza decide quella della colonna. */
.admin-nodes-table .node-chip {
  padding: 0.08rem 0.6rem;
  font-size: 0.7rem;
  color: var(--text-muted);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--pill);
}

.node-chip-ok {
  color: var(--sip-2xx);
  background: var(--tint-ok);
  border-color: rgba(79, 208, 138, 0.35);
}

.node-chip-todo {
  color: var(--sip-1xx);
  background: var(--tint-warn);
  border-color: rgba(224, 178, 63, 0.45);
  cursor: help;
}

/* Classificato ma silenzioso: nessun allarme, e' solo un fatto da sapere. */
.node-chip-gone {
  color: var(--text-dim);
  cursor: help;
}

/* --------------------------------------------------- pagina del 403 */

/* Riusa la card del login: stessa forma, stesso peso visivo. */
.denied-title {
  margin: 0 0 0.6rem;
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--sip-1xx);
}

.denied-text {
  margin: 0 0 0.8rem;
  font-size: 0.78rem;
  line-height: 1.6;
  color: var(--text-muted);
}

.denied-text strong {
  color: var(--text);
}

/* Il "torna a VIPER" e' un link vestito da bottone: deve restare un <a>. */
.btn-link {
  display: block;
  text-align: center;
  text-decoration: none;
}

@media (max-width: 900px) {
  .admin-main {
    padding: 1rem 0.85rem 2rem;
  }
}

@media (max-width: 560px) {
  /* Titolo e sezione si prendono la prima riga, i comandi la seconda: in fila
     non ci starebbero, e la barra scorrerebbe in orizzontale. Ci pensa il
     flex-wrap di .admin-top; qui si stringe solo cio' che resta. */
  .admin-top {
    padding: 0.5rem 0.7rem;
  }

  .admin-title {
    font-size: 0.72rem;
  }

  /* A questa larghezza il menu e' fatto di sole icone: questa riga diventa
     l'unica cosa che dice in che sezione si e', e non puo' andare a capo. */
  .admin-where {
    padding-left: 0.6rem;
    font-size: 0.72rem;
  }

  .admin-main {
    padding: 0.85rem 0.7rem 2rem;
  }

  /* I due gruppi di caselle occupano tutta la riga: affiancati diventerebbero
     due colonne strettissime di etichette a capo. */
  .perm-group {
    flex: 1 1 100%;
  }
}

/* Link vestito da bottone dentro una sezione di Impostazioni (il rimando ad
   Amministrazione). Non e' .btn-link, che e' a larghezza piena nella card del
   login: questo sta in linea, come un bottone qualunque. */
.btn-link-inline {
  display: inline-block;
  text-decoration: none;
}

/* Il rimando ad Amministrazione nella topbar dell'app: e' un link, e non deve
   sembrare un'azione distruttiva accanto a "Esci". */
.admin-entry {
  text-decoration: none;
}

/* =========================== adattamento ai permessi (vedi js/access.js)
   Due elementi che esistono solo per i casi in cui l'utente chiede una cosa e
   ne ottiene un'altra: l'avviso di rotta e il pannello "nessuna vista". */

/* Avviso di rotta: sta fra la barra in alto e la workspace, occupa la riga
   intera e sparisce da solo. Stesso giallo delle altre segnalazioni non
   bloccanti (.notice), perche' non e' un errore -- e' una spiegazione. */
.route-notice {
  flex-shrink: 0;
  padding: 0.5rem 1rem;
  font-size: 0.78rem;
  color: var(--sip-1xx);
  background: var(--tint-warn);
  border-bottom: 1px solid var(--border);
}

/* L'attributo hidden basta qui (il default e' `display: block`), ma la regola
   esplicita evita di doverselo chiedere leggendo. */
.route-notice[hidden] {
  display: none;
}

/* Nessuna vista concessa: un pannello unico, come Impostazioni e Nodi. Gli
   altri escono di scena, altrimenti resterebbero due colonne vuote accanto al
   messaggio. */
.workspace.is-denied {
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
}

.workspace.is-denied > .panel:not(.panel-denied) {
  display: none;
}

/* Il testo del pannello va letto, non scorso: stessa larghezza di riga delle
   sezioni di Impostazioni. */
.panel-denied .state {
  max-width: 44rem;
  margin: 0 auto;
}

/* La sidebar senza voci resta col solo marchio: dice ancora dove si e', e non
   lascia una colonna che sembra un difetto di caricamento. */
.nav[hidden] {
  display: none;
}
