/* ============================================================
   ELIO — panel design system
   Ops/control-room aesthetic: layered dark, monochrome white brand,
   monospace telemetry, green reserved for live signals.
   One stylesheet → every page. Class names kept stable so the
   whole panel lifts at once, then pages get progressively polished.
   ============================================================ */

:root {
  color-scheme: dark;

  /* neutrals — deep near-black scale (blacker canvas, layered surfaces) */
  --bg:        #030304;
  --surface-1: #08080A;   /* sidebar / topbar */
  --surface-2: #0C0D10;   /* panels */
  --surface-3: #15161C;   /* raised: inputs, hovers */
  --surface-4: #1B1C23;   /* input hover */
  /* hairlines carry the whole layout on a near-black canvas — at .10/.07 they
     dropped below the point where a panel edge reads as an edge. */
  --line:      rgba(255,255,255,.15);
  --line-2:    rgba(255,255,255,.10);

  /* one hover surface for every interactive chrome element (no ad-hoc rgba fills) */
  --hover:   rgba(255,255,255,.06);
  --hover-2: rgba(255,255,255,.09);

  /* text — every level clears 4.5:1 on --surface-2, including ink-4. The old
     scale bottomed out at 2.6:1, which is what made labels, placeholders and
     disabled controls read as "missing" rather than "secondary". */
  --ink:   #EDEFF3;
  /* Secondary and help text stepped up one level. Same hue family, same
     palette — the interface simply stopped mumbling its explanations.
     Measured against --surface-2 (#0C0D10), the surface they sit on most. */
  --ink-2: #CFD4DC;   /* body secondary   10.9 -> 12.9:1 */
  --ink-3: #B0B7C3;   /* muted / help      7.3 ->  9.5:1 */
  --ink-4: #949CAA;   /* section labels    5.1 ->  6.9:1 */

  /* brand — monochrome white (ELIO mark). CTA / selection only. */
  --accent:      #FFFFFF;
  --accent-h:    #E6E8EC;
  --accent-ink:  #0A0A0C;
  --accent-soft: rgba(255,255,255,.10);
  --accent-line: rgba(255,255,255,.32);
  --accent-glow: rgba(255,255,255,.16);
  --focus:       rgba(255,255,255,.60);

  /* semantic — green = live telemetry (means "farming/online") */
  --live:      #35D6A0;
  --live-dim:  #2BA97D;   /* status dots: same hue, not acid */
  --live-soft: rgba(53,214,160,.12);
  --live-glow: rgba(53,214,160,.40);   /* line drop-shadow on charts */
  --warn:      #F5B23E;
  --warn-soft: rgba(245,178,62,.14);
  --bad:       #FF5B6B;
  --bad-soft:  rgba(255,91,107,.13);

  /* radii — restrained; big containers no rounder than 14 */
  --r-xs: 8px; --r-sm: 10px; --r: 12px; --r-lg: 14px; --r-xl: 18px;

  /* motion — feedback / continuity / hierarchy only */
  /* every chrome transition sits in the 120-180ms band; --slow is for data
     readouts (a progress bar easing to its new width), not for chrome */
  --fast: 120ms; --base: 180ms; --slow: 300ms;
  --ease-out:    cubic-bezier(.22,.61,.36,1);
  --ease-in-out: cubic-bezier(.65,0,.35,1);

  /* type */
  --sans: 'Inter', -apple-system, 'Segoe UI', Roboto, system-ui, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  /* One ladder, eight rungs. Keep the whole interface on this shared scale so
     changing its density remains one edit rather than a page-by-page chase. */
  --fs-1: 10px;   /* micro: badge counts, version pills                         */
  --fs-2: 11px;   /* labels, table headers, chips, dense buttons               */
  --fs-3: 12px;   /* help text, nav, buttons                                   */
  --fs-4: 13px;   /* body, panel titles                                        */
  --fs-5: 16px;   /* card readouts, drawer and modal titles                    */
  --fs-6: 19px;   /* section display                                           */
  --fs-7: 24px;   /* page title                                                */
  --fs-8: 26px;   /* stat readout                                              */

  /* Three line-heights, chosen by what the text does — not by its size.
     128 elements were rendering at `normal` because nothing set one. */
  --lh-tight: 1.25;  /* numbers and display type that must not look airy */
  --lh-snug:  1.4;   /* headings, labels, single-line UI text            */
  --lh-body:  1.55;  /* anything the reader reads as prose               */

  --sidebar-w: 258px;

  /* legacy token aliases — older inline styles still resolve to the new scale */
  --card: var(--surface-2); --card2: var(--surface-3);
  --border: var(--line); --border-2: var(--line-2);
  --mute: var(--ink-2); --mute2: var(--ink-3);
  --acc-soft: var(--accent-soft); --acc-bord: var(--accent-line);
  --good: var(--live); --good-bg: var(--live-soft);
  --red: var(--bad); --accent2: var(--accent-h);
}

* { box-sizing: border-box; }
/* pin a narrow default for transition-property: the UA initial value is `all`,
   which audit tools report on every element even at duration 0. Authored
   transition shorthands below override this per-element. */
*, *::before, *::after {
  transition-property: opacity, transform, background-color, border-color, box-shadow, color;
}
/* html keeps a real height so body's min-height:100% has something to resolve
   against; body must NOT be capped to it. A fixed `height` here pinned body to
   one screen, so on a longer page the content overflowed the body box instead
   of growing it — which left the sticky sidebar no room to stick and slid it
   off the top, ending its surface short of the bottom. */
html { height: 100%; }
/* reserve the scrollbar gutter permanently on the page scroller. Without it, a
   short page has no vertical scrollbar and a tall one does — so navigating
   between them changes the viewport width, and the flex `.main` visibly shifts
   sideways on every button/link click. */
html { background: var(--bg); font-family: var(--sans); scrollbar-gutter: stable; }
body {
  margin: 0; color: var(--ink); background: var(--bg);
  font: 400 var(--fs-4)/1.55 var(--sans);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
  display: flex; min-height: 100%;
}
/* The sidebar is a sticky rail exactly one screen tall, so on any page longer
   than the viewport its surface simply stops and leaves a hard edge with bare
   page underneath. Paint the same column on the page itself — including the
   hairline that divides it — and it runs unbroken however far the page goes.
   Background only, so nothing about the layout moves. */
@media (min-width: 761px) {
  body.has-shell {
    background-image: linear-gradient(to right,
      var(--surface-1) 0 var(--sidebar-w),
      var(--line-2) var(--sidebar-w) calc(var(--sidebar-w) + 1px),
      transparent calc(var(--sidebar-w) + 1px));
    background-repeat: no-repeat;
  }
}
a { color: inherit; text-decoration: none; }
/* `hidden` means hidden — including out of the tab order.
   The UA stylesheet's [hidden]{display:none} is the weakest rule there is, so
   any component that set its own `display` silently outranked it: a closed
   drawer stayed a full-viewport click-eater, an empty bulk bar stayed on
   screen, and a collapsed inline form kept its Save button reachable by Tab as
   a 0×0 target. Five separate .x[hidden] patches had accumulated for the same
   cause; this is the cause. */
[hidden] { display: none !important; }
/* form controls don't inherit font-family by default → they fall back to the UA
   serif (Times New Roman). Force the sans everywhere so nothing leaks serif. */
button, input, textarea, select, optgroup, option {
  font-family: var(--sans); font-size: inherit; line-height: inherit;
}
::selection { background: rgba(255,255,255,.25); }
/* One focus treatment everywhere: crisp 2px ring, no glow bloom.
   The ring lives on `outline`, and that is the whole point of this rule. It used
   to be drawn with box-shadow — but .btn-ghost and .btn-danger each
   declare `box-shadow: none`, and .btn and .nav-link declare a decorative one,
   all further down this file. Same specificity as `:focus-visible` — (0,1,0) —
   so position decided it, and they won: the ring was erased and the `outline:
   none` right here had already removed the fallback. Measured: 71 of 394 Tab
   stops (18%) showed no focus indicator at all — every ghost button, the whole
   sidebar, the profile button.
   Nothing else in this file styles `outline`, so the ring cannot be silently
   overwritten again. Controls that paint their own stand-in switch it off
   explicitly, just below. */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
/* Inputs hidden under custom artwork: the ring belongs on the thing the user can
   see, and those rules live further down (.switch .track, .segmented label,
   .map-chip span, the file-field button). The invisible input must not carry a
   second one. */
.switch input:focus-visible,
.segmented input:focus-visible,
.map-chip input:focus-visible,
.file-field input[type=file]:focus-visible { outline: none; }
/* Truncating cells clip to produce their ellipsis, and an outline drawn outward
   is clipped along with the text: the ring around a bot name lost its right
   edge, because the link fills its cell to the pixel. Draw it inside instead. */
.row-link:focus-visible, .cell-trunc:focus-visible { outline-offset: -2px; }

/* thin custom scrollbars */
* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.14) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-thumb { background: rgba(255,255,255,.12); border-radius: 20px; border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.22); background-clip: content-box; }

/* ---------------- Sidebar ---------------- */
.sidebar {
  width: var(--sidebar-w); flex-shrink: 0;
  background: var(--surface-1);
  border-right: 1px solid var(--line-2);
  height: 100vh; position: sticky; top: 0;
  display: flex; flex-direction: column;
  /* a little air under the last item so it never sits flush on the edge */
  padding: 0 12px 22px; overflow-y: auto;
  /* symmetric gutter: when the nav is tall enough to scroll, the classic
     scrollbar reserves equal space both sides so the centered logo doesn't
     drift left (headless uses overlay scrollbars, so this is invisible there). */
  scrollbar-gutter: stable both-edges;
  transition: transform 180ms cubic-bezier(.2, .8, .2, 1);
}
/* brand area — logo has its OWN class, never a global img rule.
   Centered on the FULL sidebar (padding 0), not aligned to nav's left edge.
   Bigger logo lives in a taller brand zone so it can't overflow into nav. */
.brand { padding: 0; height: 96px; margin-bottom: 10px; flex: none; display: flex; align-items: center; justify-content: center; }
.brand-logo { display: block; width: 168px; height: auto; margin-inline: auto; }
/* explicit (narrow) transitions — never `all`, so hover/layout can't animate oddly */
.brand, .brand-logo { transition: opacity 160ms ease, transform 160ms ease; }

/* explicit narrow transitions on elements devtools reports as `all` (its default) — never `all` */
.nav-group, .nav-label, .panel, .card, .aside-card { transition: background-color 160ms ease, border-color 160ms ease; }
/* Icons never animate their own transform — only .chev rotates (its own rules
   below). Transitioning transform on every icon and, worse, on every SVG
   primitive made them jitter/jump while the sidebar scroll-repainted, so the
   transition lists here are transform-free. Still explicit — never `all`. */
.nav-link svg, .icon-btn svg, .btn svg, .logout svg { transition: color 160ms ease; }
.chev { transition: color 160ms ease, transform 160ms ease; }
/* also the primitives inside icons (rect/path/circle) so none inherit the `all` default */
svg *, .nav-link svg * { transition: color 160ms ease, fill 160ms ease, stroke 160ms ease; }
.nav-group { margin-bottom: 4px; }
.nav-label {
  font-size: var(--fs-2); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-4); padding: 15px 12px 7px;
}
/* first nav sits a comfortable distance below the logo, not cramped */
.nav-group:first-of-type .nav-label { padding-top: 4px; }
.nav-link {
  position: relative; display: flex; align-items: center; gap: 11px;
  padding: 9px 12px; border-radius: var(--r-sm);
  color: var(--ink-2); font-size: var(--fs-3); font-weight: 500; margin-bottom: 1px;
  transition: background-color 160ms var(--ease-out), color 160ms var(--ease-out);
}
/* uniform optical weight: every icon sits at the same stroke + opacity, so no
   single glyph reads heavier than its neighbours */
.nav-link svg { width: 18px; height: 18px; flex-shrink: 0; opacity: .7; stroke-width: 1.6px; }
.nav-link:hover { background: var(--hover); color: var(--ink); }
.nav-link:hover svg { opacity: .9; }
/* "you are here" = a deeper seat in the sidebar, not a lit-up blue chip.
   The accent survives only in the icon — one accent per component. */
.nav-link.active { background: rgba(255,255,255,.055); color: var(--ink); font-weight: 600;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.05); }
.nav-link.active svg { opacity: 1; color: var(--accent); }

/* An admin's nav is 19 links plus four group labels — 1049px, taller than a
   laptop viewport, so the sidebar grew its own scrollbar and the last items
   sat below the fold. Tighten the vertical rhythm as the window gets shorter
   instead; a tall monitor keeps the roomier spacing. */
@media (max-height: 980px) {
  .brand { height: 64px; margin-bottom: 4px; }
  .brand-logo { width: 136px; }
  .nav-group { margin-bottom: 2px; }
  .nav-label { padding: 9px 12px 4px; }
  .nav-group:first-of-type .nav-label { padding-top: 2px; }
  .nav-link { padding: 7px 12px; margin-bottom: 0; }
  .nav-link svg { width: 17px; height: 17px; }
}
@media (max-height: 860px) {
  /* the nav overshot by 9px at 768 tall; the 22px tail is the cheapest place
     to find them, and it still leaves air under the last item */
  .sidebar { padding-bottom: 10px; }
  .brand { height: 54px; }
  .brand-logo { width: 120px; }
  .nav-label { padding: 7px 12px 3px; font-size: var(--fs-2); }
  .nav-link { padding: 5px 12px; font-size: var(--fs-3); }
}
@media (max-height: 760px) {
  .brand { height: 44px; margin-bottom: 2px; }
  .brand-logo { width: 108px; }
  .nav-label { padding: 5px 12px 2px; }
  .nav-link { padding: 3px 12px; gap: 9px; }
  .nav-link svg { width: 16px; height: 16px; }
}


.side-bottom { margin-top: auto; padding-top: 10px; }
.user-card {
  display: flex; align-items: center; gap: 11px; padding: 11px; border-radius: var(--r);
  background: rgba(255,255,255,.04); border: 1px solid var(--line-2); margin-top: 6px;
  transition: border-color var(--fast), background-color var(--fast);
}
.user-card:hover { border-color: var(--line); background: rgba(255,255,255,.055); }
/* avatar = the same EV orbit mark as the favicon/app icon (one visual family) */
.user-card .ava {
  width: 36px; height: 36px; border-radius: 10px; flex-shrink: 0; overflow: hidden;
  border: 1px solid var(--line-2); background: #0a0d14;
  display: flex; align-items: center; justify-content: center;
}
.user-card .ava img { width: 100%; height: 100%; object-fit: cover; display: block; }
.user-card .nm { font-size: var(--fs-3); font-weight: 600; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-card .tg { font-size: var(--fs-1); color: var(--ink-3); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* icon-only controls share one geometry: 38px square, radius 12
   (same as .icon-btn and .nav-toggle) — no invisible odd-sized squares */
.user-card .logout { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; padding: 0; border-radius: 12px; color: var(--ink-3); transition: color var(--fast), background-color var(--fast); }
.user-card .logout svg { width: 17px; height: 17px; }
.user-card .logout:hover { color: var(--bad); background: var(--bad-soft); }
/* own glow ring — outline off, or the two stack into a double ring */
.user-card .logout:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }

/* ---------------- Main / topbar ---------------- */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 100vh; }
/* Above 760px this bar is empty and therefore gone. Its hamburger and brand
   are already display:none on desktop (the sidebar is permanent there), the
   fleet counters left, the account block moved to the account page — what
   remained was a 64px strip of nothing across every page. The mobile block
   below turns it back on, where it is the only way to open the nav. */
.topbar { display: none; }
/* maintenance warning, now that it lives in the sidebar instead of the bar */
.side-maint { padding: 0 2px 12px; }
.side-maint .status-chip { width: 100%; justify-content: center; text-align: center; }

/* What the shell says on its own. The fleet counters that lived here are gone
   (.tb-stat / .tb-num / .tb-lab went with them); the maintenance chip is all
   that is left, and it appears only while pickup is actually paused. */
.tb-state { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* still used by the slot readout on /machines/create */
.tb-sep { color: var(--ink-4); margin: 0 1px; }
.tb-maint { margin-left: 4px; }
/* No `@media (max-width: 760px) { .tb-state { display: none } }` any more. Hiding
   this area on a phone was right when it held two numbers available one tap away;
   it now holds a warning that job pickup is paused, which is worth the room. */
/* topbar chrome: the bell and the profile are ONE family — same surface,
   same border, same hover. Neither carries a shadow. */
.icon-btn {
  position: relative; color: var(--ink-2); display: flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; padding: 0; border-radius: var(--r);
  background: transparent; border: 1px solid var(--line-2); box-shadow: none;
  transition: color var(--fast), background-color var(--fast), border-color var(--fast);
}
.icon-btn:hover { color: var(--ink); background: var(--hover); border-color: var(--line); }
.icon-btn:disabled { opacity: 1; color: var(--ink-4); cursor: not-allowed; background: transparent; }
.icon-btn:disabled:hover { color: var(--ink-4); background: transparent; }
.icon-btn svg { width: 18px; height: 18px; }
.icon-btn .count {
  position: absolute; top: -6px; right: -6px; min-width: 17px; height: 17px; padding: 0 4px;
  background: var(--accent); color: var(--accent-ink); font: 700 var(--fs-1)/17px var(--mono); text-align: center;
  border-radius: 999px; border: 2px solid var(--bg);
}
/* hover tooltip for icon-only controls (keeps aria-label for a11y) */
[data-tip] { position: relative; }
/* The tooltip generates NO box until it is actually shown.
   It used to be rendered permanently at opacity:0 — and an absolutely
   positioned descendant still counts toward its containing block's scrollable
   overflow, so every `nowrap` tooltip silently widened its ancestor chain. One
   28-character tip on an IP readout in the settings aside was enough to push
   the whole page 74px past the viewport at 1440 and give the document a
   horizontal scrollbar. `content: none` at rest means no box, no overflow;
   the text is still exposed to assistive tech through the attribute itself. */
/* Placement is two independent axes, and they are held in custom properties on
   purpose. Every variant used to restate the whole `transform`, so a rule that
   only wanted to move the tooltip sideways silently reset the vertical slide as
   well — which is why a tooltip could not be both right-anchored AND pointed
   upwards. Now a variant sets only the axis it cares about. */
[data-tip]::after {
  /* Two variables, not one, and the split matters. Placement rules set only
     --tip-from (which way it slides in); the hover rule sets only --tip-y. If
     both wrote --tip-y, the placement rules — being far more specific, e.g.
     `.table-wrap tbody tr:last-child [data-tip]` at (0,3,2) against hover's
     (0,2,0) — would win on hover too, and the tooltip would pop into place
     instead of sliding. Measured that exact failure before splitting them. */
  --tip-x: -50%;                 /* centred on the host */
  --tip-from: -3px;              /* resting offset: negative = slides down */
  --tip-y: var(--tip-from);
  content: none; position: absolute; z-index: 60; left: 50%; top: calc(100% + 8px);
  transform: translateX(var(--tip-x)) translateY(var(--tip-y));
  background: var(--surface-4); color: var(--ink); border: 1px solid var(--line);
  font: 500 var(--fs-2) var(--sans); padding: 5px 9px; border-radius: 8px;
  /* wraps instead of running off the edge; wide enough to stay readable */
  width: max-content; max-width: min(260px, 70vw); white-space: normal; text-align: left;
  box-shadow: 0 8px 24px -8px rgba(0,0,0,.7);
  opacity: 0; pointer-events: none; transition: opacity 120ms ease, transform 120ms ease;
}
[data-tip]:hover::after, [data-tip]:focus-visible::after {
  content: attr(data-tip); opacity: 1; --tip-y: 0px;
}
/* The trailing action column sits at the right edge of a table that scrolls on
   overflow. A centred tooltip hangs past that edge and gives the table a
   horizontal scrollbar even when every column fits — anchor these inwards. */
.col-actions [data-tip]::after { left: auto; right: 0; --tip-x: 0px; }
/* Same problem mirrored: the first column stands at the container's left edge,
   and a wide tooltip (a full user-agent string on /settings/sessions) centred on
   a narrow cell hangs 71px past it and gets cut off. Anchor those to the left. */
.table-wrap td:first-child [data-tip]::after,
.table-wrap th:first-child [data-tip]::after { left: 0; right: auto; --tip-x: 0px; }

/* A table wrapper scrolls sideways, and `overflow-x: auto` forces the vertical
   axis to clip too — so a tooltip pointing DOWN from the last row is always cut
   off by the wrapper's bottom edge, no matter what z-index it carries. Measured
   on six pages: /jobs/bot, /machines, /license-keys, /admin/audit,
   /bots/web-api-keys and /subscriptions, every one of them the last row, cut by
   1–6px. Point them up instead; there is always a row above. */
.table-wrap tbody tr:last-child [data-tip]::after {
  top: auto; bottom: calc(100% + 8px); --tip-from: 3px;
}
/* logout sits at the sidebar bottom → point the tooltip up so it isn't clipped */
.user-card .logout[data-tip]::after { top: auto; bottom: calc(100% + 8px); --tip-from: 3px; }

/* The account block is gone from the shell entirely. Sign out — the only
   thing it offered that the Account nav item did not — now lives on the
   account page. In the sidebar it cost 56px of a nav that had to scroll. */

/* 1200px used to cap this. On a 1600+ monitor that left ~400px of empty page
   while the tables inside scrolled sideways — the panel is a work tool, it
   should spend the screen it is given. Prose and forms keep their own caps
   (.page-sub 60ch, form max-widths), so only data widens. */
.content { padding: 26px 34px 72px; max-width: 1440px; width: 100%; margin-inline: auto; }

/* Cross-page transitions. Chromium cross-fades between full navigations via the
   View Transitions API: the new page dissolves in ON TOP of the old one, so
   there is no mid-transition flash to the background, and because the sidebar is
   identical page to page only the content appears to change. Browsers without
   it keep the pageIn rise-fade below; reduced-motion turns both off.
   htmx same-document swaps run their own transition:true and are unaffected. */
@view-transition { navigation: auto; }
/* Hold the outgoing page fully opaque for the whole transition while the new
   page fades in on top of it. The old `animation: none` let the browser drop
   the outgoing snapshot at frame 0, and the incoming snapshot (fill: both) was
   still held at opacity 0 — that one-frame gap showed the bare background as a
   flicker. An explicit hold keeps something opaque on screen at every frame. */
::view-transition-old(root) { animation: vt-hold 200ms both; }
::view-transition-new(root) { animation: vt-page-in 200ms var(--ease-out) both; }
@keyframes vt-hold { from, to { opacity: 1; } }
@keyframes vt-page-in { from { opacity: 0; } to { opacity: 1; } }

/* page-enter fallback: subtle rise+fade. fill-mode backwards, NOT both — `both`
   keeps applying the final transform, making .content a containing block for
   position:fixed children (the floating save-bar would pin to the page). */
@keyframes pageIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@supports not (view-transition-name: none) {
  .content { animation: pageIn 160ms var(--ease-out) backwards; }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
  .content { animation: none; }
}

/* ---------------- UI contract ----------------
   Rules that hold everywhere, so no page has to remember them. */
/* A flex or grid child defaults to min-width:auto, which means it refuses to
   shrink below its longest word. That single default is what turns one long
   value — an IP, an email, a hardware id — into a page-wide scrollbar. */
.row > *, .panel-head > *, .list-toolbar > *, .form-actions > *,
.stat > *, .setting-row > *, .cfg-head > *, .drawer-head > * { min-width: 0; }
/* form controls never exceed their container, whatever inline width they carry */
input, select, textarea, .secret-field { max-width: 100%; }
/* a long unbroken value wraps rather than pushing its column */
.cell-flex > *, .ac-row .v, .drawer-title { overflow-wrap: anywhere; }


/* ---------------- Live viewer ---------------- */
.live-frame { position: relative; aspect-ratio: 16 / 9; width: 100%; background: #000; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; }
/* connection state, pinned to the preview it describes */
.live-state {
  position: absolute; top: 12px; left: 12px; z-index: 3;
  display: inline-flex; align-items: center; gap: 9px;
  padding: 7px 13px; border-radius: 999px;
  background: rgba(4,4,6,.78); backdrop-filter: blur(8px);
  border: 1px solid var(--line); font-size: var(--fs-3); font-weight: 600; color: var(--ink);
}
.live-state-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-3); flex: none; }
.live-state[data-state="live"] { border-color: rgba(53,214,160,.4); }
.live-state[data-state="live"] .live-state-dot { background: var(--live); box-shadow: 0 0 0 4px rgba(53,214,160,.16); }
.live-state[data-state="connecting"] .live-state-dot { background: var(--warn); animation: pulseDot 1.4s ease-in-out infinite; }
.live-state[data-state="stale"] .live-state-dot { background: var(--warn); }
.live-state[data-state="error"] { border-color: rgba(255,91,107,.4); }
.live-state[data-state="error"] .live-state-dot { background: var(--bad); }
.live-state[data-state="offline"] .live-state-dot { background: var(--ink-4); }
@keyframes pulseDot { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .live-state-dot { animation: none !important; } }

