/* Austyn Cloud portal.
   Tokens and component vocabulary are lifted from the MAG-GW-03 gateway UI
   (projects/ESP32-IQRF-GW-V3/docs/UI_GUIDE.md) so the two products read as one.
   Never hard-code a colour here; if a shade is missing, add a token. */

:root {
  --bg:        #101215;
  --bg-2:      #0b0d0f;
  --surface:   #16191d;
  --surface-2: #1d2126;
  --line:      #262b31;
  --line-2:    #333940;
  --ink:       #e9ecef;
  --dim:       #a9b0b8;
  --faint:     #7b828a;

  --surface-raised: #1b2027;   /* cards: one visible step above the page, not a whisper */
  --signal:    #bf0d3e;   /* Pantone 193C. Primary action + selected, nothing else. */
  --signal-2:  #d8244f;
  --ok:        #3ecf8e;   /* online / healthy / saved */
  --amber:     #e0a33a;   /* offline / attention */
  --danger:    #f0665c;   /* destructive / failed */
  --char:      #5c636b;   /* inactive / neutral data */
  --rail:      #08090b;

  --f-display: 'Saira', system-ui, sans-serif;
  --f-sans:    Arial, Helvetica, system-ui, sans-serif;
  --f-mono:    'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;

  --r:    6px;
  --rail-w: 172px;

  /* One height for anything that sits in a row of controls. Without it the
     three elements of a filter bar end up 40px, 35px and 35px tall — not by
     design, but because an input, a select and a <summary> each derive their
     height from their own padding and line-height, and a select's line-height
     is `normal` while everything else is 1.5. Side by side that reads as
     sloppy, and no amount of align-items fixes it: the boxes really are
     different sizes. */
  --control-h: 2.5rem;

  --z-drawer: 40;
  --z-toast:  60;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

:root[data-theme="light"] {
  --bg:        #f4f6f8;
  --bg-2:      #e9edf1;
  --surface:   #ffffff;
  --surface-2: #f4f6f8;
  --line:      #dde2e8;
  --line-2:    #c6cdd6;
  --ink:       #14171a;
  --dim:       #4d545c;
  --faint:     #6e767f;
  --char:      #8b939c;
  --surface-raised: #ffffff;
  /* rail stays dark in both themes */
}

* { box-sizing: border-box; }

/* The browser's own `[hidden] { display: none }` lives in the UA stylesheet and
   loses to ANY class selector that sets `display`. So `.gate { display: grid }`
   quietly defeated `gate.hidden = true`: the app signed in and polled happily
   behind an overlay that never went away, and the user kept staring at the
   sign-in form. The DOM property reads `true` throughout, so it also fools any
   test that asserts on `.hidden` instead of on what is actually painted.
   `.skeleton`, `.rail`, `.save-row` and friends all had the same latent bug.
   One rule, once, so it cannot come back. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--f-sans);
  font-size: 15px;
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
}

.mono { font-family: var(--f-mono); font-variant-numeric: tabular-nums; }

h2 {
  font-family: var(--f-display);
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0;
}
h3 {
  font-family: var(--f-display);
  font-size: 1rem;
  font-weight: 600;
  margin: 0;
}

.hint { color: var(--dim); font-size: 0.875rem; max-width: 68ch; margin: 0.5rem 0; }
.hint b { color: var(--ink); font-weight: 600; }

.fieldset-label {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 0.4rem;
}

/* ── Rail ─────────────────────────────────────────────────────────── */

.rail {
  position: fixed;
  inset: 0 auto 0 0;
  width: var(--rail-w);
  background: var(--rail);
  border-right: 1px solid #14171a;
  display: flex;
  flex-direction: column;
  padding: 1rem 0.6rem;
  gap: 0.15rem;
  z-index: var(--z-drawer);
}

.rail-brand {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: 0.1em;
  color: #f2f4f6;
  padding: 0 0.55rem 1.1rem;
}
.rail-brand span { color: var(--signal); }

.rail-item {
  appearance: none;
  border: 0;
  background: transparent;
  color: #9aa2ab;
  font: inherit;
  text-align: left;
  padding: 0.5rem 0.55rem;
  border-radius: var(--r);
  cursor: pointer;
  transition: background 140ms var(--ease), color 140ms var(--ease);
}
.rail-item:hover { background: #131619; color: #dfe4e9; }
.rail-item.active { background: var(--signal); color: #fff; }
.rail-item:focus-visible { outline: 2px solid var(--signal-2); outline-offset: 2px; }

.rail-foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.55rem;
}

.lang { display: flex; border: 1px solid #1c2025; border-radius: var(--r); overflow: hidden; }
.lang-btn {
  border: 0;
  border-radius: 0;
  background: transparent;
  color: #767d86;
  font-size: 0.7rem;
  font-family: var(--f-mono);
  letter-spacing: 0.04em;
  padding: 0.2rem 0.4rem;
}
.lang-btn:hover:not(.active) { background: #131619; color: #dfe4e9; }
.lang-btn.active { background: #1c2025; color: #e9ecef; }

.drawer-btn {
  display: none;
  position: fixed;
  top: 0.6rem; left: 0.6rem;
  z-index: calc(var(--z-drawer) + 1);
  width: 38px; height: 38px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
  color: var(--ink);
  font-size: 1.1rem;
  cursor: pointer;
}

/* ── Layout ───────────────────────────────────────────────────────── */

main {
  margin-left: var(--rail-w);
  padding: 1.6rem 1.6rem 4rem;
}
main > .tab { max-width: 1240px; margin-inline: auto; }

.tab { display: none; }
.tab.active { display: block; }

.head-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 0.9rem;
}

.submeta { color: var(--faint); font-size: 0.9rem; margin: -0.4rem 0 1.4rem; line-height: 1.7; }
.submeta .mk { color: var(--faint); }
.submeta .mv { color: var(--ink); }
.submeta .msep { color: var(--line-2); }

.who-row { display: flex; align-items: center; gap: 0.7rem; margin: 0; }
.who-user { font-size: 1.05rem; color: var(--ink); }

.back {
  appearance: none;
  border: 0;
  background: none;
  color: var(--dim);
  font: inherit;
  cursor: pointer;
  padding: 0 0 0.8rem;
}
.back:hover { color: var(--ink); }

.card {
  background: var(--surface-raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  padding: 1.15rem 1.35rem 1.35rem;
  margin-bottom: 1.25rem;
}
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.6rem 1rem;
  padding-bottom: 0.75rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--line);
}
.card-head h3 { font-size: 1.02rem; color: var(--ink); }

/* Collapsible settings: native <details>, no display hacks. The summary IS
   the card-head — closed, it reads as a title; open, a rule underneath does
   the same job .card-head's border does elsewhere. */
.details-card { padding: 0; }
.details-card > summary {
  list-style: none;
  cursor: pointer;
  padding: 1rem 1.1rem;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--f-display);
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}
.details-card > summary::-webkit-details-marker { display: none; }
.details-card > summary::before {
  content: '▸';
  color: var(--faint);
  display: inline-block;
  transition: transform 140ms var(--ease);
}
.details-card[open] > summary { border-bottom: 1px solid var(--line); }
.details-card[open] > summary::before { transform: rotate(90deg); }
.details-card .details-body { padding: 0.9rem 1.1rem 1.1rem; }

/* A dashed rule, not a border around the button: enough to separate "delete
   this gateway" from the rest of Access without a second card. */
.danger-zone { margin-top: 1.3rem; padding-top: 1rem; border-top: 1px dashed var(--line-2); }

.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0.9rem; }

label { display: block; color: var(--dim); font-size: 0.8rem; }
.spaced { margin-top: 1.2rem; }
input.narrow { max-width: 260px; }

/* Allow-list by exclusion, not by listing types. The earlier selector listed
   text/search/untyped and silently missed type=password, so the login field
   rendered unstyled and half-width next to a styled username field. */
input:not([type=checkbox]):not([type=radio]):not([type=file]) {
  display: block;
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.5rem 0.6rem;
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  color: var(--ink);
  font: inherit;
}
input:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; border-color: var(--signal); }
input::placeholder { color: var(--faint); }

