/* =====================================================================
   swpcview — shared UI component layer
   Load order everywhere:  tokens.css -> icons.css -> ui.css -> page CSS
   Consumed by:  index.html (+style.css)   console.html   install.html

   WHY THIS FILE EXISTS
   The pre-redesign audit's biggest structural finding was that the product
   had two parallel UI vocabularies: style.css dressed the monitoring grid
   and console.html's inline <style> dressed the admin console, each with
   its own button, its own input, its own table, its own notion of a panel.
   The same control looked and behaved differently depending on which half
   of the product you were in. This file is the one place those primitives
   are defined; the page stylesheets keep only what is genuinely unique to
   that page.

   EVERY SELECTOR HERE IS A CLASS, DELIBERATELY.
   A bare `button{}`/`input{}` base rule in a shared file would restyle all
   three pages the instant it loaded, including the parts not yet migrated
   and including markup built by JS that nobody re-read. Opt-in classes mean
   a screen changes when it is migrated and tested, not when this file is
   linked. The only element-level rules are wrapped in :where() (zero
   specificity), so a page rule always wins without needing !important.

   DENSITY
   Controls are 34px tall, not 28 (cramped, and misses the WCAG 2.5.8 24px
   target once padding is real) and not 44 (a form of six fields becomes a
   scroll). 44px is applied on pointer:coarse only, where a finger needs it —
   keyed on the pointer, not on the viewport width, because a 1024px touch
   tablet needs the bigger target and a 700px desktop window does not.
   ===================================================================== */