.live-frame img { width: 100%; height: 100%; object-fit: contain; display: none; }
.live-frame.has-frame img { display: block; }
.live-empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; text-align: center; padding: 24px; }
.live-frame.has-frame .live-empty { display: none; }
.live-empty h3 { margin: 6px 0 0; font-size: var(--fs-4); font-weight: 600; }
.live-empty p { margin: 0; font-size: var(--fs-2); color: var(--ink-3); max-width: 320px; line-height: 1.5; }
.live-refresh { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; border-radius: 8px; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; color: #fff; opacity: 0; transition: opacity 160ms ease; pointer-events: none; }
.live-refresh.on { opacity: 1; }
.live-refresh .btn-spin { width: 14px; height: 14px; margin: 0; }
/* Expandable "why did the capture fail" log under the live placeholder.
   min-width:0 lets this flex item honour max-width even when the message is a
   long unbroken line, so the block stays centred instead of blowing out. */
.live-log { margin: 14px auto 0; width: 100%; max-width: 340px; min-width: 0; text-align: center; }
.live-log summary { cursor: pointer; font-size: var(--fs-2); font-weight: 600; color: var(--ink-3); }
.live-log summary:hover { color: var(--ink-2); }
.live-log[open] summary { margin-bottom: 8px; }
.live-log-hint { margin: 0 0 8px !important; color: var(--ink-2) !important; font-size: var(--fs-2) !important; line-height: 1.5; max-width: none !important; }
.live-log-detail { margin: 0; padding: 8px 10px; text-align: left; background: var(--surface-3); border: 1px solid var(--line); border-radius: 8px; font: var(--fs-2)/1.5 var(--mono); color: var(--ink-3); overflow-wrap: anywhere; }

/* interactive surfaces only — a card lifts on hover if (and only if) it's clickable */
.interactive-card { transition: transform 180ms var(--ease-out), border-color 180ms var(--ease-out), box-shadow 180ms var(--ease-out); }
.interactive-card:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.16); box-shadow: 0 14px 36px rgba(0,0,0,.22); }

/* accessibility: honour reduced-motion — kill transforms, loops, page-enter, smooth-scroll */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------- Headings ---------------- */
.crumb { font-size: var(--fs-2); color: var(--ink-3); margin-bottom: 10px; }
.crumb a { color: var(--ink-2); transition: color var(--fast); }
.crumb a:hover { color: var(--accent); }
.crumb span { color: var(--ink); font-weight: 500; }
.page-title { font-size: var(--fs-7); font-weight: 700; letter-spacing: 0; margin: 0 0 20px; text-wrap: balance; }
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.page-head .page-title { margin: 0; }
.page-sub { color: var(--ink-2); font-size: var(--fs-3); margin: -12px 0 22px; max-width: 60ch; }
/* Inside .page-head the title already has margin:0, so the -12px pulled the
   subtitle up ONTO the title. Stack them normally there. */
.page-head .page-sub { margin: 6px 0 0; }

/* Administrator-only information has one visual grammar everywhere. The scope
   strip says when an otherwise shared route has widened to the whole platform;
   the compact badge marks the exact columns and controls that customers never
   receive. Blue stays distinct from live/warn/fail status colours. */
.admin-scope {
  display: flex; align-items: center; gap: 12px; min-width: 0;
  margin: 0 0 18px; padding: 10px 13px;
  background: rgba(91,141,239,.08); border: 1px solid rgba(91,141,239,.28);
  border-radius: var(--r-sm); color: var(--ink-2);
}
.admin-scope-copy { display: flex; align-items: baseline; gap: 8px; min-width: 0; font-size: var(--fs-2); }
.admin-scope-copy strong { color: var(--ink); font-size: var(--fs-3); font-weight: 600; white-space: nowrap; }
.admin-scope-copy > span { color: var(--ink-3); }
.admin-badge {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  min-height: 20px; padding: 2px 7px; border-radius: 6px;
  background: rgba(91,141,239,.14); border: 1px solid rgba(111,158,255,.34);
  color: #9DBAFF; font: 700 var(--fs-1)/1.2 var(--mono); letter-spacing: .06em;
}
.admin-badge--compact { min-height: 17px; padding: 1px 5px; font-size: 9px; }
.admin-column-heading, .admin-section-title, .admin-owner {
  display: inline-flex; align-items: center; gap: 7px; min-width: 0;
}
.admin-owner { max-width: 210px; }
.admin-owner-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-2); }
.admin-section-title { flex-wrap: wrap; }
.resource-owner { margin: 5px 0 0; color: var(--ink-3); font-size: var(--fs-2); }
.resource-owner .admin-owner { vertical-align: middle; }
.admin-only {
  border-color: rgba(91,141,239,.28) !important;
  background: linear-gradient(135deg, rgba(91,141,239,.075), transparent 46%), var(--surface-2) !important;
}
.admin-control { display: inline-flex; align-items: center; gap: 8px; }
@media (max-width: 640px) {
  .admin-scope { align-items: flex-start; }
  .admin-scope-copy { flex-direction: column; gap: 2px; }
  .admin-scope-copy strong { white-space: normal; }
}

/* Persistent identity boundary while an administrator is using a customer's
   account. It remains pinned as the page scrolls and carries the only route
   that can cross back into the administrator identity. */
.impersonation-notice {
  position: sticky; top: 0; z-index: 19;
  display: flex; align-items: center; gap: 11px; min-width: 0;
  margin: 0; padding: 10px 34px;
  background: color-mix(in srgb, var(--warn-soft) 92%, var(--bg));
  border-bottom: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
  color: var(--warn); box-shadow: 0 8px 24px rgba(0,0,0,.18);
}
.impersonation-notice > svg { width: 18px; height: 18px; flex: none; }
.impersonation-copy { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 1; }
.impersonation-copy strong { color: var(--ink); font-size: var(--fs-3); white-space: nowrap; }
.impersonation-copy > span { color: var(--ink-2); font-size: var(--fs-2); }
.impersonation-notice form { flex: none; }
@media (max-width: 760px) {
  .impersonation-notice { top: 59px; padding: 10px 14px; align-items: flex-start; }
  .impersonation-copy { flex-direction: column; gap: 2px; }
  .impersonation-copy strong { white-space: normal; }
}
@media (max-width: 520px) {
  .impersonation-notice { flex-wrap: wrap; }
  .impersonation-notice form { width: 100%; padding-left: 29px; }
  .impersonation-notice form button { width: 100%; }
}

/* ---------------- Panels (real containers only) ---------------- */
.card, .panel {
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-lg); padding: 20px; margin-bottom: 20px;
}
.card-title, .panel-title { font-size: var(--fs-4); font-weight: 600; letter-spacing: -.005em; margin: 0 0 4px; color: var(--ink); }
.card-sub, .panel-sub { font-size: var(--fs-2); color: var(--ink-3); margin: 0 0 16px; }
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.panel-head > :first-child { margin-bottom: 0; }
.panel-head .panel-title, .panel-head .card-sub { margin: 0; }

/* ---------------- Stat tiles (instrument readouts, not fat cards) --- */
.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 24px; }
.stat {
  background: var(--surface-2); border: 1px solid var(--line-2); border-radius: var(--r);
  padding: 16px 17px; position: relative; overflow: hidden;
  transition: border-color 180ms var(--ease-out), background-color 180ms var(--ease-out);
  /* A tile with a sparkline pushed its caption up under the number while its
     neighbours kept theirs at the foot — four cards in a row, captions on two
     different lines. The caption is the tile's last line, always. */
  display: flex; flex-direction: column;
}
/* hover is a whisper: the border wakes up, nothing moves, nothing casts a shadow */
a.stat:hover { border-color: var(--line); background: var(--surface-3); }
.stat .k { font-size: var(--fs-2); font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-3); display: flex; align-items: center; gap: 7px; }
/* readout: the number is the loudest thing in the tile — full ink, tight, tabular */
.stat .v { font: 700 var(--fs-8)/1 var(--mono); letter-spacing: -.01em; margin-top: 13px; font-variant-numeric: tabular-nums; color: var(--ink); }
.stat .v small { font-size: var(--fs-4); color: var(--ink-3); font-weight: 600; }
/* "no measurement" is not the number zero — it reads quieter and never invites
   the tile to be compared against one that has real data */
.stat .v .v-none { color: var(--ink-4); font-weight: 600; }
/* margin-top:auto, not a fixed gap — the caption is the tile's last line even in
   the tile that carries a sparkline after it */
.stat .d { font-size: var(--fs-2); margin-top: auto; padding-top: 8px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.stat .d.up { color: var(--live); } .stat .d.down { color: var(--bad); }

/* Countdown readout — the one figure on the dashboard that must never wrap and
   never clip. It sizes itself to the tile instead of the other way round, and
   the word "days" lives in the caption below, not inside the number. */
.stat .v.cd { display: flex; align-items: baseline; gap: 7px; white-space: nowrap; min-width: 0; }
.cd-d { display: inline-flex; align-items: baseline; gap: 1px; }
.cd-d b { font: inherit; }
.cd-d small { font-size: .58em; font-weight: 600; color: var(--ink-3); }
.cd-t { font: inherit; font-variant-numeric: tabular-nums; }
/* clamp so the widest form ("7d 23:59:59") still fits the narrowest tile */
.stat { container-type: inline-size; }
.stat .v.cd { font-size: clamp(16px, calc(8cqi - 1px), 24px); }  /* fluid, but both ends land on the ladder */
/* on a phone the timer takes the full row rather than half of one — a clock
   squeezed into a 112px column is the reason it was losing its last digits */
@media (max-width: 620px) {
  .stat-grid .stat:has(.cd) { grid-column: 1 / -1; }
}

/* ---------------- Tables (one DataTable, every page) ---------------- */
/* overflow-x:auto forces overflow-y to compute to auto → a stray vertical
   scrollbar. overflow-y:hidden stops that; padding-bottom then gives the last
   row breathing room AND a gutter so a horizontal scrollbar never clips it.
   (No negative bottom margin — that used to pull the panel edge over the row.) */
.table-wrap { overflow-x: auto; overflow-y: hidden; margin: 0 -20px; padding-bottom: 14px; border-radius: 12px; }
.table-wrap table { padding: 0 20px; }
/* The wrapper is the query container, so a table reacts to the width it actually
   gets — not to the width of the window. The two differ by the sidebar plus the
   page gutters (~300px), and by however much an aside column takes, which is why
   a viewport-keyed breakpoint left rows in table form at 1024 and 768 with their
   action buttons parked outside the visible area. */
.table-wrap.as-cards { container-type: inline-size; container-name: rows; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 11px 14px; border-bottom: 1px solid var(--line-2); white-space: nowrap; vertical-align: middle; }
tr:last-child td { border-bottom: 0; }
th { color: var(--ink-3); font-weight: 600; font-size: var(--fs-2); text-transform: uppercase; letter-spacing: .04em; }
tbody tr, table tr { transition: background-color var(--fast); }
tbody tr:hover td { background: rgba(255,255,255,.03); }
td .mono, td.mono, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--ink-3); }
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* Opt-in figure column: header and digits both pin right, so the numbers line up
   under each other and under the end of their label. Left-aligned digits below a
   long header ("GAMES (24H)" over a lone "0") read as belonging to nothing.
   Opt-in rather than applied to every .num — other tables pair left-aligned
   headers with their numbers and would end up mismatched. */
th.col-num, td.col-num { text-align: right; }

/* Two counts sharing one column ("0 / 197"). Right-aligning the whole string
   would put the slash in a different place on every row, so each side gets a
   fixed box and the slash becomes the pivot everything lines up on — heading
   included, since it carries the same separator. */
th.col-pair, td.col-pair { text-align: center; }
/* one box, so the phone's card layout treats the pair as a single value
   instead of flinging its three parts to opposite edges of the row */
.pair { display: inline-block; white-space: nowrap; }
.pair-a, .pair-b { display: inline-block; min-width: 3ch; }
.pair-a { text-align: right; }
.pair-b { text-align: left; color: var(--ink-3); }
.pair-s { color: var(--ink-4); margin: 0 5px; }

/* column sizing: one column absorbs slack, the rest pack to content.
   .col-grow → primary text column takes leftover width (dense, no lopsided gaps)
   .col-actions → trailing icon column shrinks and pins its content right */
th.col-grow, td.col-grow { width: 100%; }
th.col-actions, td.col-actions { width: 1%; text-align: right; white-space: nowrap; }
th.col-owner, td.col-owner { width: 1%; min-width: 150px; }
.row-actions { display: inline-flex; gap: 6px; justify-content: flex-end; align-items: center; }
/* A destructive control must not sit shoulder to shoulder with an ordinary one —
   put a gap and a hairline between them so a mis-aimed click lands on nothing. */
.row-actions .ra-danger { margin-left: 12px; padding-left: 12px; position: relative; }
.row-actions .ra-danger::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 1px; height: 20px; background: var(--line-2);
}