/* ── Buttons ──────────────────────────────────────────────────────── */

button {
  appearance: none;
  font: inherit;
  padding: 0.45rem 0.9rem;
  border-radius: var(--r);
  border: 1px solid var(--line-2);
  background: var(--surface-2);
  color: var(--ink);
  cursor: pointer;
  transition: background 140ms var(--ease), border-color 140ms var(--ease), opacity 140ms var(--ease);
}
button:hover:not(:disabled) { background: var(--line); }
button:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
button:disabled { opacity: 0.45; cursor: not-allowed; }

.save-btn { background: var(--signal); border-color: var(--signal); color: #fff; font-weight: 600; }
.save-btn:hover:not(:disabled) { background: var(--signal-2); border-color: var(--signal-2); }

.danger { color: var(--danger); border-color: var(--danger); background: transparent; }
.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 14%, transparent); }

.small { padding: 0.25rem 0.55rem; font-size: 0.8rem; }
.ghost { background: transparent; border-color: transparent; color: var(--dim); }
.ghost:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }

/* A number or a name that happens to be a button: no chrome of its own, just
   the cursor and a colour shift, so a stat and a click target can be the
   same character on the page instead of a number plus an icon beside it. */
.linklike {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.linklike:hover { color: var(--signal-2); text-decoration: underline; }
.linklike:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }

.save-row { display: flex; align-items: center; gap: 0.75rem; margin-top: 1rem; }
.save-status { font-size: 0.85rem; color: var(--dim); }
.save-status.ok   { color: var(--ok); }
.save-status.err  { color: var(--danger); }
/* Not a failure: the answer arrived, part of it is just older than the rest. */
.save-status.warn { color: var(--amber); }

/* ── Status vocabulary ────────────────────────────────────────────── */

.dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--char);
  display: inline-block;
  flex: none;
  transition: background 220ms var(--ease);
}
.dot.on   { background: var(--ok); }
.dot.off  { background: var(--amber); }
.dot.none { background: var(--char); }

.state { display: inline-flex; align-items: center; gap: 0.5rem; white-space: nowrap; }