/* ---------------------------------------------------------------- base */
:where(body){
  font-family:var(--font-ui);
  font-size:var(--fs-body);
  font-weight:var(--fw-body);
  line-height:1.5;
  color:var(--txt);
  background:var(--bg);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
:where(h1,h2,h3,h4){ margin:0; font-weight:var(--fw-head); letter-spacing:-.011em }
:where(a){ color:var(--acc); text-decoration:none }

/* The hidden attribute must beat a class.
   The browser hides [hidden] via a UA rule with specificity (0,0,1), so ANY
   class rule that sets display — .warn{display:flex}, .ft-gate{display:flex} —
   silently outranks it and the element is shown even though JS set .hidden =
   true. That is not theoretical: it shipped the install page's "Limit reached"
   warning permanently visible at 8/25 PCs, and pinned all three file-transfer
   gates open ("Start remote control to send files") even while controlling.
   One rule fixes every present and future case; the guards written per-element
   elsewhere in the page CSS are now belt-and-braces, not load-bearing.
   !important is the documented idiom here — it is what makes an attribute
   defeat a class, which is exactly the intent of `hidden`. */
[hidden]{ display:none !important }

/* Hairlines are drawn with a token, never a raw colour. --line is the default
   separator; --line-strong is reserved for something you must be able to find
   with your eye (an input's edge, a dropped divider between action groups). */

/* ------------------------------------------------------------ typography
   Seven roles, no more. The audit found eight ad-hoc pixel sizes across the
   product with no relationship between them; these are the whole scale and
   every one maps to a token in tokens.css. */
.t-page   { font-size:var(--fs-xl);   font-weight:var(--fw-head); letter-spacing:-.015em }
.t-section{ font-size:var(--fs-lg);   font-weight:var(--fw-head); letter-spacing:-.01em }
.t-card   { font-size:var(--fs-md);   font-weight:var(--fw-label) }
.t-body   { font-size:var(--fs-body); font-weight:var(--fw-body) }
.t-sub    { font-size:var(--fs-body); color:var(--mut) }
.t-label  { font-size:var(--fs-label);font-weight:var(--fw-label); color:var(--mut) }
.t-meta   { font-size:var(--fs-caption); color:var(--faint) }
/* Column headers / eyebrow labels. Uppercase is a deliberate exception, used
   only where a label must read as chrome rather than as content. */
.t-eyebrow{
  font-size:var(--fs-caption); font-weight:var(--fw-label); color:var(--mut);
  text-transform:uppercase; letter-spacing:.06em;
}
.t-num{ font-variant-numeric:tabular-nums }
.mono{ font-family:var(--font-mono) }
.truncate{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0 }

/* --------------------------------------------------------------- buttons
   Four intents, three sizes. Intent is what the button MEANS (primary =
   the one action the screen exists for), size is how much room it gets.
   Both are additive classes so a JS caller never has to know a compound
   name like .btn-primary-sm. */
.btn{
  --btn-h:34px;
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  height:var(--btn-h); padding:0 13px;
  border:1px solid transparent; border-radius:var(--r-2);
  background:var(--surface-2); color:var(--txt);
  font:inherit; font-size:var(--fs-body); font-weight:var(--fw-label);
  line-height:1; white-space:nowrap; cursor:pointer;
  transition:background var(--dur) var(--ease), border-color var(--dur) var(--ease),
             color var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
/* Secondary is the default weight: a bordered surface. Most buttons on a
   working screen are this, which is why it needs no class of its own. */
.btn{ border-color:var(--line-strong) }
.btn:hover{ background:var(--surface-3) }
.btn:active{ transform:translateY(.5px) }

.btn-primary{ background:var(--acc-fill); border-color:var(--acc-fill); color:#fff }
.btn-primary:hover{ background:var(--acc-fill-hover); border-color:var(--acc-fill-hover) }

/* Ghost: no chrome until hovered. For icon-only toolbar actions and menu
   items, where a row of bordered boxes would out-shout the content. */
.btn-ghost{ background:transparent; border-color:transparent; color:var(--mut) }
.btn-ghost:hover{ background:var(--surface-3); color:var(--txt) }

/* Destructive actions are tinted, not filled: a wall of red buttons trains
   people to ignore red. The fill arrives on hover, at the moment of intent. */
.btn-danger{ background:transparent; border-color:var(--line-strong); color:var(--bad-fg) }
.btn-danger:hover{ background:var(--bad-bg); border-color:var(--bad) }

.btn-sm{ --btn-h:28px; padding:0 10px; font-size:var(--fs-label) }
.btn-lg{ --btn-h:40px; padding:0 18px; font-size:var(--fs-md) }
.btn-block{ width:100% }

/* Icon-only: square, and REQUIRED to carry an aria-label (the audit found
   icon buttons whose only label was a title attribute, invisible to a screen
   reader and to touch). Nothing enforces that in CSS, so it is enforced in
   review — every .btn-icon in this repo has one. */
.btn-icon{ width:var(--btn-h); padding:0 }
.btn-icon .ic{ font-size:17px }
.btn-sm.btn-icon .ic{ font-size:15px }

.btn:disabled,.btn[aria-disabled="true"]{
  opacity:.45; cursor:not-allowed; pointer-events:none;
}
/* A button that is disabled because a permission is missing keeps its
   pointer events so its title/tooltip can still explain why — opacity alone,
   no pointer-events:none. Set by JS alongside disabled. */
.btn.is-blocked{ opacity:.45; cursor:not-allowed; pointer-events:auto }

/* In-flight: the label stays (no layout jump), a spinner replaces the icon. */
.btn.is-busy{ pointer-events:none; opacity:.7 }
.btn.is-busy .ic{
  background:none;
  border:2px solid currentColor; border-top-color:transparent;
  border-radius:50%; width:1em; height:1em; animation:ui-spin .7s linear infinite;
}
@keyframes ui-spin{ to{ transform:rotate(360deg) } }

/* A group of buttons that read as one control (segmented view switch). */
.btn-group{ display:inline-flex; gap:0 }
.btn-group .btn{ border-radius:0; margin-left:-1px }
.btn-group .btn:first-child{ border-radius:var(--r-2) 0 0 var(--r-2); margin-left:0 }
.btn-group .btn:last-child{ border-radius:0 var(--r-2) var(--r-2) 0 }
.btn-group .btn.is-on{ background:var(--acc-soft); border-color:var(--acc); color:var(--txt); z-index:1 }

/* ---------------------------------------------------------------- forms */
.field{ display:flex; flex-direction:column; gap:5px; min-width:0 }
.field > label,.lbl{
  font-size:var(--fs-label); font-weight:var(--fw-label); color:var(--mut);
}
/* Required is marked with a glyph AND the word in the a11y tree — never by
   colour alone, which is also why .field-err below carries an icon. */
.lbl .req{ color:var(--bad); margin-left:2px }

.inp,.sel,.txa{
  width:100%; min-width:0;
  height:34px; padding:0 11px;
  background:var(--surface-2); color:var(--txt);
  border:1px solid var(--line-strong); border-radius:var(--r-2);
  font:inherit; font-size:var(--fs-body);
  transition:border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.txa{ height:auto; min-height:80px; padding:9px 11px; resize:vertical; line-height:1.5 }
.inp::placeholder,.txa::placeholder{ color:var(--faint) }
.inp:focus,.sel:focus,.txa:focus{
  outline:none; border-color:var(--acc); box-shadow:0 0 0 3px var(--acc-soft);
}
.inp:disabled,.sel:disabled,.txa:disabled{ opacity:.55; cursor:not-allowed }
.sel{
  appearance:none; padding-right:30px; cursor:pointer;
  /* chevron drawn in CSS so a <select> matches .btn without a wrapper element */
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='gray' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
  background-repeat:no-repeat; background-position:right 9px center; background-size:15px;
}
/* iOS zooms the page when a focused field is under 16px. Only on touch, so
   desktop keeps the 14px that the rest of the UI is set in. */
@media (pointer:coarse){
  .inp,.sel,.txa{ font-size:16px }
}

.field-hint{ font-size:var(--fs-caption); color:var(--faint) }
.field-err{
  display:flex; align-items:center; gap:5px;
  font-size:var(--fs-caption); color:var(--bad-fg);
}
.field-err .ic{ font-size:13px }
.field.is-err .inp,.field.is-err .sel,.field.is-err .txa{ border-color:var(--bad) }

/* Checkbox / radio keep the native control (free a11y, free keyboard) and
   only get an accent colour and a comfortable hit row. */
.chk{ display:flex; align-items:center; gap:9px; cursor:pointer; font-size:var(--fs-body) }
.chk input{ width:16px; height:16px; accent-color:var(--acc); flex:none; cursor:pointer; margin:0 }
@media (pointer:coarse){ .chk input{ width:22px; height:22px } }

/* Switch: for a setting that takes effect immediately (no Save button).
   A checkbox means "this will be true when you submit"; a switch means
   "this is true now" — they are not interchangeable. */
.swt{ display:inline-flex; align-items:center; gap:9px; cursor:pointer }
.swt input{ position:absolute; opacity:0; width:0; height:0 }
.swt .swt-track{
  width:36px; height:20px; border-radius:var(--r-full); flex:none;
  background:var(--surface-3); border:1px solid var(--line-strong);
  transition:background var(--dur) var(--ease), border-color var(--dur) var(--ease);
  position:relative;
}
.swt .swt-track::after{
  content:""; position:absolute; top:2px; left:2px; width:14px; height:14px;
  border-radius:50%; background:var(--mut);
  transition:transform var(--dur) var(--ease), background var(--dur) var(--ease);
}
.swt input:checked + .swt-track{ background:var(--acc-fill); border-color:var(--acc-fill) }
.swt input:checked + .swt-track::after{ transform:translateX(16px); background:#fff }
.swt input:focus-visible + .swt-track{ outline:2px solid var(--acc); outline-offset:2px }

/* Search box: the icon is part of the control, not a placeholder emoji. */
.search{ position:relative; display:flex; align-items:center; min-width:0 }
.search > .ic{
  position:absolute; left:10px; font-size:15px; color:var(--faint); pointer-events:none;
}
.search .inp{ padding-left:32px }
.search .inp::-webkit-search-cancel-button{ cursor:pointer }

/* -------------------------------------------------------------- surfaces
   A panel is a grouping device, not a decoration. Nesting one inside another
   is a smell: if a panel needs sections, use .panel-sec separators. */
.panel{
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--r-4); box-shadow:var(--sh-1);
}
.panel-hd{
  display:flex; align-items:center; gap:var(--sp-3);
  padding:var(--sp-4) var(--sp-5); border-bottom:1px solid var(--line);
}
.panel-hd > .t-card{ margin-right:auto }
.panel-bd{ padding:var(--sp-5) }
.panel-bd.is-flush{ padding:0 }            /* a table fills its panel edge to edge */
.panel-ft{
  display:flex; align-items:center; gap:var(--sp-2); justify-content:flex-end;
  padding:var(--sp-3) var(--sp-5); border-top:1px solid var(--line);
}
.panel-sec + .panel-sec{ border-top:1px solid var(--line) }

/* Stat tile. The number is the content; the label is chrome under it. */
.stat{
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r-4);
  padding:var(--sp-4) var(--sp-5); box-shadow:var(--sh-1);
  transition:border-color var(--dur) var(--ease);
}
.stat:hover{ border-color:var(--line-strong) }
.stat .stat-n{ font-size:var(--fs-2xl); font-weight:700; letter-spacing:-.02em; font-variant-numeric:tabular-nums }
.stat .stat-l{ margin-top:2px }
.stat-row{ display:grid; grid-template-columns:repeat(auto-fit,minmax(168px,1fr)); gap:var(--sp-3) }

/* ---------------------------------------------------------- page framing
   Every screen uses the same three bands, in the same order, so a person who
   learns one page can read the next: header (what am I looking at + the
   primary action), toolbar (how do I narrow it), content. */
.page-hd{
  display:flex; align-items:flex-start; gap:var(--sp-4);
  margin-bottom:var(--sp-5); flex-wrap:wrap;
}
.page-hd-txt{ min-width:0; flex:1 }
.page-hd .t-sub{ margin-top:3px }
.page-hd-acts{ display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap }

.toolbar{
  display:flex; align-items:center; gap:var(--sp-2);
  margin-bottom:var(--sp-4); flex-wrap:wrap;
}
.toolbar .search{ flex:0 1 280px }
.toolbar-gap{ flex:1 }                      /* pushes what follows to the right */
.toolbar-div{ width:1px; height:20px; background:var(--line); flex:none }

/* Breadcrumbs appear ONLY where a real parent/child hierarchy exists (the
   super-admin org drill-down). A breadcrumb on a flat page is decoration
   that teaches people to distrust breadcrumbs. */
.crumbs{ display:flex; align-items:center; gap:6px; font-size:var(--fs-label); margin-bottom:var(--sp-2) }
.crumbs a{ color:var(--mut) } .crumbs a:hover{ color:var(--txt) }
.crumbs .ic{ font-size:13px; color:var(--faint) }

/* ------------------------------------------------------------------ tabs */
.tabs{ display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:var(--sp-4) }
.tabs .tab{
  position:relative; background:transparent; border:0; cursor:pointer;
  padding:9px 13px; margin-bottom:-1px;
  color:var(--mut); font:inherit; font-size:var(--fs-body); font-weight:var(--fw-label);
  border-bottom:2px solid transparent;
  transition:color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.tabs .tab:hover{ color:var(--txt) }
.tabs .tab.active,.tabs .tab[aria-selected="true"]{ color:var(--txt); border-bottom-color:var(--acc) }

/* ---------------------------------------------------------------- tables
   Working surfaces. Horizontal rules only — vertical rules turn a table into
   a spreadsheet and make every cell a box. */
.tbl{ width:100%; border-collapse:collapse; font-size:var(--fs-body) }
.tbl th,.tbl td{ text-align:left; padding:10px var(--sp-4); border-bottom:1px solid var(--line) }
.tbl th{
  font-size:var(--fs-caption); font-weight:var(--fw-label); color:var(--mut);
  text-transform:uppercase; letter-spacing:.06em; white-space:nowrap;
  background:var(--surface); position:sticky; top:0; z-index:1;
}
.tbl tbody tr:last-child td{ border-bottom:0 }
.tbl tbody tr{ transition:background var(--dur) var(--ease) }
.tbl tbody tr:hover td{ background:var(--surface-2) }
.tbl tr.is-clickable{ cursor:pointer }
.tbl tr.is-selected td{ background:var(--acc-soft) }
.tbl td.num,.tbl th.num{ text-align:right; font-variant-numeric:tabular-nums }
/* Row actions stay invisible until the row is touched, so a 30-row table is
   not 90 competing buttons. They become visible on hover, on keyboard focus
   within the row, and permanently on touch (where there is no hover). */
.tbl .row-acts{ display:flex; gap:4px; justify-content:flex-end; opacity:0; transition:opacity var(--dur) var(--ease) }
.tbl tr:hover .row-acts,.tbl tr:focus-within .row-acts{ opacity:1 }
@media (pointer:coarse){ .tbl .row-acts{ opacity:1 } }

/* A table that is wider than its container scrolls inside it — the page
   itself must never scroll sideways. */
.tbl-wrap{ overflow-x:auto; -webkit-overflow-scrolling:touch }

/* ------------------------------------------------------- badges / status */
.badge{
  display:inline-flex; align-items:center; gap:5px;
  padding:2px 9px; border-radius:var(--r-full);
  font-size:var(--fs-label); font-weight:var(--fw-label); white-space:nowrap;
  background:var(--surface-2); color:var(--mut); border:1px solid transparent;
}
.badge.ok  { background:var(--ok-bg);   color:var(--ok-fg) }
.badge.warn{ background:var(--warn-bg); color:var(--warn-fg) }
.badge.bad { background:var(--bad-bg);  color:var(--bad-fg) }
.badge.info{ background:var(--info-bg); color:var(--info-fg) }
.badge.acc { background:var(--acc-soft);color:var(--acc) }
.badge .ic{ font-size:12px }

/* Status is carried by a dot AND a word, never by colour alone. */
.dot{ width:7px; height:7px; border-radius:50%; background:var(--faint); flex:none }
.dot.on{ background:var(--ok) }
.dot.live{ background:var(--ok); box-shadow:0 0 0 3px var(--ok-bg) }
.dot.warn{ background:var(--warn) } .dot.bad{ background:var(--bad) }

/* --------------------------------------------------------------- banners */
.note{
  display:flex; align-items:flex-start; gap:9px;
  padding:10px 13px; border-radius:var(--r-2);
  font-size:var(--fs-label);
  background:var(--info-bg); color:var(--info-fg); border:1px solid transparent;
}
.note .ic{ font-size:15px; flex:none; margin-top:1px }
.note.warn{ background:var(--warn-bg); color:var(--warn-fg) }
.note.bad { background:var(--bad-bg);  color:var(--bad-fg) }
.note.ok  { background:var(--ok-bg);   color:var(--ok-fg) }

/* ------------------------------------------------------- menus / popovers
   One skin for the "⋮" overflow, the per-view picker and the timer
   dropdowns, which the audit found as three unrelated implementations. */
.menu{
  min-width:196px; padding:5px;
  background:var(--surface); border:1px solid var(--line-strong);
  border-radius:var(--r-3); box-shadow:var(--sh-2);
  z-index:var(--z-popover);
}
.menu-item{
  display:flex; align-items:center; gap:10px; width:100%;
  padding:8px 10px; border:0; border-radius:var(--r-1);
  background:transparent; color:var(--txt);
  font:inherit; font-size:var(--fs-body); text-align:left; cursor:pointer;
  transition:background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.menu-item:hover{ background:var(--surface-3) }
.menu-item .ic{ font-size:16px; color:var(--mut) }
.menu-item:hover .ic{ color:var(--txt) }
.menu-item.is-danger{ color:var(--bad-fg) }
.menu-item.is-danger:hover{ background:var(--bad-bg) }
.menu-item.is-danger .ic{ color:var(--bad-fg) }
.menu-item[disabled]{ opacity:.45; pointer-events:none }
.menu-sep{ height:1px; background:var(--line); margin:5px 0 }
.menu-lbl{ padding:7px 10px 4px }

/* Animated in, not out: an exit animation delays the next thing you clicked. */
.menu.is-open{ animation:ui-pop .13s var(--ease) }
@keyframes ui-pop{ from{ opacity:0; transform:translateY(-4px) } to{ opacity:1; transform:none } }

/* ------------------------------------------------------- modals / dialogs
   Three sizes. Anything that wants a fourth wants a page instead. */
.dlg{
  padding:0; border:1px solid var(--line-strong); border-radius:var(--r-4);
  background:var(--surface); color:var(--txt);
  box-shadow:var(--sh-3);
  width:min(560px,calc(100vw - 32px));
  max-height:min(86vh,860px);
}
.dlg.dlg-sm{ width:min(420px,calc(100vw - 32px)) }
.dlg.dlg-lg{ width:min(860px,calc(100vw - 32px)) }
.dlg::backdrop{ background:rgba(4,6,11,.62) }
:root[data-theme="light"] .dlg::backdrop{ background:rgba(20,25,40,.38) }
.dlg[open]{ animation:ui-dlg .16s var(--ease) }
@keyframes ui-dlg{ from{ opacity:0; transform:translateY(6px) scale(.99) } to{ opacity:1; transform:none } }

/* Header and footer are fixed; only the body scrolls. A dialog whose action
   row scrolls out of reach is a dialog you cannot submit on a short screen. */
.dlg-inner{ display:flex; flex-direction:column; max-height:inherit }
.dlg-hd{
  display:flex; align-items:center; gap:var(--sp-3);
  padding:var(--sp-4) var(--sp-5); border-bottom:1px solid var(--line); flex:none;
}
.dlg-hd .t-card{ margin-right:auto; min-width:0 }
.dlg-bd{ padding:var(--sp-5); overflow-y:auto; flex:1 1 auto }
.dlg-ft{
  display:flex; align-items:center; justify-content:flex-end; gap:var(--sp-2);
  padding:var(--sp-3) var(--sp-5); border-top:1px solid var(--line); flex:none;
}
/* On a phone a centred box wastes the bottom half of the screen and puts the
   actions under the thumb's reach — dock it to the bottom edge instead. */
@media (max-width:720px){
  .dlg,.dlg.dlg-sm,.dlg.dlg-lg{
    width:100vw; max-width:100vw; max-height:92vh;
    margin:auto auto 0; border-radius:var(--r-4) var(--r-4) 0 0; border-bottom:0;
  }
}

/* ----------------------------------------------------------------- toast
   Stacked, not overlaid: the audit found concurrent toasts printing on top of
   each other. A flex column in a fixed container stacks them for free.
   role="status" on the container means a screen reader announces each one. */
/* util.js promotes this host to popover="manual" so toasts clear an open
   <dialog> (the top layer, which z-index cannot reach). That drags in the UA
   [popover] rules -- inset:0, margin:auto, border:solid, padding:.25em,
   background:Canvas -- and none of them are wanted here. Unreset they cost two
   bugs at once: an EMPTY host still paints as a small bordered box dead-centre
   of the viewport (top:0/left:0 survive, margin:auto centres it), and a host
   WITH toasts centres them instead of anchoring bottom-right. Reset the box
   here, not in JS: console.js builds a .toast-host that is not a popover, and
   these declarations are a no-op for it. */
.toast-host{
  position:fixed; inset:auto var(--sp-5) var(--sp-5) auto; z-index:var(--z-flash);
  margin:0; border:0; padding:0; background:transparent; overflow:visible;
  display:flex; flex-direction:column-reverse; gap:var(--sp-2);
  pointer-events:none; max-width:min(380px,calc(100vw - 32px));
}
.toast{
  display:flex; align-items:flex-start; gap:9px;
  padding:10px 14px; border-radius:var(--r-3);
  background:var(--surface); color:var(--txt);
  border:1px solid var(--line-strong); box-shadow:var(--sh-2);
  font-size:var(--fs-label); pointer-events:auto; word-break:break-word;
  animation:ui-toast .18s var(--ease);
}
.toast .ic{ font-size:15px; flex:none; margin-top:1px }
.toast.ok  .ic{ color:var(--ok) }
.toast.bad .ic{ color:var(--bad) }
.toast.warn .ic{ color:var(--warn) }
@keyframes ui-toast{ from{ opacity:0; transform:translateY(8px) } to{ opacity:1; transform:none } }
@media (max-width:720px){
  .toast-host{ left:var(--sp-3); right:var(--sp-3); bottom:var(--sp-3); max-width:none }
}

/* --------------------------------------------------- empty / loading states
   An empty table and a loading table look identical if neither says anything,
   and "no results for your filter" is a different message from "you have
   nothing yet" — the first has a way out, the second does not. */
.empty{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--sp-3); padding:var(--sp-10) var(--sp-5); text-align:center;
}
.empty .ic-empty{ color:var(--faint) }
.empty-t{ font-size:var(--fs-md); font-weight:var(--fw-label) }
.empty-d{ font-size:var(--fs-label); color:var(--mut); max-width:44ch }

.skel{
  background:linear-gradient(90deg,var(--surface-2) 25%,var(--surface-3) 37%,var(--surface-2) 63%);
  background-size:400% 100%; border-radius:var(--r-1);
  animation:ui-skel 1.3s ease-in-out infinite;
}
@keyframes ui-skel{ from{ background-position:100% 50% } to{ background-position:0 50% } }
.skel-line{ height:11px; margin:9px 0 }
.skel-row{ height:38px; margin:0 }

.spin{
  display:inline-block; width:15px; height:15px; flex:none;
  border:2px solid var(--line-strong); border-top-color:var(--acc);
  border-radius:50%; animation:ui-spin .7s linear infinite;
}
.loading-row{ display:flex; align-items:center; gap:9px; padding:var(--sp-5); color:var(--mut); font-size:var(--fs-label) }

/* ------------------------------------------------------------ navigation */
.nav-item{
  display:flex; align-items:center; gap:10px;
  padding:8px 11px; border-radius:var(--r-2);
  color:var(--mut); font-size:var(--fs-body); font-weight:500;
  cursor:pointer; border:0; background:transparent; width:100%; text-align:left;
  transition:background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nav-item:hover{ background:var(--surface-2); color:var(--txt) }
.nav-item .ic{ font-size:17px; flex:none }
/* Active state is carried by weight + fill + a rail, not by colour alone. */
.nav-item.on{
  background:var(--acc-soft); color:var(--txt); font-weight:var(--fw-label);
  box-shadow:inset 2px 0 0 var(--acc);
}
.nav-item.on .ic{ color:var(--acc) }
.nav-group{ padding:var(--sp-4) var(--sp-2) 4px }

/* ------------------------------------------------------------- utilities
   Deliberately few. A utility set that can express any layout is a second
   styling language competing with the components above. */
.row{ display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap }
.col{ display:flex; flex-direction:column; gap:var(--sp-2) }
.grid2{ display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-3) var(--sp-5) }
@media (max-width:720px){ .grid2{ grid-template-columns:1fr } }
.gap{ flex:1 }
.muted{ color:var(--mut) }
.faint{ color:var(--faint) }
.danger-txt{ color:var(--bad-fg) }
.ok-txt{ color:var(--ok-fg) }
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* ---------------------------------------------------------- touch floors
   Keyed on the pointer, not the viewport: a 1024px touch tablet needs these
   and a 700px desktop window does not. Checkboxes are excluded on purpose —
   min-height only stretches them vertically (a 16x44 target is still 16px
   wide); .chk input sizes the box itself above. */
@media (pointer:coarse){
  .btn{ --btn-h:44px }
  .btn-sm{ --btn-h:38px }
  .inp,.sel{ height:44px }
  .nav-item,.menu-item{ min-height:44px }
  .tabs .tab{ min-height:44px }
}