/* level readout: the figures lead, the bar illustrates */
.lvl { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 4px; min-width: 0; }
.lvl-head { display: inline-flex; align-items: baseline; gap: 8px; }
.lvl-head b { font-size: var(--fs-2); font-weight: 600; color: var(--ink); }
.lvl-pct { font: 600 var(--fs-2) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-3); }
.lvl-xp { font-size: var(--fs-2); color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
.lvl-none { color: var(--ink-4); font-style: normal; }
/* A job row swaps its buttons when it changes state (cancel → run again +
   delete). Reserve room for the widest set so the columns never shift. */
.row-actions.ra-2 { min-width: 86px; }

/* Danger, once per row, is a warning; danger repeated down every row is
   wallpaper. /license-keys showed five tinted Revoke buttons and /admin/invites
   six Delete — together the loudest thing on either page, louder than the one
   button that actually creates something. Inside a row the destructive control
   keeps its red word and drops the red seat; the tint returns on hover and
   focus, where the pointer already is, and the confirm dialog still carries the
   full weight. Header and danger-zone buttons are untouched. */
.row-actions .btn-danger,
td .btn-danger { background: transparent; border-color: var(--line-2); }
.row-actions .btn-danger:hover, .row-actions .btn-danger:focus-visible,
td .btn-danger:hover, td .btn-danger:focus-visible {
  background: var(--bad-soft); border-color: rgba(255,91,107,.25);
}

/* Compact marker for an auto-queued job — a full "auto" pill cost the jobs
   table ~40px of width it does not have. */
.auto-mark { display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; margin-left: 6px; border-radius: 5px;
  background: var(--surface-3); border: 1px solid var(--line-2);
  font: 700 var(--fs-1)/1 var(--sans); color: var(--ink-3); vertical-align: middle; }

/* Below a roomy desktop the jobs table drops its least-critical column instead
   of making the whole table scroll sideways. */
@media (max-width: 1400px) { .md-hide { display: none; } }
/* One tier up: a column that is nice to have but must not starve the row's
   primary text column. Below a wide monitor it steps aside. */
@media (max-width: 1600px) { .lg-hide { display: none; } }
/* truncate an over-long inline value inside a cell (tokens, urls) */
.cell-trunc { display: inline-block; max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
/* a flexible cell that GROWS to fill leftover width but truncates when squeezed
   (shows the full value on wide screens, ellipsis in a narrow column) */
td.cell-flex { width: 100%; max-width: 0; }
td.cell-flex > * { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
/* an avatar + name pair stays a flex row while it truncates (the generic rule
   above would turn it into an inline-block and drop the avatar out of line) */
/* the rule above forces inline-block on every child; these two need to lay their
   own contents out side by side, so they opt back in to flex */
td.cell-flex > .name-cell, td.cell-flex > .drop-item { display: inline-flex; }

/* .dot* removed: every one of the 13 sat immediately before text that already
   said the same thing ("● Machines Online", "● claimed"), so it carried no
   information and pulsed for attention it had not earned. */

/* ---------------- Pills / badges ---------------- */
.pill-ok, .pill-fail, .pill-warn, .pill-mute, .badge, .status-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: 999px; font-size: var(--fs-2); font-weight: 600; line-height: 1.4;
  border: 1px solid transparent;
}
.pill-ok  { background: var(--live-soft); color: var(--live); border-color: rgba(53,214,160,.18); }
.pill-fail{ background: var(--bad-soft);  color: var(--bad);  border-color: rgba(255,91,107,.18); }
.pill-warn{ background: var(--warn-soft); color: var(--warn); border-color: rgba(245,178,62,.18); }
.pill-mute{ background: transparent; color: var(--ink-3); border-color: var(--line-2); }
/* status chips read stronger with a leading colour dot (inherits the pill's colour) */
.pill-ok::before, .pill-fail::before, .pill-warn::before, .pill-mute::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: none;
}
.pill-mute::before { opacity: .7; }
/* Status column: one width for every chip so the column reads as a tidy stack
   instead of pills that jump wider on longer words ("offline" vs "active"). */
#bots-table td[data-label="Status"] .status-chip { min-width: 86px; justify-content: center; }

/* Status as an actions menu — a chip that is also a button (payments history).
   Native <details>; the popover is position:fixed (set by JS) so it isn't clipped
   by the table-wrap's overflow. Colour modifiers mirror the pill-warn/pill-mute chips. */
.status-menu { position: relative; display: inline-block; }
.status-menu > summary {
  list-style: none; cursor: pointer; user-select: none;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 7px 3px 9px; border-radius: 999px;
  font-size: var(--fs-2); font-weight: 600; line-height: 1.4; border: 1px solid transparent;
  transition: background var(--fast) var(--ease-out), border-color var(--fast) var(--ease-out);
}
.status-menu > summary::-webkit-details-marker { display: none; }
.status-menu > summary::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: none; }
.status-menu > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.status-menu .chev { transition: transform var(--fast) var(--ease-out); opacity: .7; margin-left: 1px; }
.status-menu[open] > summary .chev { transform: rotate(180deg); }
.status-menu--warn > summary { background: var(--warn-soft); color: var(--warn); border-color: rgba(245,178,62,.18); }
.status-menu--warn > summary:hover, .status-menu--warn[open] > summary { border-color: rgba(245,178,62,.5); }
.status-menu--mute > summary { background: transparent; color: var(--ink-3); border-color: var(--line-2); }
.status-menu--mute > summary:hover, .status-menu--mute[open] > summary { border-color: var(--line); color: var(--ink-2); }
.status-menu-pop {
  position: fixed; z-index: 60; min-width: 180px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 5px; display: grid; gap: 2px;
  box-shadow: 0 14px 34px -10px rgba(0,0,0,.7), 0 0 0 1px rgba(0,0,0,.35);
  animation: status-pop var(--fast) var(--ease-out);
}
@keyframes status-pop { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.status-menu-item {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left; cursor: pointer;
  padding: 7px 9px; border-radius: var(--r-xs); border: 0; background: none;
  font: 500 var(--fs-3)/1.4 var(--sans); color: var(--ink-2);
  transition: background var(--fast) var(--ease-out), color var(--fast) var(--ease-out);
}
.status-menu-item:hover, .status-menu-item:focus-visible { background: var(--surface-3); color: var(--ink); outline: none; }
.status-menu-item.is-primary { color: var(--ink); }
.status-menu-item svg { flex: none; opacity: .85; }
@media (prefers-reduced-motion: reduce) { .status-menu-pop { animation: none; } .status-menu .chev { transition: none; } }

code, .code {
  font: var(--fs-2) var(--mono); background: var(--surface-3); border: 1px solid var(--line-2);
  padding: 2px 8px; border-radius: 7px; color: var(--ink-2); font-variant-numeric: tabular-nums;
}

/* ---------------- Forms ---------------- */
label.field { display: block; margin-bottom: 17px; }
/* New-bot credentials use <div class="field"> (not <label>), so they miss the
   rule above. With the per-field help lines gone, give them the same rhythm:
   label sits 7px over its input, 17px between one field and the next. */
#bot-form .field { margin-bottom: 17px; }
#bot-form [data-cm-secrets] .field:last-child { margin-bottom: 0; }
label.field .lab, .field-lab { display: block; font-size: var(--fs-2); font-weight: 600; margin-bottom: 7px; color: var(--ink); }
label.field .lab .req, .req { color: var(--accent); }
label.field .help, .field-help { display: block; font-size: var(--fs-3); line-height: 1.5; letter-spacing: .01em; color: var(--ink-3); margin-top: 7px; }
input[type=text], input[type=email], input[type=password], input[type=number], input[type=search], select, textarea {
  background: var(--surface-3); border: 1px solid var(--line); color: var(--ink);
  min-height: 40px; padding: 9px 13px; border-radius: var(--r-sm); width: 100%; font: 400 var(--fs-4) var(--sans);
  outline: none; transition: border-color var(--fast), box-shadow var(--fast), background-color var(--fast);
}
textarea { min-height: 88px; line-height: 1.6; }
/* placeholders must be readable as examples, but must not read as a filled value */
input::placeholder, textarea::placeholder { color: var(--ink-4); opacity: 1; }
input:hover:not(:focus), select:hover:not(:focus), textarea:hover:not(:focus) {
  border-color: rgba(255,255,255,.18); background: var(--surface-4);
}
input:focus, select:focus, textarea:focus {
  border-color: var(--accent); background: var(--surface-3);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
/* Time input: the browser's own control renders its digits at whatever contrast
   it likes, so pin them; the zone sits beside the field rather than only in the
   label, where it was easy to read one schedule and assume another. */
.time-field { display: flex; align-items: center; gap: 10px; }
.time-field input[type=time] { flex: 1; min-width: 0; font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); font-size: var(--fs-4); }
.time-field input[type=time]::-webkit-calendar-picker-indicator { filter: invert(1); opacity: .55; cursor: pointer; }
.time-field input[type=time]::-webkit-calendar-picker-indicator:hover { opacity: .9; }
.time-tz {
  flex: none; font: 600 var(--fs-2) var(--mono); letter-spacing: .04em; color: var(--ink-3);
  padding: 5px 9px; border: 1px solid var(--line-2); border-radius: var(--r-xs); background: var(--surface-2);
}

/* clean number field — no spinners */
input[type=number] { -moz-appearance: textfield; }
input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.inline-form { display: inline; }
.row { display: flex; gap: 11px; align-items: center; }
input[type=checkbox] { accent-color: var(--accent); }

/* segmented control (radio group) */
.segmented { display: inline-flex; background: var(--surface-3); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 3px; gap: 3px; }
.segmented input { position: absolute; opacity: 0; width: 0; height: 0; }
.segmented label {
  display: flex; flex-direction: column; align-items: center; gap: 1px; margin: 0;
  padding: 6px 16px; border-radius: 8px; font-size: var(--fs-3); font-weight: 600; color: var(--ink-2);
  cursor: pointer; transition: background-color var(--fast), color var(--fast); white-space: nowrap;
}
.segmented label small { font: 500 var(--fs-1) var(--sans); color: var(--ink-3); text-transform: none; letter-spacing: 0; }
/* wide variant — options share the row and read as a real choice, not a hint */
.segmented.seg-wide { display: flex; width: 100%; }
.segmented.seg-wide label { flex: 1; padding: 9px 14px; text-align: center; white-space: normal; }
.segmented.seg-wide label small { color: var(--ink-3); }
.segmented.seg-wide input:checked + label small { color: rgba(10,10,12,.66); }
/* explanation shown where a group of fields used to be */
.field-note {
  margin: 0; padding: 12px 14px; border: 1px dashed var(--line); border-radius: var(--r-sm);
  background: var(--surface-3); color: var(--ink-3); font-size: var(--fs-2); line-height: 1.55;
}
/* stacked variant — full-width vertical options (e.g. machine-job action) */
.segmented.seg-stack { display: flex; flex-direction: column; align-items: stretch; width: 100%; gap: 3px; }
.segmented.seg-stack label { flex-direction: row; justify-content: flex-start; padding: 9px 13px; }
/* button variant — the same control driving panes instead of a radio value */
/* a tab is a control like any other: 40px on a mouse, 44 on a finger */
.segmented .seg-btn {
  background: transparent; border: 0; box-shadow: none; min-height: 40px;
  padding: 8px 16px; border-radius: 8px; font: 600 var(--fs-3) var(--sans); color: var(--ink-2);
  transition: background-color var(--fast), color var(--fast); white-space: nowrap;
}
.segmented .seg-btn:hover { background: var(--hover); color: var(--ink); }
.segmented .seg-btn[aria-selected="true"] { background: var(--accent); color: var(--accent-ink); }
.segmented .seg-btn[aria-selected="true"]:hover { background: var(--accent-h); }
@media (max-width: 760px) { .segmented .seg-btn { min-height: 44px; } }
.segmented input:checked + label { background: var(--accent); color: var(--accent-ink); }
.segmented input:checked + label small { color: rgba(10,10,12,.72); }
.segmented input:focus-visible + label { box-shadow: 0 0 0 3px var(--accent-glow); }
/* Disabled is "not yet", not "broken". At opacity .45 the label fell under 3:1
   and the control read as a rendering fault; give it a real muted surface with
   legible text instead, and never let it move on hover. */
button:disabled, .btn:disabled, button[aria-disabled="true"] {
  cursor: not-allowed; box-shadow: none; opacity: 1;
  background: var(--surface-3); color: var(--ink-3); border-color: var(--line-2);
}
.btn-ghost:disabled, .btn-danger:disabled { background: transparent; color: var(--ink-3); border-color: var(--line-2); }
button:disabled:hover, .btn:disabled:hover { background: var(--surface-3); transform: none; }
.btn-ghost:disabled:hover, .btn-danger:disabled:hover { background: transparent; transform: none; }
button:disabled:active, .btn:disabled:active { transform: none; }
input:disabled, select:disabled, textarea:disabled {
  background: var(--surface-2); color: var(--ink-3); border-color: var(--line-2); cursor: not-allowed;
}
input:disabled::placeholder, textarea:disabled::placeholder { color: var(--ink-4); }
/* A disabled button owes the reader a reason. This is the one place it goes:
   beside the control, in the flow, not a tooltip you have to discover on a
   button you cannot even hover on a phone. Hidden until something fills it. */
.btn-reason { font-size: var(--fs-2); color: var(--ink-3); line-height: 1.45; }
.btn-reason:empty { display: none; }
/* an action and its reason travel together and wrap as one unit */
.action-with-reason { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }

/* field-level validation for main-app forms */
.field.invalid input, .field.invalid select { border-color: var(--bad); box-shadow: 0 0 0 3px rgba(255,91,107,.14); }
.field-err { display: none; font-size: var(--fs-2); color: var(--bad); margin-top: 6px; }
.field.invalid .field-err { display: block; }
.field.invalid .field-help { display: none; }

/* proxy add-form: smooth reveal + test-connection feedback */
.proxy-reveal { overflow: hidden; max-height: 0; opacity: 0; visibility: hidden; pointer-events: none;
  transition: max-height 180ms var(--ease-out), opacity 140ms ease, visibility 180ms; }
.proxy-reveal.open { max-height: 720px; opacity: 1; visibility: visible; pointer-events: auto; }
.pf-toggle { width: 100%; justify-content: space-between; }
.pf-toggle .chev { transition: transform 160ms var(--ease-out); }
.pf-toggle[aria-expanded="true"] .chev { transform: rotate(180deg); }
.field-split { display: grid; grid-template-columns: 1fr 96px; gap: 11px; align-items: start; }
.btn-test { background: transparent; color: var(--ink); border: 1px solid var(--line); box-shadow: none; }
.btn-test:hover { background: var(--surface-3); border-color: rgba(255,255,255,.2); }
.test-result { display: none; align-items: center; gap: 8px; font-size: var(--fs-2); padding: 8px 11px; border-radius: var(--r-sm); margin-top: 10px; }
.test-result.show { display: flex; }
.test-result.ok  { background: var(--live-soft); color: var(--live); border: 1px solid rgba(53,214,160,.24); }
.test-result.err { background: var(--bad-soft);  color: var(--bad);  border: 1px solid rgba(255,91,107,.24); }
.test-result.load{ background: var(--surface-3); color: var(--ink-2); border: 1px solid var(--line-2); }

/* job progress bar */
.progress-bar { height: 6px; background: var(--surface-4); border-radius: 3px; overflow: hidden; margin-top: 7px; max-width: 160px; }
.progress-fill { height: 100%; background: var(--accent); border-radius: 3px; transition: width 180ms ease; }

/* list search toolbar (bots / machines) */
.list-toolbar { display: flex; align-items: center; gap: 12px; padding: 2px 0 16px; }
/* audit filters wrap onto as many rows as they need instead of squeezing */
/* Six filters wrapping into three ragged rows on a 1920 screen with room to
   spare: flex-wrap packs by content width, so a wide <select> pushed the next
   control to a new line and left the rest of the row empty. A grid gives every
   control a declared share and drops to two rows, then one column, on purpose. */
/* Two class names, not one: `.list-toolbar { display: flex }` is declared
   further down this file, and at equal specificity the later rule wins — which
   is exactly how this grid silently reverted to a wrapping flex row. */
.list-toolbar.audit-toolbar {
  display: grid; gap: 10px; align-items: center; flex-wrap: nowrap;
  grid-template-columns: minmax(220px, 1.6fr) repeat(3, minmax(120px, 1fr)) minmax(118px, .7fr) auto;
}
.list-toolbar.audit-toolbar > .search-field { max-width: none; flex: none; }
.list-toolbar.audit-toolbar select,
.list-toolbar.audit-toolbar input[type=text] { min-height: 40px; width: 100%; max-width: none; }
/* Apply + Reset travel together in the last cell */
.list-toolbar.audit-toolbar .tb-actions { display: flex; gap: 8px; }
/* Mid tier: the sidebar is still on screen below 1280, so the working width is
   ~260px narrower than the viewport. Three filter columns plus an actions cell
   did not fit at 768 and pushed the page 23px sideways. Two columns do. */
@media (max-width: 1280px) {
  .list-toolbar.audit-toolbar { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
  .list-toolbar.audit-toolbar > .search-field,
  .list-toolbar.audit-toolbar .tb-actions { grid-column: 1 / -1; }
}
@media (max-width: 760px) {
  .list-toolbar.audit-toolbar { grid-template-columns: 1fr; }
  .list-toolbar.audit-toolbar > .search-field { grid-column: auto; }
  .list-toolbar.audit-toolbar .tb-actions > * { flex: 1; }
}
}
.filter-summary { margin: 0 0 14px; font-size: var(--fs-2); color: var(--ink-3); font-variant-numeric: tabular-nums; }
/* An email is a known, bounded thing — give it room to print rather than making
   it compete for slack and lose.
   `width: 1%` alone shrinks a column to its MINIMUM content width, and with
   overflow-wrap:anywhere that minimum is one character: the cell went from
   "ad…" to a four-line "admi/n@el/io.de/v". The declared min-width is what
   actually reserves room for a whole address. */
th.col-user, td.col-user { width: 1%; min-width: 210px; }
.user-cell { display: inline-block; max-width: 100%; overflow-wrap: anywhere; cursor: pointer; }
.user-cell:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--focus); border-radius: var(--r-xs); }
@media (max-width: 1400px) { th.col-user, td.col-user { min-width: 160px; } }
/* An identifier the reader copies — an invite code, a short key — prints whole
   or it is useless. /admin/invites had TWO col-grow columns splitting the
   surplus, so the code was capped at 150px and rendered "ELIO-INV-ATHZ8-…"
   beside 550px of empty Label. Sizing to content and taking no slack, it fits. */
th.col-code, td.col-code { width: 1%; white-space: nowrap; }
td.col-code > code { font-size: var(--fs-2); }
.search-field { position: relative; display: flex; align-items: center; flex: 1; max-width: 340px; }
.search-field svg { position: absolute; left: 12px; color: var(--ink-3); pointer-events: none; }
.search-field input { padding-left: 34px; min-height: 38px; }

/* row name → detail link */
.row-link { font-weight: 500; transition: color 140ms ease; }
.row-link:hover { color: var(--accent); }

/* spinner state for icon buttons (sync) */
.btn-icon.spinning svg { animation: spin .7s linear infinite; }

/* ---------------- Drawer ----------------
   Holds the operations a dense table row cannot: they get room, labels and
   consequences instead of being crushed into a trailing cell. */
.drawer { position: fixed; inset: 0; z-index: 150; display: flex; justify-content: flex-end; }
.drawer::before {
  content: ""; position: absolute; inset: 0; background: rgba(0,0,0,.62);
  opacity: 0; transition: opacity 180ms var(--ease-out);
}
.drawer.show::before { opacity: 1; }
.drawer-panel {
  position: relative; width: min(460px, 100%); height: 100%; overflow-y: auto;
  background: var(--surface-2); border-left: 1px solid var(--line);
  transform: translateX(100%); transition: transform 180ms var(--ease-out);
  display: flex; flex-direction: column;
}
.drawer.show .drawer-panel { transform: none; }
.drawer-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 20px 22px; border-bottom: 1px solid var(--line-2);
  position: sticky; top: 0; background: var(--surface-2); z-index: 1;
}
.drawer-title { font-size: var(--fs-5); font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.drawer-sub { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 7px; font-size: var(--fs-2); color: var(--ink-3); }
.drawer-body { padding: 6px 22px 28px; }
.drawer-section { padding: 20px 0; border-top: 1px solid var(--line-2); }
.drawer-section:first-of-type { border-top: 0; }
.drawer-section > .lab { display: block; font-size: var(--fs-3); font-weight: 600; color: var(--ink); }
.drawer-section > .help { display: block; font-size: var(--fs-2); line-height: 1.55; color: var(--ink-3); margin-top: 5px; }
.drawer-section > .help b { color: var(--ink-2); font-weight: 600; }
.drawer-section.danger-zone { border-top-color: rgba(255,91,107,.24); background: none; }
.drawer-section .form-actions { margin-top: 16px; padding-top: 14px; }
body.drawer-open { overflow: hidden; }
@media (max-width: 520px) {
  .drawer-panel { width: 100%; border-left: 0; }
  .drawer-head, .drawer-body { padding-inline: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .drawer-panel { transition: none; }
  .drawer::before { transition: none; }
}

/* account recovery warning — a consequence, not a field hint */
.recovery-note { display: flex; gap: 14px; align-items: flex-start; border-color: rgba(245,178,62,.3); }
.recovery-note .rn-ic { flex: none; color: var(--warn); display: flex; }
.recovery-note .rn-ic svg { width: 20px; height: 20px; }
.recovery-note .rn-tx { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.recovery-note b { font-size: var(--fs-3); font-weight: 600; color: var(--ink); }
.recovery-note span { font-size: var(--fs-2); line-height: 1.6; color: var(--ink-2); }

/* a whole card that is one link — the destination is the point, so it gets the
   affordance of a card rather than the weight of a footnote */
.link-card {
  display: flex; align-items: center; gap: 14px; text-decoration: none;
  transition: background-color var(--fast), border-color var(--fast);
}
.link-card:hover { background: var(--surface-3); border-color: var(--line); }
.link-card .lc-ic { flex: none; display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: var(--r-sm); background: var(--surface-3); color: var(--ink-2); }
.link-card:hover .lc-ic { color: var(--accent); }
.link-card .lc-ic svg { width: 19px; height: 19px; }
.link-card .lc-tx { display: flex; flex-direction: column; gap: 3px; min-width: 0; margin-right: auto; }
.link-card .lc-title { font-size: var(--fs-4); font-weight: 600; color: var(--ink); }
.link-card .lc-sub { font-size: var(--fs-2); line-height: 1.5; color: var(--ink-3); }
.link-card .lc-count { flex: none; font: 700 var(--fs-6) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); }
.link-card .lc-go { flex: none; width: 16px; height: 16px; color: var(--ink-4); }
@media (max-width: 620px) {
  .link-card .lc-count { font-size: var(--fs-5); }
}

/* Shown where a chart would be if there were anything to plot. Deliberately a
   fraction of the chart's height — an empty 190px canvas is dead space that
   also implies the data is real and flat. */
.chart-empty {
  display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
  padding: 4px 0 2px;
}
.chart-empty p { margin: 0; font-size: var(--fs-3); line-height: 1.55; color: var(--ink-3); max-width: 52ch; }
.chart-empty .panel-title { margin: 0; }

/* SVG area chart — 7-day XP (/stats) */
.xpchart { padding: 6px 2px 2px; }
.xpchart-with-axis { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 10px; }
.xpchart-y-axis {
  height: 190px; padding: 7px 0; display: flex; flex-direction: column;
  align-items: flex-end; justify-content: space-between; white-space: nowrap;
  font: 500 var(--fs-1)/14px var(--mono); color: var(--ink-3); font-variant-numeric: tabular-nums;
}
.xpchart-canvas { min-width: 0; }
.xpchart-svg { display: block; width: 100%; height: 190px; }
.xpchart-grid { fill: none; stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke;
  stroke-dasharray: 2 5; opacity: .7; }
.xpchart-area { fill: var(--live-soft); }
.xpchart-line { fill: none; stroke: var(--live); stroke-width: 1.9; vector-effect: non-scaling-stroke;
  stroke-linejoin: round; stroke-linecap: round; filter: drop-shadow(0 1px 4px rgba(53,214,160,.22)); }
/* The rebuilt path carries pathLength="1" — draw it on once. Untouched fallback
   polylines keep a solid stroke. */
.xpchart-svg[data-area] .xpchart-line { stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: xp-draw .8s ease-out forwards; }
@keyframes xp-draw { to { stroke-dashoffset: 0; } }

/* Hover scrubbing overlay: crosshair + snapping dot + a tooltip pinned to the
   top of the plot. Sits in .xpchart-plot, a tight unpadded wrapper around the
   SVG, so overlay coordinates match the curve and axis labels exactly. HTML (not
   SVG) so the round dot can't get squashed by the non-uniform viewBox. */
.xpchart-plot { position: relative; }
.xpchart-mark { position: absolute; inset: 0; pointer-events: none; }
.xpchart-cross { position: absolute; top: 0; bottom: 0; width: 1px; transform: translateX(-50%);
  background: linear-gradient(var(--live), rgba(53,214,160,.05)); opacity: .5; z-index: 1; }
.xpchart-hover-dot { position: absolute; width: 9px; height: 9px; border-radius: 50%;
  background: var(--surface-2); border: 2px solid var(--live); transform: translate(-50%, -50%);
  box-shadow: 0 0 0 3px rgba(53,214,160,.14), 0 0 8px var(--live-glow); z-index: 2; }
.xpchart-tip { position: absolute; top: 4px; z-index: 3; pointer-events: none; white-space: nowrap;
  transform: translateX(-50%); display: flex; align-items: baseline; gap: 7px;
  padding: 4px 9px; border-radius: 7px; background: var(--surface-3); border: 1px solid var(--line-2);
  box-shadow: 0 8px 22px rgba(0,0,0,.5); }
.xpchart-tip b { font: 700 var(--fs-1) var(--mono); color: var(--ink); font-variant-numeric: tabular-nums; }
.xpchart-tip span { font-size: var(--fs-2); color: var(--ink-3); }
@media (prefers-reduced-motion: reduce) {
  .xpchart-svg[data-area] .xpchart-line { animation: none; stroke-dashoffset: 0; }
}
.xpchart-hit { fill: transparent; }
.xpchart-bar { fill: var(--accent); opacity: .55; }
.xpchart-bars .xpchart-hit:hover { fill: rgba(255,255,255,.06); }
.xpchart-labels { display: flex; margin-top: 8px; }
.xpchart-lab { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 3px; }
.xpchart-lab b { font: 600 var(--fs-1) var(--mono); color: var(--ink-2); font-variant-numeric: tabular-nums;
  transition: color .2s; }
.xpchart-lab b.is-zero { color: var(--ink-3); opacity: .38; font-weight: 500; }
.xpchart-lab b.is-peak { color: var(--live); }
.xpchart-lab span { font-size: var(--fs-2); color: var(--ink-3); letter-spacing: 0; }

/* map multi-select chips (job creation) */
.map-grid { display: flex; flex-wrap: wrap; gap: 7px; }
.map-chip { position: relative; margin: 0; cursor: pointer; }
/* same idiom as .switch: invisible, not sizeless */
.map-chip input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer; z-index: 1; }
.map-chip span { display: inline-block; padding: 6px 12px; border-radius: 999px; font-size: var(--fs-2); font-weight: 500;
  background: var(--surface-3); border: 1px solid var(--line); color: var(--ink-2);
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease; }
.map-chip:hover span { border-color: var(--ink-3); }
.map-chip input:checked + span { background: var(--accent-soft); border-color: var(--accent-line); color: #fff; }
.map-chip input:focus-visible + span { box-shadow: 0 0 0 3px var(--accent-glow); }

/* dashboard bot-status summary */
/* one pill style: no fill, one hairline, a small colour dot for the state.
   These are readouts sitting in a row — they must not read as four buttons. */
.botsum { display: flex; flex-wrap: wrap; gap: 8px; }
.botsum-item { display: inline-flex; align-items: center; gap: 8px; padding: 8px 13px; border-radius: var(--r-sm);
  background: transparent; border: 1px solid var(--line-2); font-size: var(--fs-2); color: var(--ink-3);
  transition: background-color 140ms ease, border-color 140ms ease; }
.botsum-item:hover { background: var(--hover); border-color: var(--line); }
.botsum-item b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }

/* toggle rows (settings) */
.toggle-row { display: flex; align-items: flex-start; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--line-2); }
.toggle-row.last { border-bottom: 0; padding-bottom: 0; }
.toggle-row input[type=checkbox] { width: auto; margin-top: 2px; flex-shrink: 0; }
.toggle-row .lab { display: block; font-size: var(--fs-3); font-weight: 500; }
.toggle-row .help { display: block; font-size: var(--fs-2); color: var(--ink-3); margin-top: 3px; }