.badge {
  font-size: 0.75rem;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  color: var(--dim);
}
.badge.ok    { color: var(--ok);     border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.badge.warn  { color: var(--amber);  border-color: color-mix(in srgb, var(--amber) 45%, transparent); }
.badge.muted { color: var(--faint); }
.badge.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
/* A revoked release badge: same muted grey as any other retired state, plus a
   strike so "revoked" reads even to someone not stopping to parse the word. */
.badge.strike { text-decoration: line-through; }

.counts { color: var(--dim); font-size: 0.875rem; font-family: var(--f-mono); font-variant-numeric: tabular-nums; }
.counts b, .counts .linklike { color: var(--ink); font-weight: 600; }
.counts .linklike:hover { color: var(--signal-2); }

/* Unobtrusive: it rides along with the state word, not a second badge. */
.reporting { color: var(--faint); font-size: 0.78rem; }

/* ── Table ────────────────────────────────────────────────────────── */

.net-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.net-table thead th {
  text-align: left;
  font-family: var(--f-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 400;
  padding: 0 0.7rem 0.5rem;
  border-bottom: 1px solid var(--line);
}
.net-table tbody tr {
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background 140ms var(--ease);
}
.net-table tbody tr:hover { background: var(--surface); }
.net-table tbody tr:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }
.net-table td { padding: 0.6rem 0.7rem; }
.net-table td.c-id, .net-table td.c-fw { font-family: var(--f-mono); font-variant-numeric: tabular-nums; color: var(--dim); }
.net-table tr.disabled td { color: var(--faint); }
/* A gateway nobody has named yet: shown by its label id, in the data voice. */
.unnamed { font-family: var(--f-mono); color: var(--faint); }

/* Newly imported rows announce themselves once, then settle. */
@keyframes flash { from { background: color-mix(in srgb, var(--signal) 22%, transparent); } }
.net-table tbody tr.fresh { animation: flash 900ms var(--ease); }

/* ── Empty & loading ──────────────────────────────────────────────── */

.empty {
  border: 1px dashed var(--line-2);
  border-radius: var(--r);
  padding: 2.2rem 1.6rem;
  text-align: left;
  max-width: 62ch;
}
.empty h3 { margin-bottom: 0.6rem; }

.skeleton { display: grid; gap: 1px; }
.sk-row {
  height: 41px;
  background: linear-gradient(90deg, var(--surface) 25%, var(--surface-2) 50%, var(--surface) 75%);
  background-size: 300% 100%;
  animation: sweep 1.4s linear infinite;
  border-bottom: 1px solid var(--line);
}
@keyframes sweep { to { background-position: -300% 0; } }

/* ── Overview ─────────────────────────────────────────────────────── */

.ov-grid { display: block; }
@media (min-width: 900px) {
  .ov-grid {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: 0 1.25rem;
    align-items: start;
  }
  .ov-grid > .ov-left, .ov-grid > .ov-right { min-width: 0; }
}

/* Riadok logu portálu. Čas vľavo v pevnej šírke, text vedľa a zalomiteľný —
   log má dlhé riadky (chyby s cestami a JSON) a orezať ich by znamenalo
   skrývať práve tú časť, kvôli ktorej sa doň človek pozerá. */
#logBox { max-height: 68vh; }
.log .row.logline {
  grid-template-columns: 96px 1fr;
  align-items: start;
  padding: 0.1rem 0;
}
.log .row.logline .t  { white-space: nowrap; }
.log .row.logline .pl { white-space: pre-wrap; word-break: break-word; }

/* Verzia portálu v pätke lišty. Zámerne najtichšia vec na obrazovke — je to
   údaj, ktorý hľadáš raz za mesiac, a keď ho hľadáš, vieš kde je. */
.rail-ver { margin-left: auto; color: var(--faint); font-size: 0.72rem; }

/* Command palette. Sits above everything, including the drawer and the toast,
   because it is modal by intent: while it is open it IS the interface. */
.palette {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-toast) + 10);
  display: flex;
  justify-content: center;
  /* Not centred vertically: a list that grows downward from a fixed point is
     calmer than one that shifts up the screen as it fills. */
  padding: 12vh 1rem 1rem;
  background: rgba(0, 0, 0, 0.55);
}
.palette[hidden] { display: none; }
.palette-box {
  width: 100%;
  max-width: 560px;
  height: max-content;
  background: var(--surface-raised);
  border: 1px solid var(--line-2);
  border-radius: 10px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
/* The id beats the global input rule's :not() chain without !important. */
#palInput {
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  padding: 0.9rem 1rem;
  font-size: 1rem;
  color: var(--ink);
}
#palInput:focus-visible { outline: none; border-bottom-color: var(--signal); }
.palette-list { max-height: 46vh; overflow-y: auto; padding: 0.35rem; }
.palette-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.45rem 0.6rem;
  border-radius: var(--r);
  cursor: pointer;
}
.palette-row.on { background: var(--surface-2); }
.palette-label { color: var(--ink); }
.palette-sub { color: var(--faint); font-size: 0.8rem; margin-left: auto; }
.palette-kind {
  color: var(--faint);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.palette-row .dot { align-self: center; }
.palette-empty { color: var(--faint); padding: 0.8rem 0.6rem; }
.palette-foot {
  border-top: 1px solid var(--line);
  padding: 0.45rem 1rem;
  color: var(--faint);
  font-size: 0.75rem;
}

/* The shared log box is 55vh, which on the Overview stretched the right column
   to nearly twice the left one and left a tall empty gutter beside it — the
   page read as sparse because one card was tall, not because it was empty.
   The list still scrolls; it just stops setting the height of the page. */
#ovEvents { max-height: 24rem; }

/* Fleet availability: ONE band, 24 h wide, filled from the bottom by the share
   of gateways online at that moment. Its height does not depend on fleet size —
   a row per gateway reads well at four and is a wall at four hundred.
   The shape carries the diagnosis: a shallow dip is one unit having a bad day,
   a drop to the floor is the broker or the cloud. */
.ov-avail { margin-top: 1.25rem; }
.band-head { display: flex; align-items: baseline; gap: 0.6rem; margin-bottom: 0.6rem; }
.band-pct { font-size: 1.6rem; font-variant-numeric: tabular-nums; }
.band-pct.bad { color: var(--amber); }
.band-bar {
  display: flex;
  align-items: flex-end;
  height: 46px;
  border-radius: var(--r);
  overflow: hidden;
  /* The bar's own background IS the offline half; green grows up into it. */
  background: color-mix(in oklab, var(--amber) 55%, var(--surface-2));
}
/* A 30-second blip is 0.03 % of the window — sub-pixel, and invisible is the
   one thing an outage must never be. The floor costs a little accuracy on the
   neighbouring segment and buys the only reading that matters. */
.band-bar > .seg { display: flex; align-items: flex-end; height: 100%; min-width: 2px; }
.band-bar > .seg > i { display: block; width: 100%; background: var(--ok); }
.avail-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 0.35rem;
  color: var(--faint);
  font-size: 0.8rem;
}

/* Named only when naming helps: the few that actually lost time, then a count.
   Capped width — this is a three-item list, and stretching it across a 1400 px
   card puts the percentage a hand's width away from the name it belongs to. */
.worst { margin-top: 1rem; max-width: 40rem; }
.worst-h { margin: 0 0 0.35rem; }
.worst-row {
  display: grid;
  grid-template-columns: minmax(6rem, 14rem) 1fr auto;
  gap: 0.75rem;
  align-items: baseline;
  padding: 0.12rem 0;
  font-size: 0.9rem;
}
/* A <button> centres its own text, so a stretched grid cell floated the name
   into the middle of its column. Shrink to content instead. */
.worst-row .avail-gw { justify-self: start; }
.worst-dur { color: var(--amber); }
.worst-pct { color: var(--dim); font-variant-numeric: tabular-nums; }
.worst-more { margin: 0.35rem 0 0; }

.ov-numbers { display: flex; flex-wrap: wrap; gap: 1.5rem; }
.ov-num {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.ov-num.online b  { color: var(--ok); }
.ov-num.offline b { color: var(--amber); }
.ov-num b {
  font-family: var(--f-display);
  font-size: 2.6rem;
  font-weight: 600;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.ov-num span { color: var(--dim); font-size: 0.8rem; }
.ov-num:hover b { color: var(--signal-2); }
.ov-num:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; border-radius: var(--r); }

.ov-fw-row { display: flex; align-items: center; justify-content: space-between; padding: 0.3rem 0; }
.ov-fw-row + .ov-fw-row { border-top: 1px solid var(--line); }
.ov-fw-more { display: block; margin-top: 0.6rem; font-size: 0.85rem; }

/* ── Detail bits ──────────────────────────────────────────────────── */

.kv { display: grid; grid-template-columns: minmax(140px, max-content) 1fr; gap: 0.45rem 1.2rem; margin: 0; }
.kv dt { color: var(--faint); font-size: 0.85rem; }
.kv dd { margin: 0; color: var(--ink); font-size: 0.9rem; }
/* Verdikt updatu povedaný vetou — nad surovými poľami, nie namiesto nich. */
.ota-say { margin: 0.35rem 0 0.8rem; max-width: 62ch; color: var(--ink); font-size: 0.95rem; line-height: 1.45; text-wrap: pretty; }

/* Credentials someone is about to paste into atomng: lifted off the page on
   its own tinted panel so the eye lands there first, values in mono because
   they are typed verbatim, not read as prose. */
.conn-panel {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 0.8rem 1rem;
  display: grid;
  gap: 0.55rem;
}
.conn-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem 0.8rem; }
.conn-k {
  flex: 0 0 72px;
  color: var(--faint);
  font-size: 0.78rem;
  font-family: var(--f-mono);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.conn-v { flex: 1 1 auto; min-width: 0; color: var(--ink); font-size: 0.9rem; word-break: break-all; }

/* A copy button never grows the row: fixed width so "Copy" -> "✓" doesn't
   nudge the value next to it. */
.copy-btn { flex: none; min-width: 4.4rem; padding: 0.2rem 0.5rem; font-size: 0.78rem; text-align: center; }
.copy-btn.copied { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }

/* 24 h uptime: green by default, amber segments where history says otherwise.
   Char grey means "no data at all" — a gateway that has never been seen, not
   one that is currently down. */
.uptime-row { display: flex; align-items: flex-end; gap: 1.1rem; }
.uptime-wrap { flex: 1; min-width: 0; }
.uptime-scale {
  display: flex;
  justify-content: space-between;
  font-family: var(--f-mono);
  font-size: 0.7rem;
  color: var(--faint);
  margin-bottom: 0.35rem;
}
.uptime-bar {
  position: relative;
  height: 14px;
  border-radius: var(--r);
  background: var(--ok);
  overflow: hidden;
}
.uptime-bar.never { background: var(--char); }
.uptime-seg { position: absolute; top: 0; bottom: 0; background: var(--amber); }
.uptime-summary {
  flex: none;
  white-space: nowrap;
  color: var(--ink);
  font-size: 0.95rem;
}

.sample {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 0.7rem 0.8rem;
  font-size: 0.8rem;
  color: var(--dim);
  overflow-x: auto;
  margin: 0 0 0.8rem;
}

.file { display: inline-block; margin-top: 0.4rem; }
.file input { display: none; }
.file span {
  display: inline-block;
  padding: 0.45rem 0.9rem;
  border: 1px dashed var(--line-2);
  border-radius: var(--r);
  color: var(--dim);
  cursor: pointer;
}
.file:hover span { border-color: var(--signal); color: var(--ink); }

.result {
  margin-top: 0.9rem;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--bg-2);
  font-size: 0.85rem;
  color: var(--dim);
}
.result b { color: var(--ink); font-weight: 600; }
.result ul { margin: 0.5rem 0 0; padding-left: 1.1rem; }
.result li { color: var(--amber); }

/* ── Sign-in gate ─────────────────────────────────────────────────── */

.gate {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  background: var(--bg);
  display: grid;
  place-items: center;
  padding: 1.2rem;
}
.gate-card {
  width: 100%;
  max-width: 400px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 2.2rem 2rem 1.6rem;
}
.gate-brand { padding: 0 0 0.35rem; font-size: 1.05rem; }
.gate-card h2 {
  font-size: 0.78rem;
  letter-spacing: 0.12em;
  color: var(--faint);
  font-weight: 500;
  margin-bottom: 1.8rem;
}
.gate-card label { margin-bottom: 1rem; }
/* The other labels in this card stack their input underneath; this one is a
   checkbox and reads as one line. Sized explicitly for the same reason as the
   column picker: a bare checkbox inherits whatever the surrounding form layout
   does to inputs, and here that would stretch it across the card. */
.gate-card label.remember {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: -0.3rem 0 0;
  font-size: 0.85rem;
  color: var(--dim);
  cursor: pointer;
}
.gate-card label.remember input[type=checkbox] {
  flex: 0 0 auto;
  width: 0.95rem;
  height: 0.95rem;
  margin: 0;
  accent-color: var(--signal);
  cursor: pointer;
}
.gate-card .save-row {
  margin-top: 1.5rem;
  flex-direction: column;
  align-items: stretch;
  gap: 0.6rem;
}
.gate-card .save-btn { width: 100%; padding: 0.6rem 1rem; }
.gate-card .save-status { min-height: 1.2em; }
/* Language is a preference, not a step in signing in — it sits quietly at the
   bottom edge rather than competing with the form. */
.gate-lang {
  margin-top: 1.8rem;
  padding-top: 1.3rem;
  border-top: 1px solid var(--line);
  width: max-content;
}