/* ---------------- Buttons ---------------- */
button, .btn {
  background: var(--accent); border: 1px solid transparent; color: var(--accent-ink);
  min-height: 40px; padding: 10px 17px; border-radius: var(--r-sm); font: 600 var(--fs-3) var(--sans);
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 8px; line-height: 1;
  /* no accent drop-glow: it leaked onto every <button> that only restyled the
     background (.icon-btn, .profile-btn) and lit them with a blue halo */
  box-shadow: 0 1px 0 rgba(255,255,255,.08) inset;
  transition: background-color var(--fast), transform var(--fast), box-shadow var(--fast), border-color var(--fast);
}
button:hover, .btn:hover { background: var(--accent-h); }
button:active, .btn:active { transform: scale(.975); }
/* radius + transition apply to the WHOLE button family so they hold on <a>
   too (ghost/sm anchors don't match the `button, .btn` selector otherwise → radius 0) */
.btn, .btn-ghost, .btn-danger, .btn-sm, .btn-icon {
  border-radius: var(--r-sm);
  /* …and the box model too. Only `button` got the flex layout, so an <a> in the
     same family laid itself out as text: a "Cancel" link stood 37px beside a
     40px submit, and the same class measured 29/33/35/37px depending on what
     was inside it. */
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  line-height: 1; white-space: nowrap; text-decoration: none;
  transition: background-color var(--fast) var(--ease-out), border-color var(--fast) var(--ease-out),
              color var(--fast) var(--ease-out), transform var(--fast) var(--ease-out), box-shadow var(--fast) var(--ease-out);
}
/* every secondary control (All bots / All machines / Statistics / toolbar) is
   this one button: transparent seat, hairline, same hover as the topbar chrome */
.btn-ghost {
  background: transparent; color: var(--ink-2); border: 1px solid var(--line-2);
  /* same seat height as the primary it stands next to; .btn-sm still shrinks
     the dense ones in table rows and toolbars */
  min-height: 40px; padding: 8px 14px; font-size: var(--fs-2); font-weight: 500; box-shadow: none;
}
.btn-ghost:hover { background: var(--hover); border-color: var(--line); color: var(--ink); }
.btn-danger { background: var(--bad-soft); color: var(--bad); border: 1px solid rgba(255,91,107,.25); box-shadow: none; }
.btn-danger:hover { background: rgba(255,91,107,.2); }
/* .btn-sm is DENSITY, not a smaller hit target. It used to stand 32px, which
   made Revoke / Send test / Rotate Device Token / Manage a different physical
   control from the Save button above them. Keep the tighter type and padding,
   keep the 40px seat: one panel, one control height. */
.btn-sm { min-height: 40px; padding: 8px 13px; font-size: var(--fs-2); }
/* P2: unified icon action target — min 40×40 hit area (was ~31px) */
.btn-icon { min-width: 40px; min-height: 40px; padding: 0; font-size: var(--fs-2); line-height: 0;
  display: inline-flex; align-items: center; justify-content: center; }
/* One icon size for the whole family. Templates hand these SVGs width="13",
   width="15" or width="18" attributes; a presentational attribute loses to CSS,
   so this single rule ends the three-sizes-in-one-row look. It is also why the
   glyph cannot change size on hover or while loading — nothing else sets it. */
.btn-icon svg, .icon-btn svg { display: block; width: 16px; height: 16px; flex: none; }
/* .btn-icon.btn-sm is still a 40px target: only the seat's padding differs */
.btn-icon.btn-sm { min-width: 40px; min-height: 40px; padding: 0; }
/* submit loading state: spinner + disabled while the request is in flight */
.btn-spin { display: inline-block; width: 13px; height: 13px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .6s linear infinite; margin-right: 2px; vertical-align: -1px; }
button.is-loading, .btn.is-loading { cursor: progress; opacity: .9; }
select.compact { width: auto; font-size: var(--fs-3); }
.hint { color: var(--ink-3); font-size: var(--fs-3); margin-top: 13px; }

/* form footer: actions pinned bottom-right, hint may sit left (margin-right:auto) */
.form-footer {
  display: flex; align-items: center; justify-content: flex-end; gap: 12px;
  margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line-2);
}
.form-footer .hint, .form-footer .foot-note { margin: 0 auto 0 0; }

/* ---------------- Action footer (one submit contract, every form) ----------
   A form's submit is part of the form and always occupies space. The previous
   pattern hid the whole footer until JS saw a change (.await-dirty / .cfg-footer
   without .show), so a pristine form rendered its Save button at 0×0 — present
   in the DOM, impossible to click, and invisible to anyone whose change didn't
   fire an `input` event (autofill, paste, programmatic set).

   The footer now always renders. `data-state` carries pristine | dirty | saving
   | saved | error; pristine disables the submit but keeps it visible and states
   the reason in text next to it, never in a title-attribute alone. */
.form-actions {
  display: flex; align-items: center; gap: 12px 16px; flex-wrap: wrap;
  margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line-2);
}
/* inside a panel, the footer spans the panel's padding so it reads as a base */
.panel > .form-actions:last-child, section > .form-actions:last-child { margin-bottom: 0; }
.form-actions .fa-status {
  margin: 0 auto 0 0; font-size: var(--fs-3); letter-spacing: .01em; color: var(--ink-3);
  display: inline-flex; align-items: center; gap: 7px; min-width: 0;
}
.form-actions .fa-status svg { width: 14px; height: 14px; flex: none; }
.form-actions .fa-buttons { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.form-actions[data-state="dirty"] .fa-status { color: var(--warn); }
.form-actions[data-state="saved"] .fa-status { color: var(--live); }
.form-actions[data-state="error"] .fa-status { color: var(--bad); }
/* a prerequisite the form cannot satisfy by being filled in correctly */
.form-actions[data-state="blocked"] .fa-status { color: var(--warn); }
/* A short answer does not need a long box. Inside a full-width panel a single
   input stretched to 1,330px at Full HD — a password field the width of the
   screen. The panel keeps its alignment with the rest of the page; the fields
   keep a measure you can actually read across. */
.form-measure { max-width: 680px; }
/* the footer stays inside the measure too — a Save button 650px to the right of
   the last field it saves is a button that belongs to nothing */
/* what is running right now, beside the fields that pin it — one line, not three
   cards at the top of a settings page */
.ver-now { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 10px 0 16px;
  font-size: var(--fs-3); color: var(--ink-3); }
.ver-now b { color: var(--ink-2); font-weight: 600; font-variant-numeric: tabular-nums; }
/* A panel whose only child is the footer drew the footer's own top rule 18px
   below its top padding — an empty band with a hairline through it, which is
   what /machines/create and /settings showed above their buttons. */
.form-actions-panel > .form-actions,
.panel > .form-actions:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* saved → the confirmation fades out on its own; the row keeps its height */
.form-actions .fa-status[data-fade] { transition: opacity 180ms var(--ease-out); }
.form-actions .fa-status.is-faded { opacity: 0; }
/* the footer never changes height between states: the status line reserves one
   row and the buttons keep their width while a spinner swaps in */
.form-actions .fa-buttons > button { min-width: var(--fa-btn-w, auto); }

@media (max-width: 620px) {
  .form-actions { flex-direction: column; align-items: stretch; }
  .form-actions .fa-status { margin: 0; order: 2; }
  .form-actions .fa-buttons { order: 1; }
  .form-actions .fa-buttons > * { flex: 1 1 auto; }
}
.field-group { margin-bottom: 17px; }
.field-group > .field-lab { margin-bottom: 7px; }

/* ---------------- Empty state (orbital signature glyph) ------------ */
.empty { color: var(--ink-3); padding: 8px 0; }
/* empty ≠ dead: the heading reads at full ink, the body at secondary — an empty
   panel should look intentional, not like the page failed to load */
.empty-state { text-align: center; padding: 46px 20px; color: var(--ink-3); }
.empty-state .orbit, .empty-state .empty-ic { margin-left: auto; margin-right: auto; margin-bottom: 16px; }
/* Placeholder titles used to be <h3> directly under the page <h1>, skipping a
   level on 13 pages. Same size, honest level. */
.empty-state h2, .empty-state h3, .es-title {
  color: var(--ink); font-size: var(--fs-4); font-weight: 600; margin: 0 0 6px;
}
.empty-state p { font-size: var(--fs-3); line-height: 1.6; color: var(--ink-3); margin: 0 auto; max-width: 42ch; }

/* orbital ring — echoes the ELIO "o"; used in empty states & loaders */
.orbit { width: 46px; height: 46px; position: relative; color: var(--accent); }
.orbit .ring { width: 100%; height: 100%; border-radius: 50%; border: 1.5px solid var(--accent-line); }
.orbit .node { position: absolute; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent-glow); }
.orbit .node.a { top: -3px; left: 50%; margin-left: -4px; }
.orbit .node.b { bottom: -3px; left: 50%; margin-left: -4px; }
.orbit.spin { animation: spin 3.2s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* progress bar (xp toward weekly cap etc.) */
.bar { height: 6px; border-radius: 4px; background: var(--surface-3); overflow: hidden; }
.bar > span { display: block; height: 100%; border-radius: 4px; background: var(--accent); box-shadow: 0 0 10px var(--accent-glow); transition: width var(--slow) var(--ease-out); }
.bar.live > span { background: var(--live); box-shadow: 0 0 10px rgba(53,214,160,.4); }

/* small helpers */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
/* Payments: the wallet card and the top-up form hold different amounts of
   content, so left to themselves they end at different heights. Match them and
   drop the closing element of each to the bottom for one shared baseline. */
.pay-row > .panel { display: flex; flex-direction: column; }
.pay-row .pay-foot { margin-top: auto; margin-bottom: 0; padding-top: 14px; }
.pay-row .pay-form { display: flex; flex-direction: column; flex: 1; }
.pay-row .pay-form .form-footer { margin-top: auto; }

/* A plan is one full-width row: name, what you get, price, buy. Reads left to
   right and still works if a second plan is ever added — it stacks below. */
.plan-list { display: flex; flex-direction: column; gap: 12px; }
.plan-bar { display: flex; align-items: center; gap: 22px; flex-wrap: wrap;
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-lg); padding: 16px 18px; }
.plan-bar .pb-name { font-size: var(--fs-4); font-weight: 600; }
.plan-bar .pb-specs { display: flex; gap: 18px; flex-wrap: wrap;
  color: var(--ink-3); font-size: var(--fs-3); }
.plan-bar .pb-specs b { color: var(--ink-2); font-weight: 600; font-variant-numeric: tabular-nums; }
.plan-bar .pb-price { margin-left: auto; font-size: var(--fs-6); font-weight: 600;
  font-variant-numeric: tabular-nums; }
.plan-bar form { flex-shrink: 0; }
.plan-status { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; }
/* Full-width purchase block: feature list left, price + CTA right. Reads as a
   filled panel with one plan instead of a lonely third-width card. */
.plan-buy { display: flex; align-items: center; gap: 34px; flex-wrap: wrap; }
.plan-buy .pb-feats { flex: 1 1 360px; min-width: 0; }
.plan-buy .pb-aside { flex: 0 0 auto; min-width: 190px; display: flex;
  flex-direction: column; align-items: flex-end; gap: 14px; text-align: right; }
.plan-buy .pb-aside form, .plan-buy .pb-aside .btn { width: 100%; }
@media (max-width: 640px) {
  .plan-buy .pb-aside { align-items: stretch; text-align: left; width: 100%; min-width: 0; }
}

/* Downloads — customer install screen. Two columns that fit the viewport
   without scrolling: hero (logo + download) beside the setup timeline. Height
   becomes max(columns), not their sum; stacks under 900px. */
.dl-install { display: grid; grid-template-columns: 1.05fr 1fr;
  gap: clamp(28px, 5vw, 72px); align-items: center;
  max-width: 1060px; margin-inline: auto; min-height: calc(100vh - 118px); }
@media (max-width: 900px) {
  .dl-install { grid-template-columns: 1fr; min-height: 0; gap: 8px; }
}

.dl-hero { position: relative; max-width: 460px; margin: 0 auto;
  padding: 8px 12px; text-align: center;
  display: flex; flex-direction: column; align-items: center; }
.dl-hero > * { position: relative; z-index: 1; }
/* The showpiece: the wordmark reveals from a blur and a chrome sheen sweeps its
   letters on a slow loop — no background glow disc. The real <img> is the base
   layer, so if mask-image is unsupported the logo still shows, just without the
   sheen. All motion is disabled under prefers-reduced-motion. */
.dl-logo { position: relative; display: inline-block; width: min(248px, 62%);
  aspect-ratio: 248 / 60; margin: 0 auto 34px;
  animation: dl-logo-in 1000ms cubic-bezier(.2,.75,.25,1) both; }
/* base sits below full white so the swept highlight reads as a real glint (a
   white sheen over pure white would be invisible under screen blend) */
.dl-logo-img { display: block; width: 100%; height: 100%;
  filter: brightness(.85) drop-shadow(0 12px 44px rgba(0,0,0,.65)); }
.dl-logo-sheen { position: absolute; inset: 0; pointer-events: none; }
@supports ((-webkit-mask-image: url("/static/logo.png")) or (mask-image: url("/static/logo.png"))) {
  .dl-logo-sheen {
    -webkit-mask: url("/static/logo.png?v=2") center / contain no-repeat;
            mask: url("/static/logo.png?v=2") center / contain no-repeat;
    /* tight bright core with a soft falloff = a chrome specular streak */
    background-image: linear-gradient(100deg, transparent 40%,
      rgba(255,255,255,.55) 46%, #ffffff 50%, rgba(255,255,255,.55) 54%, transparent 60%);
    background-size: 250% 100%; background-repeat: no-repeat;
    mix-blend-mode: screen; filter: drop-shadow(0 0 10px rgba(255,255,255,.45));
    animation: dl-sheen 4.4s cubic-bezier(.5,0,.15,1) 1.1s infinite; }
}
@keyframes dl-logo-in {
  0%   { opacity: 0; transform: translateY(14px) scale(.94); filter: blur(9px); }
  100% { opacity: 1; transform: none; filter: blur(0); }
}
@keyframes dl-sheen {
  0%   { background-position: 175% 0; }
  24%  { background-position: -75% 0; }
  100% { background-position: -75% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .dl-logo { animation: none; }
  .dl-logo-sheen { animation: none; display: none; }
}
.dl-title { font: 700 clamp(21px, calc(3.4vw - 1px), 27px)/1.12 var(--sans); letter-spacing: -.02em; margin: 0 0 10px; }
.dl-sub { color: var(--ink-3); font-size: var(--fs-4); line-height: 1.55; max-width: 400px; margin: 0 0 34px; }
.dl-btn.btn { min-height: 54px; padding: 0 34px; font-size: 14.5px; font-weight: 650;
  border-radius: var(--r-lg); box-shadow: 0 10px 34px var(--accent-glow); }
.dl-btn.btn svg { width: 18px; height: 18px; flex: none; }
.dl-btn.btn:hover { transform: translateY(-2px); box-shadow: 0 16px 44px var(--accent-glow); }
.dl-meta { margin-top: 15px; font-size: 11.5px; color: var(--ink-4); font-variant-numeric: tabular-nums; }

/* Tutorial as a connected timeline: numbered nodes on a spine, one action per
   step. A sequence — not a row of cards. */
.dl-steps { max-width: 520px; margin: 0 auto; width: 100%; }
.dl-steps-h { font-size: var(--fs-1); letter-spacing: .09em; text-transform: uppercase;
  color: var(--ink-4); margin: 0 0 22px; }
.dl-step { position: relative; display: grid; grid-template-columns: 40px 1fr; gap: 18px; padding-bottom: 22px; }
.dl-step:last-child { padding-bottom: 0; }
/* the spine: connects each node to the next; meaning = the steps are in order */
.dl-step:not(:last-child)::before { content: ""; position: absolute; left: 19px; top: 42px; bottom: 0;
  width: 2px; background: var(--line-2); }
.dl-step-n { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
  background: var(--surface-3); border: 1px solid var(--line); color: var(--ink);
  font: 600 14px/1 var(--sans); font-variant-numeric: tabular-nums; position: relative; z-index: 1; }
.dl-step-b { padding-top: 8px; min-width: 0; }
.dl-step-h { font-size: 14px; font-weight: 600; margin: 0 0 5px; letter-spacing: -.01em; }
.dl-step-p { color: var(--ink-3); font-size: 12.5px; line-height: 1.55; margin: 0 0 12px; }
.dl-step-b > :last-child { margin-bottom: 0; }
.dl-key-row { display: flex; align-items: center; gap: 10px; max-width: 440px; }
.dl-key-row code { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-3); color: var(--ink-2); background: var(--surface-3);
  border: 1px solid var(--line-2); border-radius: var(--r-sm); padding: 9px 11px; cursor: pointer; }
@media (max-width: 640px) {
  .plan-bar { flex-direction: column; align-items: stretch; gap: 12px; }
  .plan-bar .pb-price { margin-left: 0; }
  .plan-bar button { width: 100%; }
}
/* grid items default to min-width:auto → a wide table inside a panel blows the
   track past the viewport on narrow screens. Zero the minimum so the panel
   stays put and its .table-wrap scrolls instead. */
.grid-2 > *, .stat-grid > *, .content-grid > * { min-width: 0; }
.stack > * + * { margin-top: 12px; }
.flex { display: flex; align-items: center; gap: 10px; }
.right { margin-left: auto; }

/* mobile chrome — hidden on desktop, shown in the drawer breakpoint */
.nav-toggle, .topbar-brand, .nav-overlay { display: none; }
/* the topbar logo is a plain link, never a button — no surface, no radius */
.topbar-brand { background: transparent; border: 0; border-radius: 0; transition: opacity 160ms ease; }

@media (max-width: 980px) {
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
  .grid-2 { grid-template-columns: 1fr; }
}

@media (max-width: 760px) {
  /* --- sidebar becomes a slide-in drawer --- */
  .sidebar {
    position: fixed; left: 0; top: 0; z-index: 120; width: 266px;
    /* dvh, not vh: iOS URL-bar-inclusive 100vh clips Users/Audit/logout at the
       bottom. The drawer scrolls internally (overflow-y:auto above). */
    height: 100dvh;
    padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    overscroll-behavior: contain;
    transform: translateX(-100%); transition: transform 180ms cubic-bezier(.2, .8, .2, 1);
    box-shadow: 0 0 60px rgba(0,0,0,.6); border-right: 1px solid var(--line);
  }
  body.nav-open .sidebar { transform: translateX(0); }
  /* modal state: the page behind the drawer must not scroll */
  body.nav-open { overflow: hidden; }
  /* compact nav rhythm: the full menu (incl. Users/Audit/logout) fits a typical
     phone viewport without scrolling; shorter screens still scroll internally */
  .sidebar .brand { height: 60px; }
  .sidebar .brand-logo { width: 136px; }
  .sidebar .nav-link { padding: 8px 12px; }
  .sidebar .nav-label { padding: 9px 12px 5px; }
  .sidebar .user-card { padding: 8px; margin-top: 2px; }
  .sidebar .side-bottom { padding-top: 6px; }

  /* --- notifications: full-width sheet under the topbar (the desktop popover,
     right-anchored to the bell, used to start at x=-70 and clip its header).
     .bell-wrap prefix: must out-rank the base .bell-panel rule defined later. --- */
  .bell-wrap .bell-panel {
    position: fixed; left: 12px; right: 12px; width: auto; top: 62px;
    max-height: min(440px, calc(100dvh - 76px));
  }
  .nav-overlay {
    display: block; position: fixed; inset: 0; z-index: 110; background: rgba(0,0,0,.6);
    opacity: 0; visibility: hidden; transition: opacity var(--base), visibility var(--base);
  }
  body.nav-open .nav-overlay { opacity: 1; visibility: visible; }

  /* --- topbar: stable grid — menu | free centre zone (logo) | bell | profile.
     No absolute/viewport-centred logo: with asymmetric sides it used to
     overlap the bell (390px) and even the trailing control (320px). The logo
     now centres inside ITS OWN free track and shrinks before touching anything. */
  .topbar {
    /* back on: below 760px the sidebar is an off-canvas drawer and this bar
       carries the only control that opens it */
    display: grid;
    position: sticky; top: 0; z-index: 20;
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter: saturate(140%) blur(14px);
    border-bottom: 1px solid var(--line-2);
    padding: 10px 12px; gap: 8px;
    /* toggle · brand · profile — one column each, no bell to make room for */
    display: grid; grid-template-columns: 38px minmax(0, 1fr) auto;
    align-items: center;
  }
  .nav-toggle {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 38px; height: 38px; min-height: 0; padding: 0; border-radius: 12px;
    background: rgba(255,255,255,.05); border: 1px solid var(--line-2); color: var(--ink); box-shadow: none;
  }
  .nav-toggle:hover { background: rgba(255,255,255,.09); }
  .nav-toggle svg { width: 20px; height: 20px; }
  .topbar-brand { display: flex; align-items: center; justify-content: center; min-width: 0; }
  .topbar-logo { width: 112px; max-width: 100%; height: auto; display: block; }
  .topbar .spacer { display: none; }
  .topbar .icon-btn { padding: 8px; }

  /* --- layout --- */
  .content { padding: 18px 14px 56px; }
  .page-title { font-size: var(--fs-6); }
  .page-head { flex-direction: column; align-items: stretch; gap: 12px; }
  .page-head .btn, .page-head .row { width: 100%; }
  .page-head .row { justify-content: stretch; }
  .page-head .row > * { flex: 1; justify-content: center; }
  .stat-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .stat-grid .stat { min-height: 84px; padding: 14px; }
  .stat-grid .stat .v { font-size: var(--fs-6); }
  .card, .panel { padding: 16px; }
  /* keep the primary action reachable while scrolling a long mobile form */
  .sticky-foot {
    position: sticky; bottom: 0; z-index: 5;
    margin: 14px -16px 0; padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
    background: var(--surface-2); border-top: 1px solid var(--line-2);
    border-radius: 0 0 var(--r-lg) var(--r-lg);
  }
  .content .empty-state { padding: 30px 14px; }
  .content .empty-state .orbit { width: 44px; height: 44px; }
  form { max-width: 100% !important; }
  form .row { flex-wrap: wrap; }
  form .row > select, form .row > input, form .row > button { flex: 1 1 140px; }

  /* utility: hide low-priority table columns on phones (plain .table-wrap too) */
  .mob-hide { display: none; }


  /* other tables keep horizontal scroll, margins matched to mobile padding;
     scroll-shadows on both edges signal that the zone pans (background-attachment
     trick: covers fade out at the scrolled edge) */
  .table-wrap:not(.as-cards) {
    /* -14px matches .content's 14px mobile padding exactly — -16px used to
       poke 2px past the viewport and cause a phantom horizontal scroll */
    -webkit-overflow-scrolling: touch; margin: 0 -14px;
    background:
      linear-gradient(90deg,  var(--surface-2) 35%, transparent) 0 0 / 44px 100%,
      linear-gradient(270deg, var(--surface-2) 35%, transparent) 100% 0 / 44px 100%,
      radial-gradient(farthest-side at 0 50%,   rgba(0,0,0,.5), transparent) 0 50% / 16px 100%,
      radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.5), transparent) 100% 50% / 16px 100%;
    background-repeat: no-repeat;
    background-attachment: local, local, scroll, scroll;
  }
  .table-wrap:not(.as-cards) table { padding: 0 14px; }
  /* narrow cards: title + action must never clip — let the head wrap */
  .panel-head { flex-wrap: wrap; }
  .panel-head > .btn-ghost, .panel-head > .btn { flex: none; }
}