.rail-who {
  margin-top: auto;
  padding: 0 0.55rem 0.6rem;
  border-top: 1px solid #14171a;
  padding-top: 0.7rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.2rem;
}
.rail-who .mono { font-size: 0.72rem; color: #767d86; }
.rail-foot { margin-top: 0; }

.c-act { width: 1%; white-space: nowrap; }
.net-table td.c-act { text-align: right; }

/* ── Filters, chips, checklists ───────────────────────────────────── */

.filters {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.9rem;
}

/* Everything in the row is the same height and the same type size, stated
   rather than inherited from three different sources. `align-items: stretch`
   would have equalised the outer boxes and still left the Columns button
   short, because that button is a <summary> INSIDE the flex item, and
   stretching a <details> does nothing for what it contains. */
.filters input,
.filters select,
.filters .colpick > summary {
  height: var(--control-h);
  margin-top: 0;
  font-size: inherit;
}
.filters input { flex: 1 1 220px; }
.filters select { flex: 0 1 190px; }
/* The height above owns the vertical box now, so the padding only sets the
   side inset and the label is centred instead of sitting wherever the
   line-height happened to put it. */
.filters .colpick > summary {
  display: inline-flex;
  align-items: center;
  padding-block: 0;
}

/* "12 z 200": only present while a filter narrows the list, so it sits quiet
   and mono like every other count rather than fighting the filter row. */
#filteredCount { margin: -0.4rem 0 0.9rem; }

/* Column chooser. A popover rather than an inline block: it is opened rarely,
   and pushing the table down every time somebody peeks at it would be worse
   than the problem it solves. */
.colpick { position: relative; flex: 0 0 auto; }
.colpick > summary {
  list-style: none;
  cursor: pointer;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--surface-2);
  color: var(--dim);
  font-size: 0.85rem;
  white-space: nowrap;
}
.colpick > summary::-webkit-details-marker { display: none; }
.colpick > summary::after { content: ' ▾'; color: var(--faint); }
.colpick[open] > summary { color: var(--ink); }
.colpick-body {
  position: absolute;
  top: calc(100% + 0.35rem);
  right: 0;
  z-index: var(--z-drawer);
  padding: 0.35rem;
  background: var(--surface-raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
.colpick-row {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.3rem 0.5rem;
  border-radius: var(--r);
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--ink);
  margin: 0;
  /* A column name is two words at most; letting it wrap made every row a
     different height and the whole menu look broken. The panel is as wide as
     its longest label and no wider — there is nothing here to save space for. */
  white-space: nowrap;
}
.colpick-row:hover { background: var(--surface-2); }
/* `.filters input { flex: 1 1 220px }` also catches these — the picker lives
   inside the filter row — and stretched every checkbox into a big square.
   Reset the flexing, not just the width, and pin the size so the boxes line up
   with the text instead of towering over it. */
.colpick-row input[type=checkbox] {
  flex: 0 0 auto;
  width: 0.95rem;
  height: 0.95rem;
  margin: 0;
  accent-color: var(--signal);
  cursor: pointer;
}

/* Optional columns: nowrap so a long value widens its own column instead of
   wrapping into two lines and making every row in the table taller. */
.net-table .c-host, .net-table .c-mid,
.net-table .c-board, .net-table .c-seen { white-space: nowrap; }

/* Wide tables scroll inside their own box. Chasing per-column max-widths stopped
   being viable the moment columns became user-selectable — and the page body
   must never scroll sideways, which is exactly what made the phone layout look
   broken twice already. Fix the container once; columns can then be whatever
   width they honestly need. */
#fleetWrap, .table-scroll { overflow-x: auto; }

/* Bulk action bar: sticky so it survives scrolling a long filtered list, but
   otherwise dressed like the rest of the toolbar — no signal-red fill, that
   colour stays reserved for the one true primary action on each screen. */
.bulk-bar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  background: var(--surface-raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  padding: 0.55rem 0.85rem;
  margin-bottom: 0.9rem;
}
.bulk-count {
  font-family: var(--f-mono);
  font-size: 0.85rem;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  margin-right: 0.2rem;
}
.bulk-bar select { width: auto; min-width: 150px; margin: 0; flex: 0 1 auto; }
.bulk-bar .save-status { margin-left: auto; }

/* On a phone the bar wraps, but min-width on the select plus nowrap buttons
   still pushed the PAGE to 430px at a 375px viewport. Let both shrink: a
   toolbar that wraps is fine, a page that scrolls sideways is not. */
@media (max-width: 560px) {
  .bulk-bar select { min-width: 0; flex: 1 1 100%; }
  .bulk-bar button { flex: 1 1 auto; }
  .bulk-bar .save-status { margin-left: 0; flex: 1 1 100%; }
}

/* Checkbox column: just wide enough for the box, never pushes the columns
   that carry the actual information a person scans the table for. */
.net-table th.c-check, .net-table td.c-check { width: 1%; padding-right: 0; }
.net-table td.c-check input, .net-table th.c-check input { width: auto; margin: 0; }

.chip {
  display: inline-block;
  font-size: 0.72rem;
  padding: 0.08rem 0.42rem;
  margin-right: 0.25rem;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  color: var(--dim);
  white-space: nowrap;
}

.net-table td.c-group { white-space: nowrap; }
.net-table tr.norow td { color: var(--faint); padding: 1.2rem 0.7rem; }
.net-table tr.norow:hover { background: none; }
.net-table tr.norow { cursor: default; }

/* ── Releases ─────────────────────────────────────────────────────── */

.net-table th.c-mpt, .net-table td.c-mpt { width: 1%; white-space: nowrap; }
.net-table td.c-date, .net-table td.c-count {
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
  color: var(--dim);
}

.net-table tr.rel-row.open { background: var(--surface); }
.net-table tr.rel-detail { cursor: default; }
.net-table tr.rel-detail:hover { background: none; }
.net-table tr.rel-detail td { background: var(--bg-2); padding: 1rem 1.2rem; }
.rel-detail-inner .net-table { margin-top: 0.4rem; }
/* A hash cell carries both the truncated value and its copy button on one
   line — the button never needs its own column, and the row stays as tall
   as any other. */
.rel-detail-inner td.mono .hash-copy { margin-left: 0.5rem; vertical-align: middle; }

/* Many short toggles: columns, not a tall stack. */
.checklist {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 0.35rem 0.9rem;
}
.checklist label { display: flex; align-items: center; gap: 0.45rem; margin: 0; cursor: pointer; }
.checklist label span { color: var(--ink); font-size: 0.83rem; }
.checklist input { width: auto; margin: 0; }

/* ── Traffic log ──────────────────────────────────────────────────── */

textarea {
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.5rem 0.6rem;
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  color: var(--ink);
  font-family: var(--f-mono);
  font-size: 0.82rem;
  resize: vertical;
}
textarea:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; border-color: var(--signal); }

select {
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.45rem 0.5rem;
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  color: var(--ink);
  font: inherit;
}

.log {
  font-family: var(--f-mono);
  font-size: 0.84rem;
  line-height: 1.85;
  max-height: 55vh;
  overflow: auto;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 0.6rem 0.7rem;
}
.log .row { display: grid; grid-template-columns: 62px 14px 1fr; gap: 0.55rem; padding: 0.18rem 0; }
.log .t   { color: var(--faint); }
.log .d   { text-align: center; }
.log .d.in  { color: var(--ok); }
.log .d.out { color: var(--signal-2); }
.log .tp  { color: var(--dim); }
.log .pl  { color: var(--ink); word-break: break-all; }
.log .empty-line { color: var(--faint); padding: 0.4rem 0; }
/* History rows carry an absolute timestamp AND the delta, so they need the room
   for both. The absolute value is the fact and reads brighter; the delta is the
   convenience and sits behind it. A relative time alone forces arithmetic
   against a clock that moved while you were reading.

   The width is set by the SLOVAK worst case, "13. 08. 13:07 · 24 h 23 min" —
   the English format is shorter, and sizing to it wrapped every second row onto
   two lines the moment the language switched. */
.log .row.hist { grid-template-columns: 235px 14px 1fr; padding: 0.18rem 0; }
.log .row.hist .t { color: var(--dim); white-space: nowrap; }
.log .row.hist .t-rel { color: var(--faint); margin-left: 0.5rem; }
.log .row.hist .d.in  { color: var(--ok); }
.log .row.hist .d.out { color: var(--amber); }

/* Audit rows: no direction arrow, so the middle column carries the actor
   instead of an icon. gwId-shaped targets are one long unbroken token, so they
   need permission to break rather than pushing the row wide. */
.log .row.audit { grid-template-columns: 88px 78px 1fr; }
.audit-user   { color: var(--dim); }
.audit-target { color: var(--dim); margin-left: 0.4rem; word-break: break-all; }
.audit-detail { color: var(--faint); margin-left: 0.4rem; }

/* Traffic log toolbar: a text filter, the gateway select, and pause share one
   row, wrapping under the title on narrow screens instead of forcing scroll. */
.log-tools { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
/* id, not a class: the global input rule's :not() chain outweighs an ordinary
   class selector, so a plain .log-tools override would silently lose. */
#tSearch { width: 190px; margin-top: 0; }
.ghost.active { background: var(--surface-2); color: var(--ink); border-color: var(--line-2); }

/* The connection-history card keeps its raw log, just shorter: this is a
   detail people check occasionally, not the reason they opened the page. */
.log.hist-log { max-height: 220px; }

/* ── IQRF network ─────────────────────────────────────────────────── */

/* Addresses, in address order. Deliberately NOT a graph: BondedDevices and
   DiscoveredDevices give us membership, not who routes through whom, and drawing
   edges we cannot know would be a picture that lies. */
.mesh-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 0.5rem;
  margin-top: 1rem;
}

/* Big enough to carry its own answer: address, then up to two readings,
   without a click. A node with nothing to show is still this size — the grid
   stays a grid instead of some tiles being small and some large. */
.mesh-cell {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  min-width: 92px;
  min-height: 72px;
  padding: 0.5rem 0.4rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--bg-2);
  color: var(--dim);
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: border-color 120ms var(--ease), color 120ms var(--ease);
}
.mesh-cell:hover { border-color: var(--line-2); color: var(--ink); }
.mesh-cell.picked { border-color: var(--signal); color: var(--ink); }

/* State is carried by the left edge weight AND the dot, never by colour alone. */
.mesh-cell.ok      { color: var(--ink);   border-color: color-mix(in srgb, var(--ok) 50%, var(--line-2)); }
.mesh-cell.partial { color: var(--ink);   border-color: color-mix(in srgb, var(--amber) 50%, var(--line-2)); }
.mesh-cell.silent  { color: var(--faint); border-style: dashed; }
.mesh-cell.data    { color: var(--ink);   border-color: color-mix(in srgb, var(--signal) 50%, var(--line-2)); }

.mesh-addr { font-size: 1.05rem; font-weight: 600; }
.mesh-word { font-size: 0.68rem; color: var(--faint); font-family: var(--f-sans); }
.mesh-val { font-size: 0.72rem; color: var(--dim); }
.mesh-cell.ok .mesh-val, .mesh-cell.partial .mesh-val, .mesh-cell.data .mesh-val { color: var(--ink); }
.mesh-more { color: var(--faint) !important; }

.mesh-dot {
  position: absolute;
  top: 6px; right: 6px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--ok);
}
.mesh-cell.partial .mesh-dot { background: var(--amber); }
.mesh-cell.data .mesh-dot    { background: var(--signal-2); }

.mesh-node {
  margin-top: 1rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--bg-2);
}
.mesh-node h4 { margin: 0 0 0.2rem; font-family: var(--f-display); font-size: 1rem; color: var(--ink); }
.mesh-node .hint { margin: 0 0 0.7rem; }
.mesh-node .muted { color: var(--faint); font-size: 0.8rem; }

/* Wide screens: network left, history right. One long column left half the
   viewport empty and forced a scroll past the network to reach the history. */
@media (min-width: 1200px) {
  .detail-cols {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    gap: 0 1.25rem;
    align-items: start;
  }
  .detail-cols .hist-log { max-height: 420px; }
}

.log .row:hover { background: color-mix(in srgb, var(--ink) 5%, transparent); }

/* ── Deployments ──────────────────────────────────────────────────── */

/* Generic "quiet label" utility: same token the meta line and fieldset
   labels already use, now available on any inline span without inventing
   a one-off class per usage site. */
.faint { color: var(--faint); }

/* success/total as a number plus a sliver of colour — the number is the
   fact, the bar is just a glance-fast summary of the same fact. */
.net-table td.dep-progress { display: flex; align-items: center; gap: 0.6rem; white-space: nowrap; }
.dep-track {
  display: inline-block;
  width: 80px; height: 6px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
  vertical-align: middle;
}
.dep-fill { display: block; height: 100%; background: var(--ok); }

/* Failed/rejected rows read by their own text colour, never a side stripe. */
.dep-failed { color: var(--danger); }
.dep-rejected { color: var(--amber); }

.dep-stats { margin-bottom: 1.1rem; }
.dep-next { margin-top: 0.2rem; }

/* Gateway picker for a new deployment: the same search+checklist idea as the
   fleet table's bulk selection, just scoped to this form instead of the
   whole page — it has to stay usable with a couple hundred gateways in it. */
.dp-gwlist {
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 0.6rem 0.7rem;
  margin-top: 0.4rem;
}

/* ── Responsive: structural, not fluid type ───────────────────────── */

@media (max-width: 860px) {
  .rail { transform: translateX(-100%); transition: transform 200ms var(--ease); }
  .rail.open { transform: none; }
  .drawer-btn { display: block; }
  main { margin-left: 0; padding: 3.6rem 1rem 3rem; }
  /* c-group too: a long group chip was the one thing still pushing the fleet
     table past 375px. Groups are a detail-page concern on a phone anyway. */
  /* c-id as well, now that the checkbox column exists: 16 hex characters in a
     monospace font is 148px that pushed the whole page to 430px at a 375px
     viewport. A phone list wants state and name; the gwId is one tap away in
     the detail, and an unnamed gateway still shows its GW-XXXXXX short id. */
  .net-table .c-site, .net-table .c-fw, .net-table .c-group,
  .net-table .c-id { display: none; }
  /* The other optional columns are desk-work data — a phone has no room and
     they are all one tap away in the detail. */
  .net-table .c-mid, .net-table .c-board, .net-table .c-seen { display: none; }
  /* Hostname stays — it is how a box is identified on site. It is allowed to be
     as wide as it needs; the wrapper scrolls (see .table-scroll). */
  /* Releases table: version + channel badge are the two things worth a glance
     on a phone. MPT, date and artifact count are one tap away in the row. */
  .net-table .c-mpt, .net-table .c-date, .net-table .c-count { display: none; }
  .ov-grid { grid-template-columns: 1fr; }
}

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