/* very narrow phones (320-430): logo shrinks before it can touch neighbours */
@media (max-width: 480px) {
  .topbar-logo { width: 96px; }
}
@media (max-width: 360px) {
  .topbar-logo { width: 84px; }
  .topbar { gap: 6px; padding-inline: 10px; }
}

/* ============================================================
   POLISH PASS — native controls, table packing, small fixes
   ============================================================ */

/* Custom-styled <select> (kill the native OS dropdown look) */
select, select.compact {
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23A4A9B4' 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 11px center; background-size: 15px;
  padding-right: 34px; cursor: pointer;
}
select option { background: var(--surface-3); color: var(--ink); }

/* Custom file input button */
input[type=file] { font: 400 var(--fs-3) var(--sans); color: var(--ink-3); padding: 0; }
input[type=file]::file-selector-button {
  background: rgba(255,255,255,.05); color: var(--ink); border: 1px solid var(--line);
  padding: 9px 14px; border-radius: var(--r-sm); font: 600 var(--fs-2) var(--sans);
  margin-right: 12px; cursor: pointer; transition: background-color var(--fast), border-color var(--fast);
}
input[type=file]::file-selector-button:hover { background: rgba(255,255,255,.09); border-color: var(--accent-line); }

/* select inside a table cell keeps a usable width — 120px, not 150px: two of
   these sit in the bots row and the 60px they used to hoard is what pushed the
   table past the screen. */
td .compact, td select.compact { min-width: 120px; }

/* Stat tiles never collapse when a value is short ("—", version) */
.stat { min-height: 96px; display: flex; flex-direction: column; }
/* Pin the caption (and any sparkline after it) to the bottom, so the NUMBER
   keeps its fixed 13px gap under the label in every tile. Pinning the number
   instead made it drift — cards with a sparkline pushed it up, cards without
   let it sink — so equal-height tiles showed numbers at different heights. */
.stat .d { margin-top: auto; }

/* User card: keep it on one row, never wrap the meta line */
.user-card { flex-wrap: nowrap; }
.user-card > div { min-width: 0; }
.user-card .tg { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Empty-state orbit reads as an intentional mark, not stray dots */
.empty-state .orbit { width: 52px; height: 52px; }
.empty-state .orbit .ring { border-color: var(--accent-line); }


/* inline autosave indicator (bots → machine allocation) */
.allocate-form { display: inline-flex; align-items: center; gap: 8px; }
.save-flash { font-size: var(--fs-1); font-weight: 600; opacity: 0; transition: opacity var(--fast); }
.save-flash.ok { color: var(--live); }
.save-flash.err { color: var(--bad); }
.save-flash.show { opacity: 1; }

/* copy-to-clipboard code chips */
.copyable { cursor: pointer; position: relative; }
.copyable:hover { border-color: var(--accent-line); color: var(--ink); }
.copyable.copied::after {
  content: "copied"; position: absolute; left: 50%; bottom: calc(100% + 6px); transform: translateX(-50%);
  background: var(--live); color: #04120c; font: 700 var(--fs-1) var(--sans); padding: 2px 7px; border-radius: 6px; white-space: nowrap;
}

/* skeleton shimmer (loading states) */
.skeleton { position: relative; overflow: hidden; background: var(--surface-3); border-radius: 6px; color: transparent !important; }
.skeleton::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.06), transparent); animation: shimmer 1.3s infinite; }
@keyframes shimmer { 100% { transform: translateX(100%); } }

/* drag-and-drop upload zone */
.dropzone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; text-align: center;
  border: 1.5px dashed var(--line); border-radius: var(--r); padding: 28px 20px; cursor: pointer; color: var(--ink-3);
  transition: border-color var(--fast), background-color var(--fast), color var(--fast); }
.dropzone:hover, .dropzone.drag { border-color: var(--accent-line); background: var(--accent-soft); color: var(--ink-2); }
.dropzone.has-file { border-style: solid; border-color: var(--accent-line); }
.dropzone input[type=file] { display: none; }
.dropzone svg { width: 26px; height: 26px; color: var(--ink-3); }
.dropzone.drag svg, .dropzone.has-file svg { color: var(--accent); }
.dz-text b { color: var(--ink); font-weight: 600; }
.dz-file { font: var(--fs-2) var(--mono); color: var(--live); }

/* toast */
.toast-wrap { position: fixed; right: 20px; bottom: 20px; z-index: 200; display: flex; flex-direction: column; gap: 10px; }
.toast {
  display: flex; align-items: center; gap: 10px; min-width: 220px; max-width: 360px;
  background: var(--surface-3); border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--r-sm); padding: 12px 14px; font-size: var(--fs-3); color: var(--ink);
  box-shadow: 0 12px 40px -12px rgba(0,0,0,.7); animation: toastIn var(--base) var(--ease-out);
}
.toast.ok { border-left-color: var(--live); }
.toast.err { border-left-color: var(--bad); }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* confirm modal — dangerous actions (logout, delete, overwrite) */
.modal-overlay {
  position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(3,3,4,.68); backdrop-filter: blur(3px);
  opacity: 0; visibility: hidden; transition: opacity var(--base), visibility var(--base);
}
.modal-overlay.show { opacity: 1; visibility: visible; }
.modal {
  width: 100%; max-width: 410px; background: var(--surface-3); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 22px 22px 18px; box-shadow: 0 24px 70px -20px rgba(0,0,0,.85);
  transform: translateY(10px) scale(.985); transition: transform var(--base) var(--ease-out);
}
.modal-overlay.show .modal { transform: none; }
.modal h3 { margin: 0 0 8px; font-size: var(--fs-5); font-weight: 600; letter-spacing: 0; }
.modal p { margin: 0; font-size: var(--fs-3); color: var(--ink-2); line-height: 1.55; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; }

/* ============================================================
   WORK-AREA LAYOUT — main column + right aside (kills empty-right)
   ============================================================ */
.content-grid { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 22px; align-items: start; }
/* wider aside when the right column carries a primary action (e.g. Add proxy) */
.content-grid--wide { grid-template-columns: minmax(0, 1fr) 380px; gap: 24px; }
/* Form layout: the column holding the inputs stops at a readable measure and the
   remaining width carries context, instead of one text box being stretched to
   1400px because that is how much screen there happens to be. */
/* space-between, not start. The form keeps its short measure and the help column
   keeps its narrow one, but pinning both to the left left a 224px ragged strip
   under a heading that ran the full width. Pushing the help column to the right
   edge gives the page the same two margins as every other page — the surplus
   becomes a gutter between the form and its notes instead of a hole at the end.
   (Narrowing .content itself was the other option, and it was worse: it moved
   the left edge inward, so walking from /machines to /machines/create shifted
   the whole layout sideways.) */
.content-grid--form { grid-template-columns: minmax(0, 760px) minmax(320px, 420px); gap: 32px; justify-content: start; }
@media (max-width: 1180px) { .content-grid--form { grid-template-columns: minmax(0, 1fr); } }

/* numbered install steps in an aside card */
.ac-steps { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 10px; }
.ac-steps li { font-size: var(--fs-2); line-height: 1.55; color: var(--ink-3); }
.ac-steps li::marker { color: var(--ink-4); font-weight: 600; }
.ac-steps b { color: var(--ink-2); font-weight: 600; }

/* inline form-level error banner (was an 8-property inline style per template) */
.form-alert {
  display: flex; align-items: center; gap: 8px; margin-bottom: 16px; padding: 10px 13px;
  background: var(--bad-soft); color: var(--bad); border: 1px solid rgba(255,91,107,.24);
  border-radius: var(--r-sm); font-size: var(--fs-2);
}
.form-alert svg { flex: none; }
.content-grid > .main-col { min-width: 0; }
.content-grid > .aside-col { position: sticky; top: 82px; display: flex; flex-direction: column; gap: 12px;
  /* Safety net only: if the column is still taller than a short viewport it
     scrolls inside rather than clipping the bottom cards. The cards below are
     compacted so on a normal viewport the whole stack fits and no bar shows. */
  max-height: calc(100dvh - 98px); overflow-y: auto; scrollbar-width: thin; }
/* Compact the sticky settings aside so it fits without an inner scrollbar.
   Scoped here so aside cards elsewhere keep their roomier spacing. */
.content-grid > .aside-col .aside-card { padding: 12px 16px; }
.content-grid > .aside-col .aside-card .ac-title { margin-bottom: 9px; }
.content-grid > .aside-col .ac-row { padding: 7px 0; }
.content-grid > .aside-col .set-nav a { padding: 6px 10px; }
.content-grid > .aside-col .ac-link { padding: 7px 9px; }
.content-grid > .aside-col .ac-note { line-height: 1.5; }
@media (max-width: 1080px) {
  .content-grid { grid-template-columns: 1fr; }
  /* single column: the aside is in normal flow, so it must not cap or scroll */
  .content-grid > .aside-col { position: static; max-height: none; overflow: visible; }
}
/* stack that keeps desktop order but lifts the primary action first on mobile */
.jobs-stack { display: flex; flex-direction: column; gap: 22px; }
.jobs-stack > * { margin: 0; }
@media (max-width: 760px) {
  .jobs-stack > .js-create { order: -1; }
  /* accordion content (Targets & intensity) sits next to Create job / Queue &
     history — extra vertical air so an expanded group never crowds the queue */
  .jobs-stack { gap: 28px; }
  .js-create .panel details.grp { margin-top: 10px; }
  .js-create .panel details.grp[open] { padding-bottom: 6px; }
}

/* Install steps — the customer-facing half of /downloads. Numbered rows, one
   action each; no cards, because nothing here is an interactive container. */
.ins-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 22px; }
.ins-list > li { display: flex; gap: 14px; align-items: flex-start; }
.ins-n { flex: 0 0 26px; height: 26px; border-radius: 50%; background: var(--accent-soft);
  color: var(--accent); font: 600 var(--fs-2)/26px var(--sans); text-align: center; }
.ins-body { min-width: 0; }
.ins-h { font-size: var(--fs-4); font-weight: 600; margin-bottom: 4px; }
.ins-p { font-size: var(--fs-3); line-height: 1.55; color: var(--ink-2); margin: 0 0 12px; max-width: 62ch; }
.ins-key { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ins-key > code { font-family: var(--mono); font-size: var(--fs-3); letter-spacing: .04em;
  background: var(--surface-3); border: 1px solid var(--line-2); border-radius: var(--r-sm);
  padding: 9px 13px; cursor: pointer; overflow-wrap: anywhere; }

/* A section the page can live without: folded by default on every screen.
   (details.grp is the opposite — JS forces it open on desktop.) */
details.fold > summary { list-style: none; cursor: pointer; display: flex; align-items: center;
  gap: 8px; font-size: var(--fs-3); font-weight: 600; color: var(--ink-2); }
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary::before { content: ""; width: 8px; height: 8px; flex: none;
  border-right: 1.6px solid var(--ink-3); border-bottom: 1.6px solid var(--ink-3);
  transform: rotate(-45deg); transition: transform var(--fast); }
details.fold[open] > summary::before { transform: rotate(45deg); }
details.fold > summary:hover { color: var(--ink); }

/* aside info card — quieter surface than a .panel */
.aside-card { background: var(--surface-2); border: 1px solid var(--line-2); border-radius: var(--r-lg); padding: 16px 17px; }
/* Sentence case, because these are section titles, not value labels.
   The panel ran two card-title systems side by side: "Recent API Keys" and
   "Behaviour" in sentence case next to "WHAT HAPPENS NEXT", "GETTING THE IP
   RIGHT", "ON THIS PAGE" and "QUICK ACTIONS" in spaced caps — often on the same
   screen. The rule now: SPACED CAPS captions a single value (a stat, a kv row,
   a table column); Sentence case titles a block you read. */
.aside-card .ac-title { font-size: var(--fs-4); font-weight: 600; letter-spacing: 0;
  line-height: var(--lh-snug); color: var(--ink); margin: 0 0 12px; }
/* A key/value readout row. Both cells are flex children, so both need
   `min-width: 0` — without it a long monospace value (an IP, a version pair, a
   hardware id) refuses to shrink below its intrinsic width and pushes the card,
   the grid column and finally the page wider than the viewport. That single
   missing declaration is what put a horizontal scrollbar under this page at
   every width from 1440 down. The value now wraps instead of shoving. */
.ac-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line-2); font-size: var(--fs-3); }
.ac-row:last-child { border-bottom: 0; }
.ac-row > * { min-width: 0; }
.ac-row .k { color: var(--ink-3); flex: none; }
.ac-row .v {
  color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums;
  text-align: right; overflow-wrap: anywhere; word-break: break-word;
}
.ac-row .v.mono { font-family: var(--mono); font-size: var(--fs-2); }
.ac-note { font-size: var(--fs-2); color: var(--ink-3); line-height: 1.55; margin: 0; }
.ac-note + .ac-note { margin-top: 10px; }
.ac-note b { color: var(--ink-2); font-weight: 600; }
/* a failed action states what went wrong where the action is; empty until it
   has something to say, so it takes no space in the normal case */
.ac-error { font-size: var(--fs-2); color: var(--bad); line-height: 1.5; margin: 10px 0 0; }
.ac-error:empty { display: none; }
.ac-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; }
.ac-list li { display: flex; gap: 9px; font-size: var(--fs-2); color: var(--ink-2); line-height: 1.5; }
.ac-list li svg { width: 15px; height: 15px; flex-shrink: 0; margin-top: 1px; color: var(--accent); }
/* quick-action links in an aside card */
.ac-links { display: flex; flex-direction: column; gap: 1px; }
.ac-link { display: flex; align-items: center; gap: 10px; padding: 9px 9px; margin: 0 -9px; border-radius: 10px;
  font-size: var(--fs-3); font-weight: 500; color: var(--ink-2);
  transition: background-color 140ms ease, color 140ms ease; }
.ac-link:hover { background: var(--surface-3); color: var(--ink); }
.ac-link > svg:first-child { width: 16px; height: 16px; flex: none; color: var(--ink-3); transition: color 140ms ease; }
.ac-link:hover > svg:first-child { color: var(--accent); }
.ac-link .go { margin-left: auto; width: 14px; height: 14px; color: var(--ink-4); opacity: .7; }

/* toggle switch (settings) */
.switch { position: relative; display: inline-block; width: 38px; height: 22px; flex-shrink: 0; }
/* The real control, made invisible rather than made small. Collapsing it to
   0×0 (the old rule) left a focusable element with no box at all — the exact
   thing a "no dead 0×0 controls" pass is looking for, and it meant the pointer
   never actually hit the input. Covering the switch keeps it the hit target. */
.switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer;
  /* above the track it is standing in for — otherwise the track paints over it
     and the input, though full-size, never actually receives the pointer */
  z-index: 1; }
.switch .track { position: absolute; inset: 0; border-radius: 999px; background: var(--surface-4); border: 1px solid var(--line); transition: background-color var(--fast), border-color var(--fast); cursor: pointer; }
.switch .track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--ink-2); transition: transform var(--fast) var(--ease-out), background-color var(--fast); }
/* On reads as lit, not merely moved: the track brightens and the knob carries
   a white halo, so the state is legible from across the row instead of coming
   down to which side a grey dot sits on. Geometry is untouched — a switch that
   grew on click would shove its row around. */
.switch input:checked + .track {
  background: rgba(255,255,255,.22); border-color: rgba(255,255,255,.55);
  box-shadow: 0 0 10px rgba(255,255,255,.16);
}
.switch input:checked + .track::after {
  transform: translateX(16px); background: #fff;
  box-shadow: 0 0 9px rgba(255,255,255,.6), 0 1px 2px rgba(0,0,0,.45);
}
.switch input:focus-visible + .track { box-shadow: 0 0 0 3px var(--accent-glow); }
.switch input:checked:focus-visible + .track {
  box-shadow: 0 0 0 3px var(--accent-glow), 0 0 10px rgba(255,255,255,.16);
}
/* stable row geometry: fixed min-height so one- and two-line rows align,
   text left (.txt flex:1), switch pinned right (flex-shrink:0) */
.setting-row { display: flex; align-items: center; gap: 14px; padding: 13px 0; min-height: 58px; border-bottom: 1px solid var(--line-2); }
.setting-row:last-child { border-bottom: 0; }
/* ...and when a form footer follows the list, the last row is NOT :last-child,
   so its hairline survived while the footer drew its own right under it — two
   lines with a gap. The footer's border is the divider; drop the row's. */
.setting-row:has(+ .form-actions) { border-bottom: 0; }
/* grouped settings — labelled sections keep a long list scannable */
/* a sub-title inside a panel — one rung under .panel-title, not a caption */
.set-group-label { font-size: var(--fs-3); font-weight: 600; letter-spacing: 0;
  line-height: var(--lh-snug); color: var(--ink-2); padding: 20px 0 6px; }
.set-group:first-of-type .set-group-label { padding-top: 4px; }
.set-group .setting-row:last-child { border-bottom: 0; }

/* ---- collapsible group (accordion on mobile, always-open label on desktop) ----
   JS (base.html) forces .grp open + adds .locked on desktop; on mobile it
   respects the authored `open` attribute and lets the summary toggle. */
details.grp { }
/* collapsed state must be truly gone: no layout, no hitbox, no tab stops.
   Newer engines keep closed <details> content in the tree (::details-content
   with content-visibility) — automated overlap checks then see phantom boxes.
   Force the classic behaviour. */
details.grp:not([open]) > *:not(summary) {
  display: none !important;
  visibility: hidden; pointer-events: none;
}
details.grp > summary { list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
/* opening eases the content in — the page grows smoothly, not with a snap */
details.grp[open] > *:not(summary) { animation: accIn 200ms var(--ease-out); }
@keyframes accIn { from { opacity: 0; transform: translateY(-5px); } }
details.grp > summary::-webkit-details-marker { display: none; }
details.grp > summary::marker { content: ""; }
.grp .chev { flex: none; width: 16px; height: 16px; color: var(--ink-3); transition: transform var(--fast) var(--ease-out); }
details.grp[open] > summary .chev { transform: rotate(180deg); }
details.grp.locked > summary { cursor: default; pointer-events: none; }
details.grp.locked > summary .chev { display: none; }
/* settings variant: summary IS the group label; keep the divider tidy */
.set-group.grp > summary { padding: 20px 0 6px; }
.set-group.grp[open] > summary { border-bottom: 0; }
.set-group.grp:first-of-type > summary { padding-top: 4px; }
/* aside-card variant: collapsed card is just its title (no trailing gap) */
.aside-card.grp:not([open]) > summary { margin-bottom: 0; }
/* panel-as-accordion: collapsed panel is just its title row */
.panel.grp > summary { cursor: pointer; }
.panel.grp:not([open]) > summary { margin-bottom: 0; }
/* desktop: flow settings groups into 2 columns so the panel isn't a tall strip */
@media (min-width: 761px) {
  .settings-groups { column-count: 2; column-gap: 34px; margin-top: 4px; }
  .settings-groups .set-group { break-inside: avoid; }
  .settings-groups .set-group.grp > summary,
  .settings-groups .set-group.grp:first-of-type > summary { padding-top: 12px; }
}
.setting-row .txt { flex: 1; min-width: 0; }
.setting-row .lab { display: block; font-size: var(--fs-3); font-weight: 500; }
.setting-row .help { display: block; font-size: var(--fs-2); color: var(--ink-3); margin-top: 3px; }

/* pricing cards (plans) */
.pricing { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 260px)); gap: 14px; }
.price-card { border: 1px solid var(--line-2); border-radius: var(--r-lg); padding: 18px; background: var(--surface-2); display: flex; flex-direction: column; }
.price-card .pc-name { font-size: var(--fs-3); font-weight: 600; text-transform: capitalize; color: var(--ink-2); }
.price-card .pc-price { font: 700 var(--fs-8)/1 var(--mono); letter-spacing: 0; margin: 9px 0 12px; }
.price-card .pc-price small { font: 500 var(--fs-3) var(--sans); color: var(--ink-3); letter-spacing: 0; }
.price-card .pc-feat { display: flex; align-items: center; gap: 8px; font-size: var(--fs-2); color: var(--ink-2); padding: 5px 0; }
.price-card .pc-feat svg { width: 14px; height: 14px; color: var(--accent); flex-shrink: 0; }

/* calm empty-state mark — small, framed, NEVER reads as a loader.
   Hard-capped so a stale/absent stylesheet can't balloon the inline SVG. */
.empty-ic { width: 52px; height: 52px; flex: none; margin: 0 auto 16px; border-radius: var(--r);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  background: var(--surface-3); border: 1px solid var(--line-2); color: var(--ink-2); }
.empty-ic svg { width: 26px; height: 26px; max-width: 26px; max-height: 26px; }
@media (max-width: 760px) { .empty-ic { width: 48px; height: 48px; } .empty-ic svg { width: 24px; height: 24px; } }