/* Update state in a fleet row.
   
   Four looks, and the differences are deliberate. `busy` and `avail` are quiet
   because they are ordinary; `bad` is loud because a rollback is the one thing
   an operator must not scroll past; `stale` is dimmed because it reports what
   was last heard, not what is happening — a retained "downloading 43 %" that
   looked live would turn a finished rollout into a stuck one. */
.ota-badge {
  display: inline-block;
  margin-left: 0.45rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--r);
  font-size: 0.78rem;
  line-height: 1.5;
  white-space: nowrap;
  vertical-align: baseline;
}
.ota-badge.busy  { background: color-mix(in oklab, var(--signal) 18%, transparent); color: var(--signal); }
.ota-badge.avail { background: var(--surface-2); color: var(--dim); border: 1px solid var(--line-2); }
.ota-badge.stale { background: transparent; color: var(--faint); font-style: italic; }
.ota-badge.bad {
  background: color-mix(in oklab, var(--danger) 22%, transparent);
  color: var(--danger);
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* The "since when" next to a gateway's state.
   
   Quiet, because the state word is the headline and this is its footnote — but
   present, because a bare "4 h 4 min" makes the reader do arithmetic against a
   clock that moved while they were reading. Wraps under the word on a narrow
   screen instead of stretching the column. */
.state-when {
  display: inline-block;
  margin-left: 0.45rem;
  color: var(--faint);
  font-size: 0.8rem;
  white-space: nowrap;
}
@media (max-width: 860px) {
  .state-when { display: block; margin-left: 0; }
}

/* Gateways waiting to be let in.
   
   Deliberately a card above the fleet rather than a row inside it: these are
   not gateways yet, and mixing them into the list would blur the one line that
   matters — nothing here has been let anywhere. Gone entirely when the queue is
   empty, so an ordinary day shows an ordinary screen. */
/* Čo spraví ktoré tlačidlo. Odrážky, nie odstavec: sú to dve možnosti vedľa
   seba a čítajú sa porovnaním, nie odzadu dopredu. */
/* Sieť a IP: úzke stĺpce, ktoré sa nemajú lámať — "mesh root" je jedna
   informácia, nie dve slová pod sebou. */
.c-net { white-space: nowrap; }
.c-ip  { white-space: nowrap; }

.pending-what { margin: 0.5rem 0 0; padding-left: 1.1rem; max-width: 70ch; }
.pending-what li { margin-bottom: 0.2rem; }
.pending-what strong { color: var(--ink); font-weight: 600; }
/* NIE .pending-when - to meno uz patri casovej znacke v riadku nizsie
   (--faint, mensie pismo, nowrap) a kedze je v subore nizsie, prebilo by toto.
   Bezpecnostna veta by tak zdedila styl casovej peciatky vratane nowrap, cize
   by bola najmenej citatelna vec na karte - jedina, ktora brani zlemu kliknutiu. */
.pending-note { margin-top: 0.7rem; max-width: 70ch; }

/* Čo o tomto gwId vieme z vlastného registra.
   Známa a offline je fakt — stlmene. Známa a PRÁVE PRIPOJENÁ je jediný riadok
   na tejto karte, ktorý má zastaviť ruku nad tlačidlom, tak vyzerá inak než
   zvyšok. Nič sa tým neblokuje: rozhoduje človek. */
.pending-known { color: var(--dim); font-size: 0.85rem; }
.pending-known.live { color: var(--danger); font-weight: 600; }
.pending-row.pending-live {
  border-left: 0;
  background: color-mix(in oklab, var(--danger) 8%, transparent);
}

.pending-card { border-color: color-mix(in oklab, var(--signal) 35%, var(--line-2)); }
.pending-row {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  flex-wrap: wrap;
  padding: 0.6rem 0;
  border-top: 1px solid var(--line);
}
.pending-row:first-child { border-top: 0; }
.pending-who { flex: 1 1 260px; display: flex; flex-direction: column; gap: 0.15rem; }
.pending-ctx { color: var(--dim); font-size: 0.85rem; }
/* API tokeny: rozsahy pod sebou (popis je dlhsi nez nazov), odhaleny token
   v ramceku, ktory sa neda prehliadnut — je vidiet len raz. */
/* Staticky stitok, nie tlacidlo: .chip ma inde cursor:pointer. */
.scope-tag { display: inline-block; font-family: var(--f-mono); font-size: 0.78rem; padding: 0.08rem 0.45rem;
  margin-right: 0.25rem; border-radius: 999px; border: 1px solid var(--line-2); color: var(--dim); white-space: nowrap; }
.token-scopes { display: grid; gap: 0.5rem; margin: 0.9rem 0 0.4rem; }
.token-scope { display: flex; gap: 0.6rem; align-items: flex-start; font-size: 0.9rem; line-height: 1.4; }
.token-scope input { margin-top: 0.2rem; flex: 0 0 auto; }
.token-reveal { margin-top: 0.9rem; padding: 0.8rem 0.9rem; border: 1px solid var(--ok); border-radius: 6px; }
.token-reveal p { margin: 0 0 0.6rem; font-size: 0.9rem; max-width: 70ch; }
.token-reveal-row { display: flex; gap: 0.6rem; align-items: center; }
.token-reveal-row input { flex: 1 1 auto; min-width: 0; }
/* MID krizom proti vyrobnemu zaznamu: zhoda ticho, nezhoda nahlas. */
.pending-mid { color: var(--dim); font-size: 0.85rem; }
.pending-mid.ok { color: var(--ok); }
.pending-mid.bad { color: var(--danger); font-weight: 600; }
.pending-when { color: var(--faint); font-size: 0.82rem; white-space: nowrap; }
.pending-act { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }

/* Firmware page: what ships now, then history folded away.
   
   The flat list this replaces was an archaeology dig before every action — every
   version ever, newest first, revoked mixed in with what is actually running. */
/* One grid, not two floating cards: equal columns keep the version numbers,
   the artifact lists and the changelog ledes on the same lines across both
   cards, so the eye compares like with like instead of hunting. When only one
   card exists it takes a single column rather than stretching across the page,
   where a 1400px-wide card of 60 words looks like a layout accident. */
.hero-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
  align-items: start;
  gap: 1rem;
  margin-bottom: 1rem;
}
.hero-row.one { grid-template-columns: minmax(22rem, 34rem); }
/* An open changelog is a document, not a card detail: give it room to reach
   the 70ch the prose cap is asking for instead of squeezing twelve bullets
   through a 30rem column on a 1400px screen. */
.hero-row.reading { grid-template-columns: repeat(auto-fit, minmax(26rem, 1fr)); }
.hero-row.one.reading { grid-template-columns: minmax(22rem, 46rem); }
.hero-card { margin-bottom: 0; }
.hero-card.primary { border-color: color-mix(in oklab, var(--ok) 40%, var(--line-2)); }
.hero-version {
  font-family: var(--f-mono);
  font-size: 1.6rem;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.hero-when { color: var(--faint); font-size: 0.85rem; margin-top: 0.15rem; }
/* Artifact rows share one column grid so board names, hashes, sizes and the
   provenance line up down the card. As a flex row each line set its own
   columns and nothing below the first row agreed with anything above it. */
.hero-arts {
  margin-top: 0.8rem;
  display: grid;
  /* max-content, not auto: an auto max-track grows into the free space, which
     on a wide card flung the size column to the far edge and left a hand-span
     of nothing between a hash and its own size. */
  grid-template-columns: minmax(0, max-content) max-content max-content max-content;
  justify-content: start;
  gap: 0.3rem 1.25rem;
  align-items: baseline;
}
.hero-art {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  font-size: 0.85rem;
}
.hero-sha, .hero-size { color: var(--faint); }
/* The provenance sits under its artifact rather than in a fourth column: it
   is the longest string on the row and the least often needed, so giving it
   its own column would push the hashes off narrow cards to serve the least
   urgent fact. */
.hero-prov { grid-column: 1 / -1; color: var(--faint); font-size: 0.78rem; margin-top: -0.15rem; }
.hero-prov:empty { display: none; }
/* The per-artifact download link in the hero: the same full-width line
   hero-prov used to occupy (see releaseHeroCard) — nothing else in the row
   needs to share space with it. */
/* The download is the fourth cell of the artifact row, not a line of its own. */
.hero-dl { grid-column: auto; justify-self: start; padding: 0 0.45rem; line-height: 1.3; }

/* Koľko flotily už na tom vydaní naozaj beží. */
.adopt-total { color: var(--faint); font-size: 0.6em; }
.adopt-bar {
  margin-top: 0.7rem;
  height: 6px;
  border-radius: 999px;
  /* --line-2, nie --surface-2: pri 0 % nie je čo vyplniť a lišta, ktorá
     v tom stave splynie s kartou, vyzerá ako chýbajúci prvok. Prázdny stav
     má byť vidieť rovnako dobre ako plný. */
  background: var(--line-2);
  overflow: hidden;
}
.adopt-bar > span { display: block; height: 100%; background: var(--ok); transition: width .3s ease-out; }
@media (prefers-reduced-motion: reduce) { .adopt-bar > span { transition: none; } }
.adopt-rest { margin-top: 0.9rem; }
.adopt-rest-h { margin: 0 0 0.3rem; }
.adopt-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
  font-size: 0.85rem;
  padding: 0.15rem 0;
  max-width: 24rem;
}
.adopt-ver { color: var(--dim); }
.adopt-n { color: var(--faint); font-variant-numeric: tabular-nums; }
.adopt-all { margin-top: 0.9rem; }

/* Mazanie stiahnutého vydania. Malé a stlmené: je to upratovanie, nie akcia,
   ktorá má na obrazovke súťažiť s čímkoľvek iným. */
.rel-del {
  appearance: none;
  margin-left: 0.6rem;
  padding: 0.05rem 0.4rem;
  font: inherit;
  font-size: 0.75rem;
  color: var(--faint);
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  cursor: pointer;
}
.rel-del:hover { color: var(--danger); border-color: var(--danger); }

.hero-log { margin-top: 0.9rem; }
/* Prose is capped near 70ch wherever it appears. Past that the eye loses the
   start of the next line, and a release note is read, not scanned. */
.cl-lede { margin: 0; max-width: 68ch; color: var(--dim); }
.cl-counts { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.6rem; }
.cl-count {
  font-size: 0.78rem;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--faint);
  white-space: nowrap;
}
.cl-toggle { margin-top: 0.6rem; }
.cl-full { margin-top: 0.7rem; }

/* Rendered changelog. Headings are the smallest step that still reads as a
   heading — inside a card, an <h4> that shouts competes with the card title. */
.prose { max-width: 70ch; }
.prose h4 {
  margin: 1rem 0 0.35rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink);
}
.prose h4:first-child { margin-top: 0; }
.prose p { margin: 0 0 0.6rem; }
.prose p:last-child { margin-bottom: 0; }
.prose ul { margin: 0 0 0.6rem; padding-left: 1.1rem; }
.prose li { margin-bottom: 0.3rem; }
.prose li:last-child { margin-bottom: 0; }
.prose code {
  font-family: var(--f-mono);
  font-size: 0.9em;
  background: var(--surface-2);
  padding: 0.05rem 0.25rem;
  border-radius: 3px;
}
.prose strong { color: var(--ink); font-weight: 600; }

.rel-history { margin-bottom: 1.25rem; }
.rel-history-toggle, .rel-more, .cl-toggle {
  appearance: none;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--surface-2);
  color: var(--dim);
  font: inherit;
  font-size: 0.9rem;
  padding: 0.4rem 0.8rem;
  cursor: pointer;
}
.rel-history-toggle:hover, .rel-more:hover, .cl-toggle:hover { color: var(--ink); }
.rel-more { margin-top: 0.6rem; }
/* A revoked release is a historical fact, not a row that greets you. */
tr.rel-row.quiet td { opacity: 0.55; }

/* ── remote access (web tunnel) ─────────────────────────────────── */
.remote-quick { display: flex; flex-wrap: wrap; gap: 6px; margin: 0.4rem 0 0.7rem; }
.chip {
  padding: 3px 11px; font-size: 0.82rem; border-radius: 999px;
  border: 1px solid var(--line-2); background: var(--bg-2); cursor: pointer;
}
.chip:hover:not(:disabled) { background: var(--line); }
.remote-req { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.remote-req #remotePath { flex: 1 1 220px; min-width: 150px; font-family: var(--f-mono); }
.remote-req select { flex: 0 0 auto; }
.remote-body { width: 100%; margin-top: 8px; font-family: var(--f-mono); font-size: 0.82rem; resize: vertical; }
.remote-out {
  margin-top: 10px; max-height: 360px; overflow: auto;
  background: var(--bg-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 10px 12px; font-family: var(--f-mono); font-size: 0.8rem;
  white-space: pre-wrap; word-break: break-word;
}