/* amount preset chips (top-up) */
.presets { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }
.preset { padding: 7px 14px; border-radius: var(--r-sm); background: var(--surface-3); border: 1px solid var(--line); color: var(--ink-2); font: 600 var(--fs-3) var(--mono); cursor: pointer; transition: border-color var(--fast), color var(--fast), background-color var(--fast); }
.preset:hover { border-color: var(--accent-line); color: var(--ink); }
.preset.on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

/* upload progress bar */
.upload-progress { height: 6px; border-radius: 4px; background: var(--surface-3); overflow: hidden; margin-top: 14px; display: none; }
.upload-progress.show { display: block; }
.upload-progress > span { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 4px; box-shadow: 0 0 10px var(--accent-glow); transition: width 180ms var(--ease-out); }

/* inert sidebar: no pointer while off-canvas (a11y + no stray clicks) */
.sidebar[inert] { pointer-events: none; }

/* ============ Phase 3 — UX polish ============ */

/* H2 · bot avatar (initial in a status-tinted circle) */
.bot-avatar { width: 28px; height: 28px; border-radius: 50%; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 600; font-size: var(--fs-2); background: var(--surface-3); color: var(--ink-2); }
.bot-avatar.clean { background: var(--live-soft); color: var(--live); }
.bot-avatar.ban { background: var(--bad-soft); color: var(--bad); }
.name-cell { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }
.name-cell .row-link { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* name + a weekly-drop subtitle stacked beside the avatar */
.name-stack { display: inline-flex; flex-direction: column; min-width: 0; gap: 1px; }
.name-stack .row-link { line-height: 1.3; }
.name-sub { font-size: var(--fs-1); line-height: 1.2; color: var(--ink-3); font-weight: 400;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.name-sub .claim-yes { color: var(--live); }

/* H4 fallback · hover on tables that have no <tbody> */
.table-wrap table tr:hover td { background: rgba(255,255,255,.03); }

/* G3 · sortable headers */
th[data-sort] { cursor: pointer; user-select: none; transition: color var(--fast); }
th[data-sort]:hover { color: var(--ink-2); }
th[data-sort]::after { content: "↕"; opacity: .3; margin-left: 5px; font-size: var(--fs-1); }
th.sort-asc::after { content: "↑"; opacity: .8; color: var(--accent); }
th.sort-desc::after { content: "↓"; opacity: .8; color: var(--accent); }
/* On a right-aligned figure column the arrow would sit between the label and
   the cell edge, pushing the label 14px left of the digits below it — the
   numbers then line up under the arrow instead of under their own heading.
   Move it to the other side so label and digits share one right edge. */
th.col-num[data-sort]::after { content: none; margin-left: 0; }
th.col-num[data-sort]::before { content: "↕"; opacity: .3; margin-right: 5px; font-size: var(--fs-1); }
th.col-num.sort-asc::before { content: "↑"; opacity: .8; color: var(--accent); }
th.col-num.sort-desc::before { content: "↓"; opacity: .8; color: var(--accent); }

/* G2 · notifications bell dropdown */
.bell-wrap { position: relative; }
/* visibility (not just opacity) — a closed panel must be unfocusable: the
   `hidden` attribute is restored 160ms after close, and its zero-size link
   used to be reachable by Tab in that window */
.bell-panel { position: absolute; top: calc(100% + 10px); right: 0; width: 328px; max-height: 420px;
  overflow: auto; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r); box-shadow: 0 20px 48px rgba(0,0,0,.45); z-index: 210;
  opacity: 0; transform: translateY(-6px); pointer-events: none; visibility: hidden;
  transition: opacity 140ms var(--ease-out), transform 140ms var(--ease-out), visibility 140ms; }
.bell-panel.show { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
.bell-head { padding: 13px 16px; font-weight: 600; font-size: var(--fs-3); border-bottom: 1px solid var(--line-2);
  position: sticky; top: 0; background: var(--surface-2); }
.bell-body { padding: 4px 0; }
.bell-item { display: flex; gap: 11px; padding: 10px 16px; align-items: flex-start; }
.bell-item + .bell-item { border-top: 1px solid var(--line-2); }
.bell-item .ic { flex: none; width: 26px; height: 26px; border-radius: 8px; display: flex; align-items: center;
  justify-content: center; background: var(--live-soft); color: var(--live); }
.bell-item .ic.x { background: var(--bad-soft); color: var(--bad); }
.bell-item .tx { min-width: 0; flex: 1; }
.bell-item .t { font-size: var(--fs-3); font-weight: 500; color: var(--ink); }
.bell-item .s { font-size: var(--fs-2); color: var(--ink-3); margin-top: 2px; }
.bell-item .tm { font-size: var(--fs-1); color: var(--ink-3); font-family: var(--mono); white-space: nowrap; }
.bell-empty { padding: 26px 16px; text-align: center; color: var(--ink-3); font-size: var(--fs-2); }
.bell-foot { display: block; padding: 11px 16px; text-align: center; font-size: var(--fs-2); font-weight: 500;
  color: var(--accent); border-top: 1px solid var(--line-2); position: sticky; bottom: 0; background: var(--surface-2); }
.bell-foot:hover { background: var(--surface-3); }

/* G4 · keyboard-shortcuts overlay */
.kbd-overlay { position: fixed; inset: 0; z-index: 400; display: none; align-items: center; justify-content: center;
  background: rgba(3,3,4,.66); backdrop-filter: blur(3px); padding: 20px; }
.kbd-overlay.show { display: flex; animation: fadeIn 140ms var(--ease-out); }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.kbd-card { width: 100%; max-width: 420px; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 22px 24px; box-shadow: 0 24px 60px rgba(0,0,0,.5); }
.kbd-card h3 { margin: 0 0 16px; font-size: var(--fs-4); }
.kbd-row { display: flex; align-items: center; justify-content: space-between; padding: 7px 0; border-bottom: 1px solid var(--line-2); }
.kbd-row:last-child { border-bottom: 0; }
.kbd-row .lbl { font-size: var(--fs-3); color: var(--ink-2); }
kbd { display: inline-block; min-width: 20px; text-align: center; padding: 2px 7px; font: 600 var(--fs-1) var(--mono);
  color: var(--ink); background: var(--surface-3); border: 1px solid var(--line); border-bottom-width: 2px;
  border-radius: 6px; margin-left: 4px; }

/* ============ Phase 4 — scale, analytics, features ============ */

/* K1 · pagination */
.pagination { display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px 0 4px; }
.pg-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 40px; min-height: 40px;
  padding: 0 14px; border-radius: var(--r-sm); border: 1px solid var(--line); background: var(--surface-3);
  color: var(--ink-2); font-weight: 600; transition: border-color var(--fast), color var(--fast), background-color var(--fast); }
.pg-btn:hover { border-color: var(--accent-line); color: var(--ink); }
/* opacity:.4 dropped Prev/Next to ~2:1 against the panel — unreadable rather
   than unavailable. Same treatment as every other disabled control. */
.pg-btn.disabled { opacity: 1; pointer-events: none; color: var(--ink-4);
  background: transparent; border-color: var(--line-2); }
.pg-info { font-size: var(--fs-2); color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* M2 · bulk selection */
/* The table bleeds 20px past the panel padding (.table-wrap margin), so a
   4px cell inset put the tick 8px off the panel wall — tighter than any
   other first column on the page. The gutter matches the panel's own. */
.col-check { width: 48px; text-align: left; }
.col-check input { cursor: pointer; }
/* the `hidden` attribute (JS toggles it at 0 selected) must win over this
   display:flex — a bare class selector otherwise leaves the empty bar visible. */
.bulk-bar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 11px 14px; margin-bottom: 12px;
  background: var(--accent-soft); border: 1px solid var(--accent-line); border-radius: var(--r-sm); }
.bulk-count { font-size: var(--fs-3); color: var(--ink); }
.bulk-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }
.sr-lab { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-2); color: var(--ink-3); }
.data-table-panel { position: relative; width: 100%; }
.content-grid + .data-table-panel { margin-top: 22px; }
.data-table-head { gap: 16px; align-items: center; }
.data-table-total { margin-top: 3px; color: var(--ink-3); font-size: var(--fs-2); font-variant-numeric: tabular-nums; }
.data-table-tools { margin-left: auto; }
.data-table-loading { position: absolute; z-index: 2; top: 14px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 7px; padding: 7px 11px; border: 1px solid var(--line);
  border-radius: var(--r-sm); color: var(--ink-2); background: var(--surface-3); font-size: var(--fs-2);
  opacity: 0; pointer-events: none; transition: opacity var(--fast); }
.data-table-loading.htmx-request { opacity: 1; }
.data-table-panel.htmx-swapping { opacity: .72; transition: opacity var(--fast); }
@media (max-width: 760px) {
  .data-table-head { align-items: stretch; flex-direction: column; }
  .data-table-tools { width: 100%; margin-left: 0; }
  .data-table-tools > .row, .data-table-tools form.row { width: 100%; }
}

/* N3 · onboarding */
.onboarding { margin-bottom: 16px; }
.ob-head { margin-bottom: 14px; }
.ob-title { margin: 0; font-size: var(--fs-5); }
.ob-sub { margin: 4px 0 0; color: var(--ink-3); font-size: var(--fs-3); }
.ob-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.ob-steps li { display: flex; align-items: center; gap: 12px; font-size: var(--fs-3); color: var(--ink-2); }
.ob-steps a { color: var(--ink); }
.ob-steps a:hover { color: var(--accent); }
.ob-num { flex: none; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center;
  justify-content: center; font: 700 var(--fs-2) var(--mono); background: var(--surface-3); color: var(--ink-2); border: 1px solid var(--line); }
.ob-steps li.done .ob-num { background: var(--live-soft); color: var(--live); border-color: transparent; }

/* L1 · activity feed */
.feed { list-style: none; margin: 0; padding: 0; }
.feed-item { display: flex; align-items: center; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line-2); }
.feed-item:last-child { border-bottom: 0; }
.feed-ic { flex: none; width: 24px; height: 24px; border-radius: 7px; display: flex; align-items: center; justify-content: center;
  background: var(--live-soft); color: var(--live); }
.feed-ic.bad { background: var(--bad-soft); color: var(--bad); }
.feed-text { flex: 1; min-width: 0; font-size: var(--fs-3); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feed-time { flex: none; font-size: var(--fs-2); }

/* L2/M3 · inline proportion bar under a label */
.minibar { display: block; height: 3px; margin-top: 5px; border-radius: 2px; background: var(--surface-3); overflow: hidden; }
.minibar > span { display: block; height: 100%; background: var(--accent); border-radius: 2px; }

/* sidebar badges (FarmLabs-style counters) */
.nav-badge {
  margin-left: auto; font: 600 var(--fs-1) var(--mono); letter-spacing: .02em;
  padding: 2px 7px; border-radius: 999px; white-space: nowrap;
  background: var(--surface-4); color: var(--ink-3); border: 1px solid var(--line-2);
  font-variant-numeric: tabular-nums;
}
.nav-badge.ok { background: var(--live-soft); color: var(--live); border-color: rgba(53,214,160,.18); }
.nav-badge.beta { background: var(--warn-soft); color: var(--warn); border-color: rgba(245,178,62,.18); }

/* chat page */
.chat-meta { display: flex; gap: 14px; font-size: var(--fs-2); align-items: center; }
.chat-wrap { display: grid; grid-template-columns: 250px minmax(0, 1fr); gap: 16px; align-items: start; }
.chat-channels { padding: 8px; display: flex; flex-direction: column; gap: 2px; }
.chat-chan {
  display: flex; flex-direction: column; gap: 2px; position: relative;
  padding: 9px 11px; border-radius: var(--r-sm); transition: background var(--fast) ease;
}
.chat-chan:hover { background: var(--surface-3); }
.chat-chan.active { background: var(--surface-3); }
.chat-chan-name { font-size: var(--fs-3); font-weight: 600; color: var(--ink-2); display: flex; align-items: center; gap: 6px; }
.chat-chan.active .chat-chan-name { color: var(--ink); }
.chat-chan-desc { font-size: var(--fs-2); color: var(--ink-3); }
.chat-chan .tab-n { position: absolute; right: 10px; top: 10px; }
.chat-main { display: flex; flex-direction: column; min-height: 460px; }
.chat-head { margin-bottom: 0; }
.chat-scroll { flex: 1; overflow-y: auto; max-height: 56vh; padding: 14px 2px; display: flex; flex-direction: column; gap: 14px; }
.chat-msg { display: flex; gap: 10px; align-items: flex-start; }
.chat-msg-body { min-width: 0; }
.chat-msg-head { display: flex; gap: 8px; align-items: baseline; font-size: var(--fs-2); }
.chat-msg-head b { font-weight: 600; }
.chat-msg-head .mono { font-size: var(--fs-1); }
.chat-msg-text { font-size: var(--fs-3); color: var(--ink-2); margin-top: 2px; overflow-wrap: anywhere; }
.chat-composer { display: flex; gap: 10px; align-items: center; border-top: 1px solid var(--line-2); padding-top: 12px; }
.chat-composer input[type=text] { flex: 1; }
@media (max-width: 980px) {
  .chat-wrap { grid-template-columns: 1fr; }
  .chat-channels { flex-direction: row; overflow-x: auto; }
  .chat-chan { flex-shrink: 0; }
  .chat-chan-desc { display: none; }
}

/* horizontal bar chart (drops) */
.hbar-chart { display: flex; flex-direction: column; gap: 9px; padding: 4px 0 6px; }
.hbar-row { display: grid; grid-template-columns: minmax(110px, 220px) minmax(0, 1fr) 70px; gap: 12px; align-items: center; }
.hbar-label { font-size: var(--fs-2); color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hbar-track { display: block; height: 14px; border-radius: 4px; background: var(--surface-3); overflow: hidden; }
.hbar-fill { display: block; height: 100%; border-radius: 4px; background: var(--live); opacity: .75; }
.hbar-row:nth-child(1) .hbar-fill { background: var(--warn); opacity: .9; }
.hbar-row:nth-child(2) .hbar-fill { background: var(--warn); opacity: .65; }
.hbar-val { font-size: var(--fs-2); color: var(--live); text-align: right; }

/* status tabs (client-side table filter) */
.tabs { display: flex; gap: 2px; flex-wrap: wrap; }
.tab {
  appearance: none; background: transparent; border: 0; cursor: pointer;
  font: 500 var(--fs-2) var(--sans); color: var(--ink-3); padding: 7px 11px;
  border-radius: var(--r-sm); transition: color var(--fast) ease, background var(--fast) ease;
  /* a tab rendered as <a> matched none of the button rules and stood ~31px
     next to its <button> siblings at 40px — same strip, two heights */
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 40px; text-decoration: none;
}
.tab:hover { color: var(--ink-2); background: var(--surface-3); }
.tab.active { color: var(--ink); background: var(--surface-3); }
.tab-n {
  font: 600 var(--fs-1) var(--mono); color: var(--ink-4); background: var(--surface-4);
  padding: 1px 6px; border-radius: 999px; font-variant-numeric: tabular-nums;
}
.tab.active .tab-n { color: var(--ink-2); }

/* machines list: live preview thumbnails + version pills */
.stat-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.stat-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* five metrics belong on one row when the screen has the width for it — the
   default four-column grid left the fifth card alone on a second row */
.stat-grid-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 1400px) { .stat-grid-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 900px)  { .stat-grid-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Screen thumbnail: a real 16:9 monitor, small. Fixed box so a row never
   changes height between "no signal" and a loaded frame. */
.screen-thumb { position: relative; display: block; width: 104px; aspect-ratio: 16 / 9;
  border-radius: 7px; border: 1px solid var(--line-2); background: #05070a;
  overflow: hidden; line-height: 0; }
.screen-thumb:hover { border-color: var(--line); }
.screen-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
.screen-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  padding: 0 8px; font: 500 var(--fs-1)/1.3 var(--sans); color: var(--ink-4); text-align: center;
  overflow: hidden; }
.screen-thumb[data-state="err"] .screen-note { color: var(--warn); }
.ver-pills { display: flex; gap: 5px; flex-wrap: wrap; }
.ver-pill { font: 500 var(--fs-1) var(--mono); color: var(--ink-3); padding: 2px 7px;
  border: 1px solid var(--line-2); border-radius: 999px; background: var(--surface-3);
  font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ============ Phase 5 — subscription gating ============ */

/* subscription-expiry banner (spans content, above the page) */
.sub-warning { display: flex; align-items: center; gap: 10px; margin: 0 34px; margin-top: 18px;
  padding: 10px 16px; border-radius: var(--r-sm); font-size: var(--fs-3); color: var(--warn);
  background: var(--warn-soft); border: 1px solid color-mix(in srgb, var(--warn) 32%, transparent); }
.sub-warning svg { flex: none; }
.sub-warning b { color: var(--warn); font-variant-numeric: tabular-nums; }
.sub-warning a { margin-left: auto; font-weight: 600; color: var(--accent); white-space: nowrap; }
@media (max-width: 760px) { .sub-warning { margin: 14px 14px 0; } }

/* no-subscription gate */
.gate-wrap { min-height: 60vh; display: flex; align-items: center; justify-content: center; }
.gate-card { max-width: 460px; width: 100%; text-align: center; padding: 34px 30px; }
.gate-ic { width: 60px; height: 60px; margin: 0 auto 18px; border-radius: 16px; display: flex;
  align-items: center; justify-content: center; background: var(--warn-soft); color: var(--warn); }
.gate-title { font-size: var(--fs-6); margin: 0 0 10px; }
.gate-sub { color: var(--ink-2); font-size: var(--fs-3); line-height: 1.6; margin: 0 0 20px; }
.gate-alert { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 0 0 12px;
  padding: 10px 12px; border: 1px solid rgba(255,91,107,.24); border-radius: var(--r-sm);
  background: var(--bad-soft); color: var(--bad); font-size: var(--fs-2); text-align: left; }
.gate-alert svg { flex: none; }
.gate-form { display: flex; gap: 10px; }
.gate-form input { flex: 1; text-align: center; font-family: var(--mono); }
.gate-foot { margin: 16px 0 0; font-size: var(--fs-2); color: var(--ink-3); }
.gate-foot a { color: var(--accent); }
@media (max-width: 560px) { .gate-form { flex-direction: column; } }

/* activation screen: the plan is the offer, so it gets the weight —
   the license key is the exception and lives folded away below it */
.gate-plan { text-align: left; padding: 16px 18px 18px; margin-bottom: 16px;
  border: 1px solid var(--line-2); border-radius: var(--r-sm); background: var(--surface-2); }
.gate-plan-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; }
.gate-plan-name { font-size: var(--fs-4); font-weight: 600; }
.gate-plan-price { font-size: var(--fs-6); font-weight: 600; font-variant-numeric: tabular-nums; }
.gate-plan-price small { font-size: var(--fs-2); font-weight: 400; color: var(--ink-3); }
.gate-plan-list { list-style: none; margin: 12px 0 16px; padding: 0;
  font-size: var(--fs-2); line-height: 1.75; color: var(--ink-2); }
.gate-plan-list li { padding-left: 18px; position: relative; }
.gate-plan-list li::before { content: "✓"; position: absolute; left: 0; color: var(--live); }
.gate-buy { width: 100%; justify-content: center; }
.gate-key { margin-top: 4px; text-align: left; }
.gate-key > summary { cursor: pointer; font-size: var(--fs-2); color: var(--ink-3);
  text-align: center; padding: 6px 0; }
.gate-key > summary:hover { color: var(--ink-2); }
.gate-key[open] > summary { margin-bottom: 12px; }

/* new-machine landing: the one-time link token, front and centre */
.created-panel { border-color: rgba(53,214,160,.28); background:
  linear-gradient(180deg, rgba(53,214,160,.05), transparent 70%), var(--surface-2); }
.token-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.token-code { font: 600 var(--fs-4)/1 var(--mono); letter-spacing: .08em; padding: 12px 14px;
  background: var(--surface-3); border: 1px solid var(--line-2); border-radius: var(--r-sm);
  user-select: all; word-break: break-all; }

/* danger zone: irreversible actions, visually separated from settings */
.danger-zone { border-color: rgba(255,91,107,.28); }
.danger-zone .ac-title, .danger-zone .panel-title { color: var(--bad); }
/* one irreversible action per row: what it does on the left, the single button
   on the right, never adjacent to an ordinary control */
.danger-row {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 16px 0; border-top: 1px solid rgba(255,91,107,.16);
}
.danger-row:first-of-type { border-top: 0; padding-top: 4px; }
.danger-tx { min-width: 0; }
.danger-tx .lab { display: block; font-size: var(--fs-3); font-weight: 600; color: var(--ink); }
.danger-tx .help { display: block; font-size: var(--fs-2); color: var(--ink-3); margin-top: 4px; line-height: 1.55; max-width: 62ch; }
.danger-tx .help b { color: var(--ink-2); font-weight: 600; }
.danger-row form { flex: none; }
@media (max-width: 620px) {
  .danger-row { flex-direction: column; align-items: stretch; }
  .danger-row form, .danger-row .btn-danger { width: 100%; }
}

/* ── Account ──────────────────────────────────────────────────
   Identity strip, then connections, then sign-in. Deliberately not a
   card: who you are is context for the page, not an object on it. */
.acct-id { display: flex; align-items: center; gap: 14px; margin: 0 0 20px; }
.acct-ava { flex: none; width: 44px; height: 44px; display: grid; place-items: center;
  background: var(--surface-2); border: 1px solid var(--line-2); border-radius: var(--r-sm); }
.acct-ava img { width: 30px; height: 30px; opacity: .9; }
.acct-id-tx { min-width: 0; }
.acct-email { font-size: var(--fs-5); font-weight: 600; color: var(--ink); letter-spacing: -.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-facts { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 7px;
  font-size: var(--fs-2); color: var(--ink-3); margin-top: 3px; }
.acct-facts .sep { color: var(--ink-4); }

/* one connected service: mark, what it is, what state it's in, one action */
.conn { display: flex; align-items: center; gap: 14px; }
.conn-mark { flex: none; width: 44px; height: 44px; display: grid; place-items: center;
  background: var(--surface-3); border: 1px solid var(--line-2); border-radius: var(--r-sm);
  color: var(--ink-2); }
.conn-mark svg { width: 22px; height: 17px; }
.conn-tx { min-width: 0; flex: 1; }
.conn-name { font-size: var(--fs-4); font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the handle is data, so it reads as data */
.conn-handle { font-family: var(--mono); font-size: var(--fs-2); font-weight: 500; color: var(--ink-3); }
.conn-handle::before { content: "· "; color: var(--ink-4); }
.conn-state { font-size: var(--fs-2); color: var(--ink-2); margin-top: 3px; }
.conn-state.muted { color: var(--ink-3); }
.conn-act { flex: none; }
.conn-note { font-size: var(--fs-2); color: var(--ink-3); margin: 16px 0 0;
  padding-top: 14px; border-top: 1px solid var(--line-2); }

/* sessions link shares the footer rule with Save, pushed to the far side */
.form-footer .acct-sessions { margin: 0 auto 0 -9px; }

@media (max-width: 560px) {
  /* only the action drops to its own row — the mark stays beside the name it
     belongs to instead of floating alone above it */
  .conn { flex-wrap: wrap; }
  .conn-act { flex: 1 1 100%; margin-top: 14px; }
  .conn-act .btn, .conn-act .btn-ghost { width: 100%; }
  .form-footer { flex-wrap: wrap; }
  .form-footer .acct-sessions { flex: 1 1 100%; margin: 0 0 4px -9px; }
  .form-footer > button { width: 100%; }
}

/* ============================================================
   FRONTEND PACKAGE 2026-07 — targeted screen fixes (P0–P2)
   ============================================================ */

/* P0: /admin/users action cell — desktop inline row, mobile single column.
   Reset lives outside the pw-form (button[form=pwf-…]) so it can sit next to Disable. */
.user-actions { display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end; }
.user-actions .ua-row { display: inline-flex; align-items: center; gap: 6px; }
.user-actions .ua-pw-lab { display: none; }               /* desktop: placeholder is enough */
.user-actions .ua-pw input { min-height: 32px; width: 130px; padding: 6px 10px; }
/* Disable and Delete ride as icons in the dense row (a password field already
   lives in this cell) and get their words back in the mobile card layout,
   where there is room and hover tooltips do not exist. */
.ua-ico .lbl { display: none; }

@media (max-width: 480px) {
  .as-cards td.col-actions { display: block; }            /* let the block use the card width */
  .user-actions { display: flex; flex-direction: column; align-items: stretch; gap: 10px; width: 100%; }
  .user-actions .ua-pw { display: flex; flex-direction: column; gap: 6px; text-align: left; }
  .user-actions .ua-pw-lab { display: block; font-size: var(--fs-1); font-weight: 600; letter-spacing: .04em;
    text-transform: uppercase; color: var(--ink-3); }
  .user-actions .ua-pw input { width: 100%; }
  .user-actions .ua-row { display: flex; gap: 8px; }
  .user-actions .ua-row > button, .user-actions .ua-row > form { flex: 1; }
  .user-actions .ua-row > form > button { width: 100%; }
  .user-actions .ua-delete > button { width: 100%; }      /* Delete = its own full danger row */
  .ua-ico .lbl { display: inline; }
}

/* P1: /notifications — grouped delivery card; save lives in .form-actions */
.cfg-block + .cfg-block { border-top: 1px solid var(--line-2); margin-top: 18px; padding-top: 18px; }
.cfg-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.cfg-name { font-weight: 600; font-size: var(--fs-4); }
.cfg-act { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
/* why a test button is unavailable, in words next to it — a dimmed button alone
   reads as broken rather than as "not yet" */
.cfg-reason { display: none; font-size: var(--fs-2); color: var(--ink-3); }
.cfg-reason.show { display: inline; }
@media (max-width: 480px) {
  .cfg-head { align-items: flex-start; }
  .cfg-act { width: 100%; justify-content: space-between; }
}

/* secret input: masked by default, with a reveal control inside the field */
.secret-field { position: relative; display: flex; align-items: center; }
.secret-field input { padding-right: 52px; font-family: var(--mono); font-size: var(--fs-3); letter-spacing: .02em; }
.secret-field input[type=password] { letter-spacing: .16em; }
/* the reveal control is a real button, not a decoration inside the field:
   40×40 like every other icon action, and the same radius as its siblings */
.secret-toggle {
  position: absolute; right: 3px; width: 40px; min-width: 40px; height: 40px; min-height: 40px;
  background: transparent; border: 0; box-shadow: none; color: var(--ink-3); border-radius: var(--r-sm);
}
.secret-toggle:hover { background: var(--hover); color: var(--ink); }
.secret-toggle.is-revealed { color: var(--ink); }

/* P1: /admin/audit — tighter mobile cards (per_page also cut 50→30 server-side) */
@media (max-width: 760px) {
  .audit-cards.as-cards tr { padding: 2px 14px; margin-bottom: 10px; }
  .audit-cards.as-cards td { padding: 7px 0; font-size: var(--fs-2); }
  .audit-cards.as-cards td::before { font-size: var(--fs-2); }
}
.list-toolbar input[type="date"].compact { padding: 6px 10px; }

/* P2: /bots empty-state — shorter card (~230px), single CTA set */
.empty-state--sm { padding: 30px 20px; }
.empty-state--sm .empty-ic { width: 46px; height: 46px; margin-bottom: 12px; }
.empty-state--sm .empty-ic svg { width: 22px; height: 22px; max-width: 22px; max-height: 22px; }

/* P2: sidebar account — fit common emails fully (title tooltip covers very long ones) */
.user-card { gap: 10px; }
.user-card .ava { width: 32px; height: 32px; }
.user-card .nm { font-size: var(--fs-2); }

/* FarmLabs parity: /subscriptions plan cards */
.plan-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-bottom: 20px; }
.plan-card { background: var(--surface-2); border: 1px solid var(--line-2); border-radius: var(--r-lg);
  padding: 20px 20px 18px; display: flex; flex-direction: column; gap: 14px; }
.plan-card.current { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* min-height = the "Current plan" chip. Without it only the current card's head
   grew by the chip, and its price and list sat 3px lower than the card beside it. */
.plan-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; min-height: 24px; }
.plan-name { font-weight: 700; font-size: var(--fs-4); }
.plan-price { font: 700 var(--fs-8)/1 var(--sans); letter-spacing: -.02em; }
.plan-price small { font-size: var(--fs-2); font-weight: 500; color: var(--ink-3); letter-spacing: 0; }
.plan-feats { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; flex: 1; }
.plan-feats li { display: flex; align-items: center; gap: 8px; font-size: var(--fs-3); color: var(--ink-2); }
.plan-feats svg { color: var(--live); flex: none; }
.plan-cta { justify-content: center; text-align: center; }
@media (max-width: 900px) { .plan-cards { grid-template-columns: 1fr; } }
/* Two plans side by side on /subscriptions. A modifier outranks the media rule
   above, so it needs its own narrow-screen stacking. */
.plan-cards.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 900px) { .plan-cards.two { grid-template-columns: 1fr; } }
.plan-act { display: flex; flex-direction: column; gap: 10px; }
.plan-act form, .plan-act > .btn, .plan-act > .btn-ghost, .plan-act > button { width: 100%; }
.plan-act form > button, .plan-act > .btn, .plan-act > .btn-ghost { width: 100%; justify-content: center; text-align: center; }
.plan-act .plan-renew { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  font-size: var(--fs-2); }
.plan-note { display: block; text-align: center; font-size: var(--fs-2); color: var(--ink-3);
  padding: 10px 0; border-top: 1px solid var(--line-2); }

/* FarmLabs parity: dashboard charts & sparklines */
.stat-spark { display: block; width: 100%; height: 22px; margin-top: 8px; }
.stat-spark polyline { fill: none; stroke: var(--live); stroke-width: 1.6; vector-effect: non-scaling-stroke; }
.chart-legend { margin: 10px 2px 2px; font-size: var(--fs-2); color: var(--ink-3); display: flex; align-items: center; gap: 6px; }
.chart-legend-row { display: flex; gap: 14px; font-size: var(--fs-2); color: var(--ink-3); }
.chart-legend-row span { display: inline-flex; align-items: center; gap: 6px; }
.lg-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.xpchart-bar.bar-warm { fill: var(--warn); opacity: .55; }
.xpchart-bar.bar-cool { fill: #5B8DEF; opacity: .55; }
.ver-pills-stack { flex-direction: column; align-items: flex-start; gap: 3px; }
.ver-pill.ok { color: var(--live); border-color: rgba(53,214,160,.3); background: rgba(53,214,160,.08); }
.linklike { background: none; border: 0; padding: 0; color: var(--accent); font: inherit; font-size: var(--fs-2); cursor: pointer; text-decoration: underline; }

/* FarmLabs parity: info banner */
.info-banner { display: flex; align-items: flex-start; gap: 10px; background: rgba(91,141,239,.08);
  border: 1px solid rgba(91,141,239,.25); color: var(--ink-2); border-radius: var(--r-sm);
  padding: 11px 14px; font-size: var(--fs-2); margin-bottom: 16px; }
.info-banner svg { color: #5B8DEF; margin-top: 1px; }
/* slot counter beside the extra-keys panel: keys are unlimited-ish, slots are
   the number that actually constrains the customer */
.key-slots { font-size: var(--fs-2); color: var(--ink-3); white-space: nowrap; }
/* the key itself, as one unbreakable token you click to copy */
.key-chip {
  display: inline-block; font-size: var(--fs-2); letter-spacing: .02em; white-space: nowrap;
  padding: 4px 8px; border-radius: 6px; cursor: pointer;
  background: var(--surface-3); border: 1px solid var(--line-2); color: var(--ink);
  transition: border-color var(--fast), background-color var(--fast);
}
.key-chip:hover { background: var(--surface-4); border-color: var(--line); }
/* masked by default — a live credential should not be readable over a shoulder
   or in a screenshare just because the list happened to be open */
.key-chip[data-masked] { cursor: default; letter-spacing: .12em; color: var(--ink-3); }
.key-chip[data-masked]:hover { background: var(--surface-3); border-color: var(--line-2); }
.key-cell { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
.key-reveal.is-revealed { color: var(--ink); }
/* In card mode the row is a flex line and the chip is one unbreakable token, so
   chip + reveal button together were wider than the card and the eye hung off
   the right edge. The chip is the part that can give. */
@container rows (max-width: 900px) {
  .key-cell .key-chip { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
}
/* a revoked key is history: still legible, no longer competing for attention */
tr.row-spent > td { opacity: .62; }
tr.row-spent > td:has(.row-actions), tr.row-spent > td .status-chip { opacity: 1; }
tr.row-spent:hover > td { opacity: 1; }
.key-slots b { color: var(--ink); font-family: var(--mono); font-variant-numeric: tabular-nums; }
.plan-slots { font-size: var(--fs-2); }
.btn-warn { color: var(--warn); border-color: rgba(245,178,62,.35); }
.btn-warn:hover { background: rgba(245,178,62,.1); }

/* Toggle filters (e.g. Bots → "Without proxy"): a pressed chip has to read as
   held down, otherwise a filtered list looks like an empty one. */
.btn-ghost[aria-pressed="true"] {
  background: var(--surface-3);
  border-color: var(--line);
  color: var(--ink);
}
.btn-ghost[aria-pressed="true"] .tab-n { color: var(--ink-2); }

/* Table cells holding data the operator pastes in — proxy hosts, provider
   logins, rejection reasons. These have no spaces to break on, so without an
   explicit rule a long one runs straight out of its card on narrow screens. */
.cell-wrap { overflow-wrap: anywhere; }
.sub-line {
  display: block;
  font-size: var(--fs-1);
  line-height: 1.45;
  overflow-wrap: anywhere;
}

/* Groups several lines inside one table cell. Needed because a card-mode cell
   is a flex row: without a wrapper each line becomes its own narrow column. */
.cell-stack { min-width: 0; }

/* Page-head actions wrap instead of pushing the page wider than the viewport. */
.page-head .row { flex-wrap: wrap; }

/* The bots table is already wide; the proxy select must not add to that. */
td[data-label="Proxy"] select { max-width: 168px; }

/* File picker styled as one of our buttons; the native control is kept in the
   DOM (it is what actually carries the file) but visually replaced. */
.file-pick { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; }
.file-pick input[type="file"] {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.file-pick:focus-within { border-color: var(--focus, var(--line)); }

/* the same idea, but with our own button and our own English copy: the native
   control renders "Выберите файл / Файл не выбран" in the browser's locale */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.file-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; margin-top: 8px; }
.file-row > * { min-width: 0; }
.file-name { font-size: var(--fs-2); color: var(--ink-3); overflow-wrap: anywhere; }
.file-name.has-file { color: var(--ink-2); font-family: var(--mono); font-size: var(--fs-2); }
/* the hidden input still takes focus; show the ring on the button standing in for it */
.file-field input[type=file]:focus-visible + .file-row #file-pick-btn,
.file-field input[type=file]:focus-visible ~ .file-row > button:first-child {
  box-shadow: 0 0 0 2px var(--focus);
}

/* session device name: a short label with the full user agent behind it */
.dev-name { border-bottom: 1px dashed var(--line); cursor: help; }
.dev-name:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--focus); border-radius: var(--r-xs); }

/* Three-up form row for short qualifying fields. Top-aligned on purpose: with
   bottom alignment a taller help text under one field drags its neighbours
   down, and nothing in the row lines up. */
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: start; }
/* opt-in for grid-2 rows made of form fields — the default stretch is right for
   the card layouts that use grid-2 elsewhere, so this is not a global change */
.grid-2.form-row { align-items: start; }
@media (max-width: 760px) { .grid-3 { grid-template-columns: 1fr; } }

/* ============================================================
   RESPONSIVE CONTRACT — mobile QA pass
   A <select> sizes itself to its longest <option>, so one long group name or
   customer email used to make the whole document wider than the phone
   (/bots +305px, /admin/audit +142px at 320). These rules keep every control
   inside its container and let action rows wrap instead of pushing the page.
   ============================================================ */
input, select, textarea { max-width: 100%; }
select { text-overflow: ellipsis; }

/* Action groups wrap; buttons keep their label rather than being squeezed into
   an unreadable stub (a 60px button around 170px of text). */
.row { flex-wrap: wrap; }
.btn, .btn-ghost, .btn-danger, .btn-sm { flex-shrink: 0; }

/* List toolbars: search owns the row, filters sit beside it and wrap under. */
.list-toolbar { flex-wrap: wrap; }
.list-toolbar > .search-field { flex: 1 1 220px; }
.list-toolbar select { max-width: 220px; }
.toolbar-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }

@media (max-width: 640px) {
  .list-toolbar { align-items: stretch; }
  .list-toolbar > .search-field { flex-basis: 100%; max-width: none; }
  .toolbar-controls { margin-left: 0; width: 100%; }
  /* min-width:auto would keep the select as wide as its longest option — the
     exact reason /bots overflowed. Zeroed, it takes what is left of the row so
     the export button stays beside the filters instead of wrapping alone. */
  .toolbar-controls > select { flex: 1 1 96px; min-width: 0; max-width: 100%; }
  /* secondary action drops its label and becomes a 40x40 target — the tooltip
     and aria-label carry the meaning */
  .icon-collapse { width: 40px; min-width: 40px; height: 40px; padding: 0; justify-content: center; }
  .icon-collapse .lbl { display: none; }
}
.icon-collapse svg { flex: none; }

/* Filter forms stack on phones: field full width, then a full-width button. */
.filter-form { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
/* A <select> is width:100% by default, which in a flex row claimed the whole
   line and pushed the button onto a third row. Here each control asks for what
   it needs and the search field absorbs the rest. */
.filter-form input, .filter-form select { min-width: 0; }
.filter-form select { width: auto; flex: 0 0 auto; }
.filter-form input[type=search] { flex: 1 1 150px; }
.filter-form button { flex: 0 0 auto; }
@media (max-width: 480px) {
  .filter-form { display: grid; grid-template-columns: minmax(0, 1fr); }
  .filter-form > * { width: 100%; min-height: 40px; }
}

/* Figures never break mid-value and line up column-wise. */
.stat .v, .plan-price, .ac-row .v, td.num, .num, .mono { font-variant-numeric: tabular-nums; }
/* `nowrap` belongs to a figure in a tile that is sized for it. It used to be set
   on every `.ac-row .v` as well — but those values are not all figures: a status
   line, a failure reason, an "unavailable" note. Forced onto one line, each of
   them pushed its card, its grid column and finally the whole page wider than
   the viewport. Values in a readout row wrap; the numeric ones are short enough
   that they never need to. */
.stat .v { white-space: nowrap; }
.ac-row .v { white-space: normal; }

/* Long names stay readable: ellipsis plus a title tooltip, never a hard cut. */
.row-link { display: inline-block; max-width: 100%; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }

/* Empty states stay compact on phones — a full-height panel saying "nothing
   here" wastes the screen the user came to work on. */
@media (max-width: 640px) {
  .empty-state { padding: 26px 16px; }
  .empty-state .empty-ic { width: 44px; height: 44px; margin-bottom: 10px; }
  .empty-state .empty-ic svg { width: 22px; height: 22px; }
}

/* Page header on phones: title owns its line, actions wrap under it and the
   primary action stays reachable instead of being squeezed to a stub. */
@media (max-width: 640px) {
  .page-head { flex-wrap: wrap; gap: 10px; }
  .page-head > .row { width: 100%; gap: 8px; }
  /* primary action first and full width; secondary actions pair two per row */
  .page-head > .row > .btn { flex: 1 1 100%; order: -1; justify-content: center; }
  .page-head > .row > .btn-ghost { flex: 1 1 calc(50% - 4px); justify-content: center; }
}

/* Drops: reward-first, dense without falling back to spreadsheet labels. */
.drops-page-head { align-items: flex-end; }
/* Drops-by-group overview: the compact headline (value/count/ready per group)
   that replaces scrolling a per-account list, and doubles as the filter for the
   detail below. Each card is a stretched link; its Claim-all button clicks
   through on top. */
.drop-groups {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 12px; margin-bottom: 14px;
}
.drop-group-card {
  position: relative; display: flex; flex-direction: column; gap: 5px;
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-lg); padding: 13px 15px; text-decoration: none; min-width: 0;
  transition: border-color var(--fast) var(--ease-out), background var(--fast) var(--ease-out);
}
.drop-group-card:hover { border-color: var(--line); background: var(--surface-3); }
.drop-group-card.is-active { border-color: var(--accent-line); background: var(--surface-3);
  box-shadow: inset 0 0 0 1px var(--accent-line); }
.dg-link { position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.dg-name { color: var(--ink); font-weight: 600; font-size: var(--fs-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dg-value { color: var(--live); font: 650 var(--fs-4)/1 var(--mono); letter-spacing: -.01em; }
.dg-value .drop-price-unavailable { font-size: var(--fs-2); }
.dg-meta { color: var(--ink-3); font-size: var(--fs-2); font-variant-numeric: tabular-nums; }
.dg-owner { color: var(--ink-3); font-size: var(--fs-2); min-width: 0; }
.dg-owner .admin-owner { max-width: 100%; }
.dg-ready { color: var(--warn); font-size: var(--fs-2); font-weight: 500; }
.dg-ready.none { color: var(--ink-4); font-weight: 400; }
.dg-claim { position: relative; z-index: 2; display: flex; align-items: center;
  justify-content: space-between; gap: 8px; margin-top: 3px; }
.drops-history-scope { color: var(--ink-2); }
.drop-status-hidden { margin: 9px 0 0; color: var(--ink-4); font-size: var(--fs-2); }

/* Drops aggregated by skin: one compact row per item — thumb, name, ×count and
   total value — so thousands of individual drops never become a scroll. */
.drop-item-list { display: flex; flex-direction: column; }
.drop-item-row {
  display: grid; grid-template-columns: 56px minmax(0, 1fr) auto auto;
  align-items: center; gap: 14px; padding: 9px 0; border-top: 1px solid var(--line-2);
}
.drop-item-row:first-child { border-top: 0; }
.drop-item-thumb { width: 56px; height: 42px; border-radius: var(--r-sm); object-fit: cover;
  background: var(--surface-3); flex: none; }
span.drop-item-thumb { display: inline-flex; align-items: center; justify-content: center; color: var(--ink-4); }
.drop-item-copy { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.drop-item-name { color: var(--ink); font-weight: 600; font-size: var(--fs-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drop-item-rarity { color: var(--ink-3); font-size: var(--fs-2); }
.drop-item-count { color: var(--ink-2); font: 600 var(--fs-3)/1 var(--mono); font-variant-numeric: tabular-nums; }
.drop-item-price { display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
  text-align: right; white-space: nowrap; }
.drop-item-price strong { color: var(--live); font: 650 var(--fs-3)/1 var(--mono); }
.drop-item-price span { color: var(--ink-4); font-size: var(--fs-2); }

.drops-setup-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px; margin-bottom: 12px;
}
.drops-compact-panel { margin: 0; padding: 16px; min-width: 0; }
.drops-section-head { margin-bottom: 10px; }
.drops-section-head p, .drops-history-head p {
  margin: 3px 0 0; color: var(--ink-3); font-size: var(--fs-2); line-height: var(--lh-snug);
}
.drop-machine-list, .drop-status-list { display: flex; flex-direction: column; }
.drop-machine-row {
  display: grid; grid-template-columns: minmax(130px, 1fr) auto minmax(130px, 160px) auto;
  align-items: center; gap: 12px; min-height: 54px; padding: 7px 0;
  border-top: 1px solid var(--line-2);
}
.drop-machine-identity, .drop-status-identity, .drop-group-row > div {
  display: flex; flex-direction: column; min-width: 0;
}
.drop-machine-name, .drop-status-identity a, .drop-group-row strong {
  color: var(--ink); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.drop-machine-identity > span:last-child, .drop-status-identity > span,
.drop-group-row > div > span {
  color: var(--ink-3); font-size: var(--fs-2);
}
.admin-inline-owner .admin-owner { max-width: 100%; }
.drop-toggle-copy { display: inline-flex; align-items: center; gap: 9px; color: var(--ink-2); font-size: var(--fs-2); }
/* 40px is the control height everywhere else in the panel, and 44px is the
   touch minimum below 760px — a 36px select here was a third size. */
.drop-machine-row select { min-height: 40px; padding-top: 8px; padding-bottom: 8px; }
@media (max-width: 760px) { .drop-machine-row select { min-height: 44px; } }

.drop-status-row {
  display: grid; grid-template-columns: minmax(120px, 1fr) auto;
  align-items: center; gap: 12px; min-height: 54px; padding: 8px 0;
  border-top: 1px solid var(--line-2);
}
.drop-status-row.has-action { grid-template-columns: minmax(120px, 1fr) auto minmax(82px, auto); }
.drop-status-state { display: flex; align-items: center; justify-content: flex-end; gap: 9px; min-width: 0; }
.drop-choice-count { color: var(--ink-3); font-size: var(--fs-2); white-space: nowrap; }
.drop-status-action { min-width: 82px; display: flex; justify-content: flex-end; font-size: var(--fs-2); }

.drop-overview {
  display: flex; align-items: center; flex-wrap: wrap; gap: 9px;
  min-height: 46px; margin-bottom: 12px; padding: 9px 14px;
  color: var(--ink-2); font-size: var(--fs-3);
  background: linear-gradient(90deg, var(--live-soft), transparent 64%);
  border: 1px solid rgba(53,214,160,.18); border-radius: var(--r);
}
.drop-overview strong { color: var(--ink); font-family: var(--mono); font-size: var(--fs-4); }
.drop-overview-separator { width: 3px; height: 3px; border-radius: 50%; background: var(--ink-4); }
.drop-overview-note { margin-left: auto; color: var(--ink-3); font-size: var(--fs-2); }

.drops-content {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px;
  align-items: start; margin-bottom: 16px;
}
.drops-content.with-groups { grid-template-columns: minmax(0, 1fr) minmax(210px, 290px); }
.drops-history-panel, .drops-groups-panel { margin: 0; padding: 16px; min-width: 0; }
.drops-history-head { margin-bottom: 10px; }
.drops-history-actions { display: flex; align-items: center; gap: 8px; min-width: min(100%, 360px); }
.drops-history-actions .search-field { margin: 0; min-width: 150px; }
.drop-card-list, .drop-group-list { display: flex; flex-direction: column; }
.drop-card {
  display: grid; grid-template-columns: 64px minmax(0, 1fr) auto;
  align-items: center; gap: 13px; min-height: 70px; padding: 10px 0;
  border-top: 1px solid var(--line-2);
}
.drop-card-thumb {
  width: 64px; height: 48px; flex: none; padding: 4px; object-fit: contain;
  background: radial-gradient(circle at 50% 45%, var(--surface-4), var(--surface-2));
  border: 1px solid var(--line-2); border-radius: var(--r-xs);
}
.drop-card-thumb-empty { display: grid; place-items: center; color: var(--ink-4); }
.drop-card-copy { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.drop-card-title-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.drop-card-name {
  min-width: 0; color: var(--ink); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.drop-card-title-row .pill-ok, .drop-card-title-row .pill-warn {
  flex: none; padding: 2px 7px; font-size: var(--fs-1);
}
.drop-card-rarity { color: var(--ink-3); font-size: var(--fs-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drop-card-meta { display: flex; align-items: center; gap: 6px; margin-top: 2px; color: var(--ink-3); font-size: var(--fs-2); min-width: 0; }
.drop-card-meta a { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drop-card-meta a:hover { color: var(--ink); }
.drop-card-price { display: flex; flex-direction: column; align-items: flex-end; min-width: 102px; }
.drop-card-price strong { color: var(--live); font: 700 var(--fs-5)/var(--lh-tight) var(--mono); font-variant-numeric: tabular-nums; }
.drop-card-price > span { margin-top: 3px; color: var(--ink-4); font-size: var(--fs-1); }
.drop-card-price .drop-price-unavailable, .drop-price-unavailable {
  color: var(--ink-3); font: 500 var(--fs-2)/var(--lh-snug) var(--sans);
}
.drops-empty-state { margin: 2px 0 0; padding: 34px 16px; }

.drop-group-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-height: 56px; padding: 9px 0; border-top: 1px solid var(--line-2);
}
.drop-group-row > div { max-width: calc(100% - 72px); }
.drop-group-row > b { color: var(--live); font: 600 var(--fs-3)/var(--lh-tight) var(--mono); white-space: nowrap; }

@media (max-width: 1080px) {
  .drops-setup-grid { grid-template-columns: 1fr; }
  .drops-content.with-groups { grid-template-columns: minmax(0, 1fr) minmax(200px, 250px); }
}
@media (max-width: 760px) {
  .drops-page-head { align-items: flex-start; flex-wrap: wrap; }
  .drops-page-head .tabs { width: 100%; }
  .drops-page-head .tab { flex: 1; text-align: center; }
  .drop-machine-row { grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
  .drop-machine-row select { grid-column: 1; }
  .drop-machine-row button { grid-column: 2; }
  .drop-toggle-copy { justify-self: end; }
  .drop-status-row, .drop-status-row.has-action { grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
  .drop-status-state { justify-content: flex-end; }
  .drop-status-action { grid-column: 1 / -1; min-width: 0; }
  .drop-status-action form, .drop-status-action button { width: 100%; }
  .drops-content.with-groups { grid-template-columns: 1fr; }
  .drop-overview-note { width: 100%; margin-left: 0; }
  .drops-history-head { align-items: flex-start; }
  .drops-history-actions { width: 100%; max-width: none; }
}
@media (max-width: 480px) {
  .drops-compact-panel, .drops-history-panel, .drops-groups-panel { padding: 13px; }
  .drop-toggle-copy > span:first-child { display: none; }
  .drop-card { grid-template-columns: 52px minmax(0, 1fr); gap: 10px; }
  .drop-card-thumb { width: 52px; height: 42px; }
  .drop-card-price { grid-column: 2; align-items: flex-start; min-width: 0; padding-bottom: 2px; }
  .drop-card-price > span { display: none; }
  .drop-card-title-row .pill-ok, .drop-card-title-row .pill-warn { display: none; }
  .drop-choice-count { display: none; }
}


/* ============================================================
   MOTION — 120-180ms, and never on anything that changes size.
   Everything here is opacity/transform only, so no reflow and no layout shift.
   The global prefers-reduced-motion block above collapses all of it to 1ms.
   ============================================================ */
/* an inline form or a segmented pane appearing in place */
#new-group-panel:not([hidden]),
#generate:not([hidden]),
[role="tabpanel"]:not([hidden]) {
  animation: paneIn 150ms var(--ease-out) both;
}
@keyframes paneIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
/* the segmented control's own state change */
.segmented .seg-btn { transition: background-color 140ms ease, color 140ms ease; }
/* dirty/saved state: the status word fades rather than snapping between phrases */
.form-actions .fa-status { transition: color 160ms ease, opacity 160ms ease; }
/* the sticky save bar arriving and leaving */
.settings-save { transition: box-shadow 160ms var(--ease-out), border-color 160ms ease; }
/* local nav highlight following the reader */
.set-nav a { transition: color 140ms ease, background-color 140ms ease; }

/* onboarding / empty-state call to action */
.ob-foot { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.onboarding-solo { max-width: 720px; }

/* ============================================================
   MACHINE SETTINGS — six named sections instead of one 3200px scroll.
   The content did not change; it grew headings, an anchor per section and a
   sticky map beside it, so a long form stops being a single wall.
   ============================================================ */
.set-h {
  font: 600 var(--fs-2) var(--sans); text-transform: uppercase; letter-spacing: .09em;
  color: var(--ink-3); margin: 28px 2px 10px; scroll-margin-top: 84px;
}
.set-h:first-child { margin-top: 0; }
/* No nested sticky: the whole .aside-col is already sticky, so a second sticky
   here made the "On this page" nav detach and overlap the Identity/Status cards
   below it as the taller-than-viewport column scrolled. */
.set-nav { display: flex; flex-direction: column; gap: 1px; }
.set-nav a {
  display: block; padding: 8px 10px; margin: 0 -10px; border-radius: var(--r-sm);
  font-size: var(--fs-3); color: var(--ink-3); line-height: 1.3;
  transition: color var(--fast) ease, background-color var(--fast) ease;
}
.set-nav a:hover { color: var(--ink); background: var(--hover); }
.set-nav a.current { color: var(--ink); background: var(--surface-3); font-weight: 500; }
.set-nav .set-nav-danger { color: var(--bad); margin-top: 6px; padding-top: 12px; border-top: 1px solid var(--line-2); border-radius: 0; }
.set-nav .set-nav-danger:hover { color: var(--bad); background: var(--bad-soft); }
/* the save bar rides along ONLY while there is something to save; pristine it is
   an ordinary footer sitting at the end of the form */
.settings-save:has(.form-actions[data-state="dirty"]),
.settings-save:has(.form-actions[data-state="saving"]) {
  position: sticky; bottom: 14px; z-index: 20;
  box-shadow: 0 -1px 0 var(--line-2) inset, 0 18px 40px -14px rgba(0,0,0,.7);
  border-color: var(--line);
}
@media (max-width: 1024px) {
  .set-nav { position: static; flex-direction: row; flex-wrap: wrap; gap: 6px; }
  .set-nav .ac-title { width: 100%; }
  .set-nav a { margin: 0; border: 1px solid var(--line-2); }
  .set-nav .set-nav-danger { border-top: 1px solid var(--line-2); padding-top: 8px; margin-top: 0; border-radius: var(--r-sm); }
}

/* ============================================================
   RECORD CARDS — layout override, deliberately near the end.
   Card mode restates td/tr geometry, so anything declared after it at
   equal specificity silently wins. That is how `td.cell-flex{max-width:0}`
   (a desktop grow-and-truncate trick, 45 lines below the old position)
   came to zero the width of the Name cell in every card: on a phone the
   /machines list showed cards with no machine name in them.
   Triggered by the table's OWN available width, so the same rule covers a
   phone, a tablet, and a laptop viewing a page whose aside column has eaten
   the room. Nothing is clipped and the page never pans.
   ============================================================ */
@container rows (max-width: 900px) {
  table, tbody, tr, td { display: block; }
  /* the wrapper bleeds 20px past the panel on each side so a wide table's first
     column lines up with the panel text; cards keep that inset as padding */
  table { width: 100%; padding: 0 20px; }
  /* Drop the header row, and only the header row. Some tables wrap it in
     <thead>, others emit a bare <tr> of <th> — so match on "row made of th"
     rather than on position. The previous rule hid `tr:first-child`, which also
     matches the first <tr> inside <tbody>: on every table that did use <thead>,
     the first record silently vanished on narrow screens. */
  table > thead, tr:has(> th) { display: none; }
  table > tbody > tr {
    border: 1px solid var(--line-2); border-radius: var(--r-lg); background: var(--surface-2);
    padding: 4px 16px; margin-bottom: 12px;
  }
  table > tbody > tr:hover td { background: transparent; }
  table > tbody > tr:last-child { margin-bottom: 0; }
  td {
    width: auto; max-width: none; display: flex; align-items: center; gap: 16px;
    justify-content: space-between; text-align: right; white-space: normal;
    padding: 10px 0; border-bottom: 1px solid var(--line-2); min-width: 0;
  }
  td > * { min-width: 0; }
  tr td:last-child { border-bottom: 0; }
  /* the label the header row used to carry, restated per cell */
  td::before {
    content: attr(data-label); color: var(--ink-3); font-size: var(--fs-2); font-weight: 600;
    text-transform: uppercase; letter-spacing: .03em; text-align: left; flex: none;
  }
  td[data-label=""]::before, td:not([data-label])::before { content: none; }
  td[data-label=""], td:not([data-label]) { justify-content: flex-end; }
  /* per-page: drop a redundant column from the card (kept in the wide table) */
  .mob-hide, .md-hide, .lg-hide { display: none; }
  /* the action strip becomes a full-width footer inside the card, so no button
     can end up beyond the card's right edge */
  td.col-actions {
    display: flex; width: auto; padding-top: 12px; padding-bottom: 4px;
    border-bottom: 0; border-top: 1px solid var(--line-2); margin-top: 2px;
  }
  .row-actions { flex-wrap: wrap; justify-content: flex-end; gap: 8px; min-width: 0; }
  td .compact, td select.compact { min-width: 0; width: auto; max-width: 100%; }
  td.cell-flex { width: auto; max-width: none; }
  td.cell-flex > * { max-width: 100%; white-space: normal; }
  .cell-trunc { max-width: 100%; }
}

/* ============================================================
   TOUCH SCALE — last word on control size, deliberately at the end.
   This block used to live mid-file, and every component rule declared after
   it with the same specificity quietly won: .tab re-declared min-height:40px
   700 lines later and phones got a 40px tab strip back. Size is a contract,
   so it is stated where nothing can follow it.
   ============================================================ */
@media (max-width: 760px) {
/* ---- touch scale: 44px, no exceptions ----
   40px is the mouse target. A finger needs 44, and "почти 44" is the same
   miss as 32 — so every seat in the panel steps up together here rather than
   one class at a time. Only the type stays where it is. */
button, .btn, .btn-ghost, .btn-danger, .btn-sm, .tab, .pg-btn,
button.btn-sm, a.btn-sm, a.btn, a.btn-ghost, a.btn-danger {
  min-height: 44px;
}
.btn-sm, button.btn-sm, a.btn-sm { padding: 9px 14px; font-size: var(--fs-3); }
.btn-icon, .btn-icon.btn-sm, .icon-btn {
  min-width: 44px; min-height: 44px; width: auto; height: auto; padding: 0;
}
/* text inputs only: a checkbox or radio must keep its own box, and a 44px
   tall tick would wreck every table's select-column and toggle row */
input:not([type=checkbox]):not([type=radio]):not([type=hidden]),
select, textarea { min-height: 44px; }
.pg-btn { min-width: 44px; }
/* the avatar-only profile button, the hamburger and the reveal control inside
   a secret field are the three that used to fall short on a phone */
.nav-toggle { width: 44px; height: 44px; min-width: 44px; min-height: 44px; }
.topbar { grid-template-columns: 44px minmax(0, 1fr) auto; }
.secret-field .secret-toggle { width: 44px; min-width: 44px; height: 44px; min-height: 44px; }
.secret-field input { padding-right: 56px; }
/* a text-link button ("Deselect all") still has to be hittable */
.linklike { display: inline-flex; align-items: center; min-height: 44px; }
/* A 38×22 switch is a mouse target. Grow the *label's* box to 44px and centre
   the 22px track inside it — the visual stays put, the tappable area doesn't.
   Growing it by pushing the input outside its parent instead would have added
   scrollable overflow to whatever row it sits in. */
.switch { height: 44px; }
.switch .track { inset: auto 0; top: 50%; height: 22px; transform: translateY(-50%); }
/* the chip is its own tap target, so it gets the height rather than a wrapper */
.map-chip span { padding: 11px 14px; }
}

/* ============================================================
   HIT TARGETS — the controls the button audit did not reach.
   A responsive sweep of 31 routes × 4 widths found five kinds of control still
   below the panel's own minimum: search boxes at 38px, switches at 22px, time
   fields at 28px, table checkboxes at 13px, and toolbar controls that a mobile
   .filter-form rule pinned back down to 40px.

   Both halves are explicitly bounded so neither can leak into the other's
   width — this block is last in the file, and an unbounded desktop rule here
   would silently undo the 44px touch scale above it.
   ============================================================ */
@media (min-width: 761px) {
  /* 38px in a row of 40px buttons is not density, it is a miss */
  .search-field input { min-height: 40px; }
  .time-field input[type=time] { min-height: 40px; }
  /* same trick the touch block uses: the label's box grows, the 22px track
     stays where it is and stays the size it looks */
  .switch { height: 40px; }
  .switch .track { inset: auto 0; top: 50%; height: 22px; transform: translateY(-50%); }
}
/* ---- selection controls ----
   The native tick renders at 13px in whatever the OS thinks a checkbox looks
   like — on this surface a stark white square with a black mark, and a column of
   them reads as damage rather than as a selection. It is still a real <input>
   (select-all, indeterminate, keyboard and the row hover all depend on it); only
   its skin is ours.

   The :not() list is load-bearing and it is a LIST: four wrappers in this app
   hide their input under artwork of their own — .switch and .map-chip stretch it
   to full size for the hit area, .segmented and .check collapse it to 0×0 behind
   a drawn box. This rule outranks all four (it carries three type selectors and
   three classes), so anything left off the list gets a second, visible tick
   sitting on top of the artwork — which is exactly what /login's "Remember me"
   grew. Any new custom-artwork input belongs here too. */
input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input),
input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input) {
  appearance: none; -webkit-appearance: none;
  width: 18px; height: 18px; flex: none; margin: 0; padding: 0;
  /* `middle`, not a px nudge off the baseline: an inline-grid box takes its
     baseline from its content, so the moment the ::after tick appeared the
     header's line box grew 3.4px and every row below it stepped down. A fixed
     alignment keeps the box's contribution constant, checked or not. */
  display: inline-grid; place-content: center; vertical-align: middle;
  background: var(--surface-3); border: 1.5px solid var(--line);
  border-radius: 5px; cursor: pointer;
  transition: background-color var(--fast) var(--ease-out),
              border-color var(--fast) var(--ease-out);
}
input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input) { border-radius: 50%; }
input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):hover,
input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input):hover {
  background: var(--surface-4); border-color: var(--ink-4);
}
input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):checked,
input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):indeterminate,
input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input):checked {
  background: var(--accent); border-color: var(--accent);
}
/* the mark: two borders rotated into a tick — crisper at 18px than any glyph or
   background image, and it takes its colour from the seat it sits on */
input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):checked::after {
  content: ""; width: 4px; height: 8px; margin-top: -2px;
  border: solid var(--accent-ink); border-width: 0 2px 2px 0;
  transform: rotate(43deg);
}
/* "some of these, not all" — a state data-tables.js has always set and nothing
   was drawing, so select-all looked simply unchecked with three rows picked */
input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):indeterminate::after {
  content: ""; width: 9px; height: 2px; border-radius: 1px; background: var(--accent-ink);
}
input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input):checked::after {
  content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-ink);
}
input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):focus-visible,
input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input):focus-visible {
  outline: none; box-shadow: 0 0 0 3px var(--accent-glow);
}
input[type=checkbox]:disabled, input[type=radio]:disabled { cursor: not-allowed; opacity: .5; }
th.col-check, td.col-check { padding-left: 20px; padding-right: 10px; }
/* A selected row says so as a row. With the tick carrying the whole signal you
   had to read down a column of 18px squares to see what you had picked. */
tbody tr:has(td.col-check input:checked) { background: var(--accent-soft); }
tbody tr:has(td.col-check input:checked):hover { background: var(--hover-2); }
@media (max-width: 760px) {
  /* a mobile-only .filter-form rule was capping toolbar controls at 40px, under
     the 44px this same stylesheet asks for on a phone. Two classes on the audit
     selectors, because .list-toolbar.audit-toolbar select outranks one class. */
  .filter-form > *, .list-toolbar select, .list-toolbar input,
  .list-toolbar .compact,
  .list-toolbar.audit-toolbar select,
  .list-toolbar.audit-toolbar input[type=text] { min-height: 44px; }
  /* In card mode the row is a card with its own 20px padding, so the select
     cell no longer needs the column inset — and it must stop being 48px wide:
     48 minus my own 20+10 padding leaves an 18px content box, and `max-width:
     100%` then clamped the 24px touch target back down to 18. */
  th.col-check, td.col-check { width: auto; padding-left: 0; padding-right: 12px; }
  input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input),
  input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input) { width: 24px; height: 24px; border-radius: 7px; }
  input[type=radio]:not(.switch input, .map-chip input, .segmented input, .check input) { border-radius: 50%; }
  input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):checked::after {
    width: 6px; height: 11px; border-width: 0 2.5px 2.5px 0;
  }
  input[type=checkbox]:not(.switch input, .map-chip input, .segmented input, .check input):indeterminate::after { width: 12px; }
}

/* ---- the bulk bar floats; it does not shove ----
   Ticking one row used to insert a 76px bar above the table, so the row you had
   just aimed at slid out from under the pointer and the next click landed on a
   different one — measured at +76y on /bots and /admin/invites. The bar is out
   of flow now: it fades up over the bottom of the content column, follows you
   down a long list, and moves nothing. */
.bulk-bar {
  position: fixed; z-index: 40;
  left: var(--sidebar-w); right: 0; bottom: 18px;
  width: min(1372px, calc(100% - var(--sidebar-w) - 68px)); margin: 0 auto;
  background: var(--surface-3); border: 1px solid var(--accent-line);
  box-shadow: 0 18px 44px -14px rgba(0,0,0,.8), 0 0 0 1px rgba(0,0,0,.35);
  border-radius: var(--r); padding: 12px 16px;
  animation: bulkIn 160ms var(--ease-out);
}
@keyframes bulkIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@media (max-width: 980px) {
  /* the sidebar is off-canvas below this width, so the bar owns the full column */
  .bulk-bar { left: 0; width: calc(100% - 28px); bottom: 14px; }
}
/* room to scroll the last row clear of the floating bar. This grows the page's
   trailing padding, so the scrollable area changes and no element moves. */
body:has(.bulk-bar:not([hidden])) .content { padding-bottom: 116px; }

/* ── Weekly drop digest scope (Notifications) ─────────────────────────────── */
.digest-scope { margin-top: 14px; }
.digest-scope .help { display: block; margin-top: 4px; }

.digest-machines {
  display: grid; margin-top: 12px;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px;
}
.digest-machine {
  display: flex; align-items: center; gap: 8px; min-width: 0; cursor: pointer;
  padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface-3); font-size: var(--fs-3); color: var(--ink-2);
  transition: border-color .12s, background .12s;
}
.digest-machine:hover { border-color: var(--line-2); }
.digest-machine:has(input:checked) { border-color: var(--accent-line); background: var(--surface-4); color: var(--ink); }
.digest-machine input { accent-color: var(--accent); margin: 0; flex: none; }
.digest-machine .dm-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Import: the SDA card is a real drop target — its copy always said "drop them
   here". Highlight is a tint of the accent, so it reads as "let go here" without
   moving anything (a size/border-width change would jump the card under the cursor). */
.drop-zone { transition: border-color var(--fast) var(--ease-out), background-color var(--fast) var(--ease-out); }
.drop-zone.is-dragover { border-color: var(--accent-line); background-color: var(--accent-soft); }
.drop-zone.is-dragover .file-name { color: var(--ink); }
