﻿/* ============================================================
   SKYGATE — 4 themes via [data-theme]
   - linear: dark, indigo accent (Inter)
   - vercel: light, monochrome (Inter, tight tracking)
   - sentry: dark purple, lime accent (Rubik)
   ============================================================ */

/* 2026-08-20: B158 — self-hosted Google Fonts.
   Pre-B158 every page loaded Manrope/Inter/Geist/Sora +
   JetBrains Mono from fonts.googleapis.com, which timed
   out on networks without external access (operator
   report 2026-08-20: 'GET .../css2?family=Manrope...
   net::ERR_CONNECTION_TIMED_OUT'). We bundle the woff2
   files locally + Cache-Control immutable max-age=1y
   (handled by static.go's hasContentHash() check, B152).
   The @font-face srcs use /static/webfonts/<file>.woff2; the
   static handler serves them with the right Cache-Control
   + immutable headers because the filename has a content
   hash style (Manrope, Inter, etc) — the static handler
   treats /static/webfonts/* as versioned assets.

   The 4 families (Manrope + Inter + Geist + Sora) cover
   the 4 user-pickable body fonts; JetBrains Mono + Geist
   Mono cover the 2 user-pickable monospace fonts. Each
   has weights 400/500/600/700 (400/500 for monospace) in
   the latin subset; cyrillic + other subsets are picked
   up by the system fallback fonts (the layout's RU
   strings fall back to system-ui which is the OS's
   preferred cyrillic font — visually OK, no extra
   download).

   font-display: swap means the browser uses the
   fallback font for ~100ms while the woff2 loads,
   then swaps to the webfont. No "invisible text"
   flash. Critical for slow links (which is the
   whole reason we're self-hosting). */
@font-face{font-family:'Manrope';font-style:normal;font-weight:400;font-display:swap;src:url(/static/webfonts/manrope-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'Manrope';font-style:normal;font-weight:500;font-display:swap;src:url(/static/webfonts/manrope-latin-500-normal.woff2) format('woff2')}
@font-face{font-family:'Manrope';font-style:normal;font-weight:600;font-display:swap;src:url(/static/webfonts/manrope-latin-600-normal.woff2) format('woff2')}
@font-face{font-family:'Manrope';font-style:normal;font-weight:700;font-display:swap;src:url(/static/webfonts/manrope-latin-700-normal.woff2) format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url(/static/webfonts/inter-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;src:url(/static/webfonts/inter-latin-500-normal.woff2) format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:600;font-display:swap;src:url(/static/webfonts/inter-latin-600-normal.woff2) format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:700;font-display:swap;src:url(/static/webfonts/inter-latin-700-normal.woff2) format('woff2')}
@font-face{font-family:'Geist';font-style:normal;font-weight:400;font-display:swap;src:url(/static/webfonts/geist-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'Geist';font-style:normal;font-weight:500;font-display:swap;src:url(/static/webfonts/geist-latin-500-normal.woff2) format('woff2')}
@font-face{font-family:'Geist';font-style:normal;font-weight:600;font-display:swap;src:url(/static/webfonts/geist-latin-600-normal.woff2) format('woff2')}
@font-face{font-family:'Geist';font-style:normal;font-weight:700;font-display:swap;src:url(/static/webfonts/geist-latin-700-normal.woff2) format('woff2')}
@font-face{font-family:'Sora';font-style:normal;font-weight:400;font-display:swap;src:url(/static/webfonts/sora-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'Sora';font-style:normal;font-weight:500;font-display:swap;src:url(/static/webfonts/sora-latin-500-normal.woff2) format('woff2')}
@font-face{font-family:'Sora';font-style:normal;font-weight:600;font-display:swap;src:url(/static/webfonts/sora-latin-600-normal.woff2) format('woff2')}
@font-face{font-family:'Sora';font-style:normal;font-weight:700;font-display:swap;src:url(/static/webfonts/sora-latin-700-normal.woff2) format('woff2')}
@font-face{font-family:'Geist Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/static/webfonts/geist-mono-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'Geist Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/static/webfonts/geist-mono-latin-500-normal.woff2) format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/static/webfonts/jetbrains-mono-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/static/webfonts/jetbrains-mono-latin-500-normal.woff2) format('woff2')}

/* Defaults (linear) — v1.3.20.3 (B133) dramatic contrast overhaul.
   B131 bumped contrast moderately (5% bg→card) but the
   operator reported the page still had "no proper style
   separation". Diagnosis: it wasn't the bg/border deltas
   that were the problem — it was (1) table rows had no
   backgrounds, only a thin border-bottom in --border
   (#383b42 on #1a1c21 is barely visible), (2) cards had
   no shadow so they didn't feel "elevated" off the page,
   (3) the 4 action buttons in the rightmost column were
   all the same dark icon, (4) alert pills (info-bg /
   warning-bg / etc.) at 0.15 opacity rendered as a
   near-invisible tint on the dark bg.

   B133 fixes all 4 by (a) bumping the Linear palette to
   high-contrast values (bg→card delta ~7%, border at
   18% lift from card, alert opacities 0.15 → 0.22),
   (b) adding table zebra striping via
   `tbody tr:nth-child(even)` in a separate (theme-aware)
   rule below, (c) adding a card box-shadow, (d) making
   the action buttons color-coded by intent (tag=primary
   blue, untag=warning yellow, TS IP=info cyan, delete
   =danger red) so the column is readable at a glance.
   Other 4 themes (Vercel, Sentry, NVIDIA, Mint) unchanged. */
:root{
  --bg:#0c0d10;--bg-card:#23262e;--bg-elev:#2d3038;--bg-hover:#363941;
  --border:#555a64;--border-strong:#6b707b;
  --text:#f5f5f5;--text-muted:#a8acb5;--text-subtle:#6f7480;
  --accent:#7c7aff;--accent-fg:#0a0a0a;--accent-hover:#9b9aff;--accent-soft:rgba(124,122,255,0.22);
  --success:#4ade80;--success-bg:rgba(74,222,128,0.22);
  --warning:#fbbf24;--warning-bg:rgba(251,191,36,0.22);
  --danger:#f87171;--danger-bg:rgba(248,113,113,0.22);
  --info:#60a5fa;--info-bg:rgba(96,165,250,0.22);
  --header-bg:#181a20;--header-fg:#ededed;
  --shadow:0 1px 0 rgba(255,255,255,0.05) inset, 0 1px 3px rgba(0,0,0,0.4), 0 4px 12px rgba(0,0,0,0.25);
  --shadow-lg:0 12px 32px rgba(0,0,0,0.5);
  --radius:6px;--radius-lg:10px;
  --font:'Manrope',system-ui,-apple-system,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,monospace;
  --header-h:56px;
}

/* Vercel (light) */
[data-theme="vercel"]{
  --bg:#ffffff;--bg-card:#fafafa;--bg-elev:#f4f4f5;--bg-hover:#e4e4e7;
  --border:#e4e4e7;--border-strong:#d4d4d8;
  --text:#0a0a0a;--text-muted:#52525b;--text-subtle:#a1a1aa;
  --accent:#0a0a0a;--accent-fg:#ffffff;--accent-hover:#27272a;--accent-soft:rgba(10,10,10,0.06);
  --success:#16a34a;--success-bg:rgba(22,163,74,0.08);
  --warning:#d97706;--warning-bg:rgba(217,119,6,0.08);
  --danger:#dc2626;--danger-bg:rgba(220,38,38,0.08);
  --info:#2563eb;--info-bg:rgba(37,99,235,0.08);
  --header-bg:#ffffff;--header-fg:#0a0a0a;
  --shadow:0 0 0 1px rgba(0,0,0,0.06);
  --shadow-lg:0 8px 24px rgba(0,0,0,0.08);
  --radius:6px;--radius-lg:8px;
}

/* Sentry (dark purple) */
[data-theme="sentry"]{
  --bg:#1f1633;--bg-card:#2a1f44;--bg-elev:#362d59;--bg-hover:#3d3361;
  --border:#3a2f5e;--border-strong:#4d3f7a;
  --text:#ffffff;--text-muted:#b8b0c8;--text-subtle:#8a8198;
  --accent:#c2ef4e;--accent-fg:#1f1633;--accent-hover:#d4f77a;--accent-soft:rgba(194,239,78,0.15);
  --success:#4ade80;--success-bg:rgba(74,222,128,0.12);
  --warning:#fbbf24;--warning-bg:rgba(251,191,36,0.12);
  --danger:#fa7faa;--danger-bg:rgba(250,127,170,0.12);
  --info:#79628c;--info-bg:rgba(121,98,140,0.18);
  --header-bg:#150f23;--header-fg:#ffffff;
  --shadow:0 1px 0 rgba(255,255,255,0.04) inset, 0 4px 12px rgba(0,0,0,0.3);
  --shadow-lg:0 12px 32px rgba(22,15,36,0.6);
  --radius:8px;--radius-lg:12px;
}

/* NVIDIA (dark green) */
[data-theme="nvidia"]{
  --bg:#0a0a0a;--bg-card:#141414;--bg-elev:#1a1a1a;--bg-hover:#222;
  --border:#2a2a2a;--border-strong:#3d3d3d;
  --text:#e0e0e0;--text-muted:#999;--text-subtle:#666;
  --accent:#76b900;--accent-fg:#0a0a0a;--accent-hover:#8cd600;--accent-soft:rgba(118,185,0,0.12);
  --success:#76b900;--success-bg:rgba(118,185,0,0.12);
  --warning:#f5a623;--warning-bg:rgba(245,166,35,0.1);
  --danger:#e74856;--danger-bg:rgba(231,72,86,0.1);
  --info:#4cc2c4;--info-bg:rgba(76,194,196,0.1);
  --header-bg:#0a0a0a;--header-fg:#e0e0e0;
  --shadow:0 1px 0 rgba(255,255,255,0.03) inset,0 2px 8px rgba(0,0,0,0.4);
  --shadow-lg:0 8px 24px rgba(0,0,0,0.5);
  --radius:4px;--radius-lg:8px;
}

/* 2026-08-17: v1.3.19.2 follow-up (B121) — Mint theme.
   Light theme with silver background + mint-green accent.
   Designed for long admin sessions: high-contrast text,
   soft borders, mint accent that's easy on the eyes
   (vs the high-saturation indigo of Linear).

   Palette: very-light silver with a subtle mint tint
   (--bg #f5f7f6, --bg-card #ffffff), mint-tinted borders
   (--border #d4dad6 has a green-gray hue), and emerald
   accent (#10b981). Status colors: green success (mint),
   amber warning, rose danger, cyan info.

   The --bg-card #ffffff (pure white) instead of the
   vercel-theme #fafafa (off-white) gives cards more
   "lift" from the silver --bg. The 8px radius (vs
   vercel's 6px) rounds corners slightly more for a
   friendlier, modern look. */
[data-theme="mint"]{
  --bg:#f5f7f6;--bg-card:#ffffff;--bg-elev:#eef1ef;--bg-hover:#e0e6e3;
  --border:#d4dad6;--border-strong:#a8b3ad;
  --text:#1a2520;--text-muted:#5e6a64;--text-subtle:#8a938f;
  --accent:#10b981;--accent-fg:#ffffff;--accent-hover:#059669;--accent-soft:rgba(16,185,129,0.1);
  --success:#10b981;--success-bg:rgba(16,185,129,0.1);
  --warning:#d97706;--warning-bg:rgba(217,119,6,0.08);
  --danger:#dc2626;--danger-bg:rgba(220,38,38,0.08);
  --info:#0891b2;--info-bg:rgba(8,145,178,0.08);
  --header-bg:#f5f7f6;--header-fg:#1a2520;
  --shadow:0 1px 2px rgba(0,0,0,0.04),0 0 0 1px rgba(0,0,0,0.04);
  --shadow-lg:0 8px 24px rgba(0,0,0,0.08);
  --radius:8px;--radius-lg:12px;
}

*{box-sizing:border-box}
/* 2026-08-17: v1.3.19.2 follow-up (B121) — custom scrollbar.
   The browser default scrollbar in dark themes (e.g. Linear
   #0a0a0a bg with bright white scrollbar) is visually
   jarring — a 15-17px wide block of light gray on a
   dark background breaks the visual flow and overpowers
   the content. The fix: thin (8px), theme-aware scrollbar
   that blends with the background.

   - Firefox: `scrollbar-width: thin` + `scrollbar-color`
     (track + thumb). Track is transparent so the page bg
     shows through.
   - WebKit (Chrome/Safari/Edge): `-webkit-scrollbar` rules.
     The thumb is inset 2px from the track edges via `border`
     so the visible thumb width is 4px (track 8px - 2*2
     border). On hover, the thumb uses --border-strong
     for a subtle highlight.

   The scrollbar uses --border / --border-strong (already
   defined per-theme) so it automatically picks up the
   right shades for Linear, Vercel, Sentry, NVIDIA, and
   Mint. */
*{scrollbar-width:thin;scrollbar-color:var(--border-strong) transparent}
*::-webkit-scrollbar{width:8px;height:8px}
*::-webkit-scrollbar-track{background:transparent}
*::-webkit-scrollbar-thumb{background:var(--border);border-radius:8px;border:2px solid var(--bg)}
*::-webkit-scrollbar-thumb:hover{background:var(--border-strong)}
*::-webkit-scrollbar-thumb:active{background:var(--text-muted)}
*::-webkit-scrollbar-corner{background:transparent}
html,body{margin:0;padding:0}
body{font-family:var(--font);font-size:15px;line-height:1.6;color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overflow-x:hidden}
[data-theme="vercel"] body{font-feature-settings:normal;letter-spacing:-0.005em}
a{color:var(--text);text-decoration:underline;text-decoration-color:var(--border-strong);text-underline-offset:2px;text-decoration-thickness:1px}
a:hover{text-decoration-color:var(--accent)}
code,pre,kbd,samp{font-family:var(--mono);font-size:0.92em}
pre{background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius);padding:14px;overflow-x:auto;font-size:14px;line-height:1.6;color:var(--text)}
[data-theme="linear"] pre,[data-theme="sentry"] pre{border-radius:8px}
kbd{background:var(--bg-elev);border:1px solid var(--border-strong);border-radius:3px;padding:1px 5px;font-size:13px}

/* HEADER */
header{margin-left:220px;background:var(--header-bg);color:var(--header-fg);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:50;backdrop-filter:saturate(180%) blur(8px)}
[data-theme="sentry"] header{box-shadow:0 4px 16px rgba(0,0,0,0.4)}
header .shell{display:flex;align-items:center;gap:16px;padding:0 24px;height:var(--header-h)}
header h1{font-size:16px;font-weight:600;margin:0;letter-spacing:-0.015em;display:flex;align-items:center;gap:8px;white-space:nowrap}
header h1 .logo{width:18px;height:18px;color:var(--accent)}
header h1 .ver{opacity:.5;font-size:10px;font-weight:500;margin-left:2px;font-family:var(--mono);padding:1px 5px;border:1px solid var(--border);border-radius:3px}

/* sidebar */
.sidebar{position:fixed;left:0;top:0;width:220px;height:100vh;background:var(--bg);border-right:1px solid var(--border);z-index:100;display:flex;flex-direction:column;overflow:hidden;transition:width .25s}
.sidebar.collapsed{width:52px}
.sidebar .shead{display:flex;align-items:center;gap:10px;padding:12px 14px;height:var(--header-h);border-bottom:1px solid var(--border);flex-shrink:0}
.sidebar .shead h1{font-size:16px;font-weight:600;margin:0;white-space:nowrap;overflow:hidden;letter-spacing:-0.015em}
.sidebar .toggle{background:transparent;border:none;color:var(--fg);cursor:pointer;padding:4px;border-radius:var(--radius);margin-left:auto;font-size:14px;flex-shrink:0;opacity:.6}
.sidebar .toggle:hover{opacity:1;background:var(--bg-hover)}
.sidebar nav{display:flex;flex-direction:column;gap:2px;padding:8px;flex:1;overflow-y:auto}
.sidebar nav a{color:var(--fg);text-decoration:none;padding:8px 10px;border-radius:var(--radius);font-size:14px;font-weight:500;display:flex;align-items:center;gap:10px;white-space:nowrap;transition:all .15s;opacity:.75;overflow:hidden}
.sidebar nav a:hover{opacity:1;background:var(--bg-hover)}
/* 2026-08-12: v1.1.5 — active page highlight. The previous
   "subtle background tint" (background:var(--accent-soft);
   color:var(--accent)) was too easy to miss — operator
   reported "не понятно в какой группе и на какой странице в
   древе сейчас находишься" because the highlight was lost
   when the section collapsed. New: bold + 3px left border
   in the accent color + slightly stronger background.
   The left border is the most reliable signal even when
   the section is collapsed (e.g. on mobile where the
   user manually collapses a section and the auto-open
   re-opens it on next navigation). */
.sidebar nav a.active{
  opacity:1;
  background:var(--accent-soft);
  color:var(--accent);
  font-weight:600;
  box-shadow:inset 3px 0 0 var(--accent);
  padding-left:13px;
}
.sidebar nav a i{width:16px;text-align:center;font-size:13px;flex-shrink:0}
.sidebar.collapsed nav a span{display:none}
.sidebar.collapsed .shead h1{opacity:0;width:0;overflow:hidden}
.sidebar .nav-sep{margin:4px 0;border:none;border-top:1px solid var(--border);opacity:.2}

/* 2026-08-12: v1.1.0 (TD-1) — sidebar sections. The 22 admin
   nav items are grouped into 6 collapsible <details> blocks
   (Devices & Nodes, Access Control, System Health & Logs,
   Integrations, Data, Settings & Users). The <summary> is
   the section header; clicking it toggles the child <a> list.
   On desktop the <details> stays closed by default unless the
   current page belongs to that section (auto-open via
   {{if .InSectionX}}open{{end}} in layout.html). On mobile
   the @media rule below overrides this — sections always
   render open so the operator doesn't have to tap 6 times. */
.sidebar-section{margin:2px 0}
.sidebar-section>summary{
  list-style:none;cursor:pointer;
  padding:8px 10px;border-radius:var(--radius);
  font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;
  color:var(--text-muted);
  display:flex;align-items:center;gap:10px;
  transition:all .15s;user-select:none;
}
.sidebar-section>summary::-webkit-details-marker{display:none}
.sidebar-section>summary::before{
  content:"\f0da";font-family:"Font Awesome 6 Free";font-weight:900;
  font-size:10px;width:10px;flex-shrink:0;
  transition:transform .15s;display:inline-block;text-align:center;
}
.sidebar-section[open]>summary::before{transform:rotate(90deg)}
.sidebar-section>summary:hover{background:var(--bg-hover);color:var(--text)}
.sidebar-section[open]>summary{color:var(--text)}
/* 2026-08-12: v1.1.5 — when a section is open AND contains
   the active page, tint the section summary with a soft
   accent background. The server-rendered `.current-section`
   class (set in layout.html when InSectionX && Page is
   set) is used instead of CSS :has() for max browser
   compatibility. The class makes "which group am I in"
   visible at a glance even when looking at just the
   section headers (without scrolling into the link list). */
.sidebar-section[open]>summary.current-section{
  background:var(--accent-soft);
  color:var(--accent);
  border-left:3px solid var(--accent);
  padding-left:7px;
}
.sidebar-section>summary i{width:16px;text-align:center;font-size:12px;flex-shrink:0;color:var(--accent)}

/* 2026-08-12: v1.1.5 — "вы здесь" breadcrumb above admin
   page content. Shows the operator the full path
   (Админ › Group › Page) so they never get lost even
   when the sidebar section is collapsed. */
.admin-breadcrumb{
  display:flex;align-items:center;gap:6px;
  /* 2026-08-13: v1.3.9 round 7 (B109) — desktop padding-left
     bumped from 24px to 40px. The breadcrumb lives on its own
     line (with its own bg-card background + bottom border),
     separate from the .shell content, so the 24px "standard"
     padding that header .shell uses looked visually too tight
     against the 220px sidebar. The 40px gives the breadcrumb
     the same visual "breathing room" as the page title (h2 in
     .title-row) has from the .shell's left edge. The mobile
     @media rule below overrides padding-left:60px for the
     hamburger-clearance case (B107) — desktop and mobile
     padding values stay independent. */
  padding:10px 24px 10px 40px;
  font-size:13px;
  color:var(--text-muted);
  background:var(--bg-card);
  border-bottom:1px solid var(--border);
}
.admin-breadcrumb .crumb-home{color:var(--accent);font-size:12px}
.admin-breadcrumb .crumb-root{color:var(--text);font-weight:600}
.admin-breadcrumb .crumb-section{color:var(--text-muted)}
.admin-breadcrumb .crumb-page{color:var(--accent);font-weight:500}
.admin-breadcrumb .crumb-sep{font-size:10px;opacity:.4}
.sidebar-section>summary>span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sidebar-section>a{padding-left:30px !important;font-size:13px !important}
.sidebar.collapsed .sidebar-section>summary>span,
.sidebar.collapsed .sidebar-section>a>span{display:none}
.sidebar.collapsed .sidebar-section>summary::before{transform:rotate(0deg)}
.sidebar.collapsed .sidebar-section[open]>a{display:none}
.sidebar.collapsed .sidebar-section{opacity:.75}
.sidebar.collapsed .sidebar-section:hover{opacity:1}
/* 2026-08-13: section summary width on collapsed sidebar.
   The summary has 8px 10px padding + 10px gap + 10px caret
   (::before) + 16px icon (i) = 56px content. With the sidebar
   at 52px when collapsed, this overflows by 4px and triggers
   a horizontal scroll bar inside the sidebar. The operator-
   reported symptom: "когда меню скрыто иконки групп не
   переходят в скрытый режим и появляется снизу горизонтальный
   скролл". The pre-fix CSS only hid the <span> text inside the
   summary but left the caret + icon visible. The fix: drop
   padding/gap on collapsed, hide the caret (the section
   links are also hidden on collapsed via `[open]>a{display:none}`
   so the caret has nothing to open/close), and center the
   icon. Net result: the collapsed summary becomes a single
   centered 16px icon that fits in 52px. Works for BOTH desktop
   (where the user manually clicks .toggle to collapse the
   sidebar to 52px) AND mobile (where the B106 force-clear
   rule prevents a stuck .collapsed state, but if one ever
   does slip through, the icon still fits). */
.sidebar.collapsed .sidebar-section>summary{padding:0;gap:0;justify-content:center}
.sidebar.collapsed .sidebar-section>summary::before{display:none}

/* 2026-08-12: v1.1.0 (TD-3) — hamburger toggle + mobile drawer.
   On desktop (.sidebar-toggle-input, .sidebar-toggle) are
   display:none — the sidebar is always visible. On mobile
   (<768px) the hamburger appears in the top-left, the sidebar
   is hidden by default (translateX(-100%)), and tapping the
   hamburger slides it in. The checkbox hack — input[id=sidebar-toggle]
   + label[for=sidebar-toggle] — drives the state without any
   JavaScript. */
.sidebar-toggle-input{display:none}
.sidebar-toggle{
  display:none;position:fixed;top:12px;left:12px;z-index:200;
  width:40px;height:40px;align-items:center;justify-content:center;
  background:var(--bg-card);color:var(--text);
  border:1px solid var(--border);border-radius:var(--radius);
  cursor:pointer;box-shadow:var(--shadow-lg);
}
.sidebar-toggle i{font-size:16px}
.sidebar-user{padding:8px;border-top:1px solid var(--border);flex-shrink:0}
.sidebar-user .user-info{display:flex;align-items:center;gap:8px;padding:8px 10px;font-size:13px}
.sidebar-user .user-icon{width:16px;text-align:center;color:var(--accent)}
.sidebar-user .user-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sidebar-user .sidebar-actions{display:flex;align-items:center;gap:4px;padding:4px 8px}
.sidebar-user .sidebar-actions form{margin:0}
.sidebar-user .sidebar-actions button{background:transparent;border:none;color:var(--text);cursor:pointer;padding:6px 8px;border-radius:var(--radius);opacity:.6;font-size:13px}
.sidebar-user .sidebar-actions button:hover{opacity:1;background:var(--bg-hover)}
.theme-picker-sb{position:relative}
.theme-picker-sb summary{list-style:none;cursor:pointer;padding:6px 8px;border-radius:var(--radius);opacity:.6;font-size:13px}
.theme-picker-sb summary:hover{opacity:1;background:var(--bg-hover)}
.theme-picker-sb[open] summary{background:var(--bg-hover)}
.theme-picker-sb .menu{position:absolute;bottom:100%;left:0;min-width:140px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-lg);padding:4px;z-index:200;margin-bottom:4px}
.theme-picker-sb .menu a{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;border-radius:4px;text-decoration:none;color:var(--text)}
.theme-picker-sb .menu a:hover{background:var(--bg-hover)}
.theme-picker-sb .menu a.active{background:var(--accent-soft);color:var(--accent)}
.sidebar.collapsed .sidebar-user .user-name{display:none}
.sidebar.collapsed .sidebar-user .sidebar-actions{justify-content:center}
main .shell{margin-left:220px;transition:margin-left .25s}
.sidebar.collapsed ~ main .shell{margin-left:52px}
.sidebar.collapsed ~ header{margin-left:52px}
.sidebar.collapsed ~ header{margin-left:52px}
/* 2026-08-17: v1.3.19.2 follow-up (B120) — admin-breadcrumb offset.
   Pre-fix, the .admin-breadcrumb element (rendered in
   layout.html as a SIBLING of .shell, both inside <main>) had
   no margin-left, so its left edge sat at 0px and the
   position:fixed sidebar (left:0, width:220px) covered its
   leftmost 220px. The "Админ › Devices & Nodes › Devices"
   text was hidden under the sidebar — only the rightmost
   text fragments were visible.

   Fix: mirror the .shell margin-left pattern for .admin-
   breadcrumb. The width:calc(...) keeps the breadcrumb's
   background-card spanning the visible area instead of
   just the text width. The ~ selector mirrors the .shell
   rule so collapsing the sidebar (52px) also shifts the
   breadcrumb correctly. */
main .admin-breadcrumb{margin-left:220px;width:calc(100% - 220px);transition:margin-left .25s, width .25s}
.sidebar.collapsed ~ main .admin-breadcrumb{margin-left:52px;width:calc(100% - 52px)}

header .user-area button i{font-size:11px}

/* THEME PICKER */
.theme-picker{position:relative}
.theme-picker summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border:1px solid var(--border);border-radius:var(--radius);background:transparent;color:inherit;font:inherit;font-size:12px;transition:all .15s}
.theme-picker summary::-webkit-details-marker{display:none}
.theme-picker summary:hover{background:var(--bg-hover)}
.theme-picker summary i{font-size:12px}
.theme-picker[open] summary{background:var(--bg-hover);border-color:var(--accent)}
.theme-picker .menu{position:absolute;right:0;top:calc(100% + 4px);min-width:160px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-lg);padding:4px;z-index:100}
.theme-picker .menu a{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;border-radius:4px;text-decoration:none;color:var(--text);opacity:1}
.theme-picker .menu a:hover{background:var(--bg-hover)}
.theme-picker .menu a.active{background:var(--accent-soft);color:var(--accent);font-weight:500}
.theme-picker .menu a i{font-size:12px;width:14px;text-align:center}
.theme-picker .menu a small{color:var(--text-subtle);font-size:11px;margin-left:auto}

main{padding:32px 0 64px;min-height:calc(100vh - var(--header-h))}
.shell{max-width:1180px;margin:0 auto;padding:0 24px;min-width:0}

/* PAGE TITLE */
.title-row{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:24px;gap:16px;flex-wrap:wrap}
.title-row h2{font-size:24px;font-weight:600;margin:0 0 4px;letter-spacing:-0.02em;display:flex;align-items:center;gap:10px}
.title-row h2 i{color:var(--accent);font-size:20px}
.title-row .sub{font-size:14px;color:var(--text-muted)}

/* CARDS */
/* 2026-08-18 (B133): cards now use --border-strong (was
   --border) for the outline. The pre-B133 --border on
   Linear was #383b42 on --bg-card #1a1c21 — a 1px line
   that was nearly invisible. Bumping to --border-strong
   (#6b707b) gives a clear "card" outline. The light
   themes (Vercel/Mint) override back to --border via
   the [data-theme] block because their --border-strong
   is too dark for a light card. */
.card{background:var(--bg-card);border:1px solid var(--border-strong);border-radius:var(--radius-lg);padding:20px 24px;margin-bottom:16px;box-shadow:var(--shadow)}
[data-theme="mint"] .card,[data-theme="vercel"] .card{border-color:var(--border)}
[data-theme="sentry"] .card{box-shadow:var(--shadow)}
.card h3{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin:0 0 14px 0;display:flex;align-items:center;gap:6px}
.card h3 i{color:var(--accent);font-size:12px}

/* TABLE */
.table-wrap{overflow-x:auto;margin:0 -4px}
table{width:100%;border-collapse:collapse;font-size:14px}
th,td{padding:11px 12px;text-align:left;border-bottom:1px solid var(--border);vertical-align:middle}
th{background:transparent;font-weight:600;color:var(--text-muted);font-size:12px;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid var(--border-strong);white-space:nowrap}
/* 2026-08-18 (B133): stronger row border + zebra striping.
   Pre-B133 the td border-bottom was var(--border) which on
   Linear was #383b42 on #1a1c21 — a 1px line that was
   nearly invisible at typical monitor brightness. B133
   bumps td border-bottom to var(--border-strong)
   (#6b707b on Linear) AND adds zebra striping so each
   row alternates between card-bg and elev-bg. The
   combination gives the table a clear "tabular" feel
   that was missing in B131/B132. The light themes
   (Vercel/Mint) get a subtler stripe (--bg-elev is
   already close to --bg-card there) so this doesn't
   make them look striped. */
td{border-bottom-color:var(--border-strong)}
tbody tr:nth-child(even){background:var(--bg-elev)}
[data-theme="mint"] tbody tr:nth-child(even),
[data-theme="vercel"] tbody tr:nth-child(even){background:rgba(0,0,0,0.02)}
tbody tr:last-child td{border-bottom:none}
tbody tr{transition:background .1s}
tbody tr:hover{background:var(--bg-hover)}
td.mono,code,pre{font-family:var(--mono);font-size:13px}

/* 2026-08-04 (v0.33.1.7): /admin/update phase border.
   Used to be inline style="border-left:4px solid {{$borderColor}}"
   with $borderColor = "var(--success)" etc. — Go's html/template
   refuses to interpolate that into a CSS context and renders
   the literal "ZgotmplZ" placeholder. Static classes per phase
   sidestep the context-aware CSS sanitizer entirely. */
.card-update-info         { border-left: 4px solid var(--info) }
.card-update-done         { border-left: 4px solid var(--success) }
.card-update-failed       { border-left: 4px solid var(--danger) }
.card-update-rolled-back  { border-left: 4px solid var(--warning) }

/* 2026-08-04 (v0.33.1.7): /admin/update log-line colours.
   Replaces inline style="color:{{$lineColor}}". */
.log-line-info  { color: var(--text-muted) }
.log-line-warn  { color: var(--warning) }
.log-line-error { color: var(--danger) }

/* FORMS */
fieldset{border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin:0 0 12px}
legend{padding:0 8px;font-size:13px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em}
label{display:block;font-size:14px;font-weight:500;margin-bottom:6px;color:var(--text)}
label .lbl-sub{font-weight:400;color:var(--text-muted);font-size:13px;margin-left:4px}
.field{margin-bottom:14px}
.field .hint{font-size:13px;color:var(--text-muted);margin-top:4px;display:flex;align-items:flex-start;gap:4px}
.field .hint i{font-size:12px;margin-top:2px}
/* 2026-08-24: v1.5.1 (B165) — generic 2-column form
   grid. Used by /my/devices (the "Add new device"
   form) and any other form that wants a stable
   desktop layout (1fr 1fr) that collapses to a
   single column on mobile (<768px). Replaces the
   pre-B165 inline-flex layouts that caused the
   fields to "jump" when the screen width changed. */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px 18px;
  align-items: start;
}
.form-grid .field {
  margin-bottom: 0;
}
.form-grid .field[style*="grid-column:1 / span 2"],
.form-grid .field[style*="grid-column: 1 / span 2"] {
  /* full-width fields (like the OS row) override the
     2-col layout */
  grid-column: 1 / -1;
}
/* Inline form group (value + unit dropdown on the
   same row, e.g. the Custom TTL row). The label
   owns the pair; the input is fixed-width and the
   unit dropdown fills the remaining space. */
.form-group-inline {
  display: flex;
  gap: 8px;
  align-items: center;
}
.form-group-inline > input[type="number"] {
  width: 130px;
  flex-shrink: 0;
}
.form-group-inline > select {
  flex: 1 1 auto;
  min-width: 100px;
}
/* Stronger hint (used for the form's actionable
   hints). The pre-B165 hints were gray-on-gray
   (--text-muted); the new style uses a slightly
   darker foreground so the operator can read the
   hint without squinting. */
.form-hint-strong {
  font-size: 12.5px;
  color: var(--text);
  margin-top: 6px;
  line-height: 1.5;
  opacity: 0.85;
}
@media (max-width: 768px) {
  .form-grid {
    grid-template-columns: 1fr;
  }
  .form-group-inline {
    flex-wrap: wrap;
  }
  .form-group-inline > input[type="number"] {
    width: 100%;
  }
  .form-group-inline > select {
    width: 100%;
  }
}
input[type=text],input[type=password],input[type=email],select,textarea{
  width:100%;padding:8px 10px;
  border:1px solid var(--border-strong);border-radius:var(--radius);
  background:var(--bg-card);color:var(--text);font:inherit;font-family:var(--font);
  transition:border-color .15s,box-shadow .15s;
}
/* 2026-08-17: v1.3.19.2 follow-up (B121) — form contrast.
   Pre-fix, form inputs on Linear (--bg #0a0a0a, --bg-card
   #131313) had a 1px --border-strong (#3a3a3a) border —
   only 2 shades of difference from --bg. The form
   "blended into" the page background, especially
   long forms with multiple stacked inputs (the operator
   reported "forms blend together"). The fix: bump the
   --border-strong contrast on Linear from #3a3a3a to
   #525252 (more visible on dark bg), and add a subtle
   inset shadow + larger focus ring. The same contrast
   bump is applied to NVIDIA and Sentry so all dark
   themes have crisper form edges. Light themes (Vercel
   + Mint) keep their original --border-strong values
   because dark text on light bg already has plenty of
   contrast — the bump would make forms look too "outlined".
   Plus a unified focus state with 4px ring (was 3px) +
   the input shifts up by 1px for tactile feedback. */
input[type=text]:focus,input[type=password]:focus,input[type=email]:focus,select:focus,textarea:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 4px var(--accent-soft);
  transform:translateY(-1px);
}
input[type=checkbox]{margin-right:6px;vertical-align:-2px;accent-color:var(--accent)}
select{padding-right:28px;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;appearance:none;-webkit-appearance:none}
/* B121: dark-theme form contrast bump — Linear / NVIDIA / Sentry. */
[data-theme="linear"] input[type=text],[data-theme="linear"] input[type=password],[data-theme="linear"] input[type=email],[data-theme="linear"] select,[data-theme="linear"] textarea,
[data-theme="nvidia"] input[type=text],[data-theme="nvidia"] input[type=password],[data-theme="nvidia"] input[type=email],[data-theme="nvidia"] select,[data-theme="nvidia"] textarea,
[data-theme="sentry"] input[type=text],[data-theme="sentry"] input[type=password],[data-theme="sentry"] input[type=email],[data-theme="sentry"] select,[data-theme="sentry"] textarea{
  border-width:1.5px;box-shadow:inset 0 1px 2px rgba(0,0,0,0.2);
}
/* B121: linear/nvidia input backgrounds are slightly elevated
   above --bg so they pop more. Sentry keeps its purple.
   v1.3.20.1 (B131): bumped from hardcoded #1a1a1a to var(--bg-elev)
   so the input bg tracks the new Linear palette (B131 lifted
   --bg-elev to #25282e). Without this fix the input bg would
   be DARKER than the surrounding card. */
[data-theme="linear"] input[type=text],[data-theme="linear"] input[type=password],[data-theme="linear"] input[type=email],[data-theme="linear"] select,[data-theme="linear"] textarea,
[data-theme="nvidia"] input[type=text],[data-theme="nvidia"] input[type=password],[data-theme="nvidia"] input[type=email],[data-theme="nvidia"] select,[data-theme="nvidia"] textarea{
  background:var(--bg-elev);
}
[data-theme="sentry"] input[type=text],[data-theme="sentry"] input[type=password],[data-theme="sentry"] input[type=email],[data-theme="sentry"] select,[data-theme="sentry"] textarea{
  background:#362d59;
}

/* BUTTONS */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;background:var(--accent);color:var(--accent-fg);border:1px solid transparent;border-radius:var(--radius);font:inherit;font-weight:500;font-size:14px;text-decoration:none;cursor:pointer;transition:all .15s;line-height:1.2}
.btn:hover{background:var(--accent-hover);text-decoration:none;color:var(--accent-fg)}
.btn:active{transform:translateY(1px)}
.btn.btn-secondary{background:var(--bg-card);color:var(--text);border-color:var(--border-strong)}
.btn.btn-secondary:hover{background:var(--bg-hover);color:var(--text)}
.btn.btn-danger{background:var(--danger);color:#fff}
.btn.btn-danger:hover{filter:brightness(1.1)}
.btn.btn-sm{padding:4px 10px;font-size:13px}
.btn i{font-size:13px}
.btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.btn-row.right{justify-content:flex-end}
.btn-row.gap-lg{gap:14px;margin-top:18px}

/* TAGS */
.tag{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:3px;font-size:12px;font-weight:500;line-height:1.5;font-family:var(--mono);background:var(--bg-elev);color:var(--text-muted);border:1px solid var(--border)}
.tag i{font-size:10px}
.tag-admin{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}
.tag-success{background:var(--success-bg);color:var(--success);border-color:var(--success)}
.tag-warning{background:var(--warning-bg);color:var(--warning);border-color:var(--warning)}
.tag-danger{background:var(--danger-bg);color:var(--danger);border-color:var(--danger)}
.tag-info{background:var(--info-bg);color:var(--info);border-color:var(--info)}
.status-dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;flex-shrink:0}
.status-online{background:var(--success);box-shadow:0 0 0 3px var(--success-bg)}
.status-offline{background:var(--text-subtle);opacity:0.5}
.status-relay{background:var(--warning);box-shadow:0 0 0 3px var(--warning-bg);animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}

/* COLOR SWATCH GRID (B137 — v1.3.20.6 hotfix).
   14 small colored squares for the /my/account Display form's
   selection_bg picker. The first tile is "no color" (use
   theme default), the next 12 are preset colors, the last is
   "custom" (reveals the text input). The selected tile has
   an accent ring. Works in all 5 themes because the colors
   are explicit hex values. */
.color-swatch-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;margin:8px 0 4px;max-width:420px}
.color-swatch{aspect-ratio:1;width:100%;border:1px solid var(--border);border-radius:5px;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;background:var(--bg-card);color:var(--text-muted);font-size:13px;transition:transform .1s,box-shadow .1s;position:relative}
.color-swatch:hover{transform:scale(1.08)}
.color-swatch:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.color-swatch.selected{box-shadow:0 0 0 2px var(--accent),0 0 0 4px var(--accent-soft);border-color:var(--accent)}
.color-swatch.swatch-default{background:var(--bg-elev);color:var(--text-subtle)}
.color-swatch.swatch-custom{background:var(--bg-elev);color:var(--text)}

/* ALERTS */
.alert{border:1px solid var(--border);border-left:3px solid var(--info);background:var(--info-bg);color:var(--info);padding:13px 16px;border-radius:var(--radius);margin-bottom:16px;font-size:14px;display:flex;align-items:flex-start;gap:10px}
.alert i{font-size:15px;margin-top:1px;flex-shrink:0}
.alert-success{border-left-color:var(--success);background:var(--success-bg);color:var(--success)}
.alert-warn{border-left-color:var(--warning);background:var(--warning-bg);color:var(--warning)}
.alert-danger{border-left-color:var(--danger);background:var(--danger-bg);color:var(--danger)}
/* 2026-08-20: B153 — inline pill badges for the /my/tokens
   page. Used by the ExpiresWarn column (expired / soon / month)
   and by the admin/backup page (badge-success / badge-info /
   badge-danger). Keeps the table compact while still colour-coding
   the urgency. The base class is just rounded-padding + small
   text; the variants supply the colour. */
.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:0.02em;line-height:1.4;vertical-align:middle;border:1px solid transparent;margin-left:6px}
.badge-expired{background:var(--danger-bg);color:var(--danger);border-color:var(--danger)}
.badge-soon{background:var(--danger-bg);color:var(--danger);border-color:var(--danger)}
.badge-month{background:var(--warning-bg);color:var(--warning);border-color:var(--warning)}
.badge-success{background:var(--success-bg);color:var(--success);border-color:var(--success)}
.badge-danger{background:var(--danger-bg);color:var(--danger);border-color:var(--danger)}
.badge-info{background:var(--info-bg);color:var(--info);border-color:var(--info)}
.badge-warn{background:var(--warning-bg);color:var(--warning);border-color:var(--warning)}
/* 2026-08-20: B157 — notification bell (sidebar).
   .notif-badge: small red circle with the unread
   count, rendered next to the bell icon. Only
   shows when UnreadCount > 0 (the template
   suppresses the <span> when the count is 0).
   .notif-menu: dropdown panel inside the bell's
   <details> wrapper. Wide enough for the title +
   body to read on one line each. .notif-item:
   each unread notification row. Stacked rows
   separated by a thin border. .notif-actions:
   the row's "Open" + "Mark as read" buttons,
   inline so they fit on one row. */
.notif-badge{display:inline-block;min-width:16px;height:16px;padding:0 5px;margin-left:4px;border-radius:999px;background:var(--danger);color:#fff;font-size:10px;font-weight:700;line-height:16px;text-align:center;vertical-align:middle}
.notif-menu{min-width:280px;max-width:340px;max-height:480px;overflow-y:auto;padding:0;background:var(--bg-elev,#fff);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 12px rgba(0,0,0,0.08)}
.notif-item{padding:10px 12px;border-bottom:1px solid var(--border);font-size:13px;display:flex;flex-direction:column;gap:4px}
.notif-item:last-child{border-bottom:none}
.notif-item-header{display:flex;align-items:center;gap:6px;color:var(--text,#222);line-height:1.3}
.notif-item-header b{font-weight:600}
.notif-icon-info{color:var(--info,#075985);font-size:14px}
.notif-icon-warn{color:var(--warning,#b45309);font-size:14px}
.notif-icon-danger{color:var(--danger,#dc2626);font-size:14px}
.notif-title{color:var(--text,#222);font-weight:600;line-height:1.3}
.notif-body{color:var(--text-muted,#666);font-size:12px;line-height:1.4}
.notif-actions{display:flex;gap:6px;margin-top:4px}
.notif-actions .menu-btn{padding:3px 8px;font-size:11px;flex:0 0 auto}
.notif-empty{padding:16px;text-align:center;color:var(--text-muted,#666);font-size:13px;font-style:italic}
/* 2026-08-20: B157.1 — full-page /my/notifications
   view. .notif-filter-pills: the All / Unread
   toggle at the top. Plain <a> links (not
   forms) so the URL is shareable. .notif-pill
   uses the same pill pattern as the B155
   ExpiresWarn badge so the visual style
   matches. .notif-table: per-row icon + title +
   body + time-ago + actions. .notif-icon-cell
   keeps the icon centred. .notif-icon-{info,*
   warn,danger}: the icon colour follows the
   B153/B155 severity palette. .notif-row.unread
   has a subtle background tint so the user can
   scan for unread at a glance. .notif-empty-page:
   the empty state when the user has zero
   notifications (large icon + "we'll let you
   know" copy). .notif-pagination: prev/next
   buttons + the "N total" hint at the bottom. */
.notif-filter-pills{display:flex;gap:6px;margin:0 0 16px 0;flex-wrap:wrap}
.notif-pill{display:inline-block;padding:6px 12px;border-radius:999px;background:var(--bg-elev,#fff);border:1px solid var(--border);color:var(--text-muted,#666);font-size:13px;text-decoration:none;transition:background 0.15s}
.notif-pill:hover{background:var(--info-bg,#f0f9ff);color:var(--text,#222)}
.notif-pill.active{background:var(--info-bg,#f0f9ff);color:var(--info,#075985);border-color:var(--info,#075985);font-weight:600}
.notif-table{width:100%;border-collapse:collapse}
.notif-table th{padding:8px 10px;text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted,#666);border-bottom:1px solid var(--border)}
.notif-table td{padding:12px 10px;border-bottom:1px solid var(--border);vertical-align:top}
.notif-row.unread{background:var(--info-bg,#f0f9ff)}
.notif-row.unread .notif-title{color:var(--text,#222)}
.notif-icon-cell{text-align:center;width:48px}
.notif-icon-info{color:var(--info,#075985);font-size:18px}
.notif-icon-warn{color:var(--warning,#b45309);font-size:18px}
.notif-icon-danger{color:var(--danger,#dc2626);font-size:18px}
.notif-title{color:var(--text,#222);font-size:14px;line-height:1.4;margin-bottom:2px}
.notif-body{color:var(--text-muted,#666);font-size:12px;line-height:1.4}
.notif-empty-page{text-align:center;padding:48px 16px;color:var(--text-muted,#666)}
.notif-empty-page i{font-size:48px;color:var(--text-subtle,#aaa);display:block;margin-bottom:16px}
.notif-empty-page p{font-size:14px;line-height:1.5;max-width:420px;margin:0 auto}
.notif-pagination{display:flex;justify-content:center;align-items:center;gap:12px;padding:16px 0 0 0}
/* 2026-07-14: Этап 14 v2 — Tailscale reachability probe. The
   .alert-probe class uses the same layout as .alert but adds
   room for a multi-line body (sub-text + a troubleshooting
   bullet list). The .probe-ok-direct / .probe-ok-relay variants
   use the success palette; .probe-unreachable uses the danger
   palette. The "ok_relay" variant also gets a subtle icon tint
   to hint "this is going through a relay" — operators on RF
   VPSes want visual confirmation that the relay path is active
   vs. a fallback to direct. */
.alert-probe{align-items:flex-start}
.alert-probe>div{flex:1;min-width:0}
.alert-probe ul{margin:0}
.probe-ok-direct{border-left-color:var(--success);background:var(--success-bg);color:var(--success)}
.probe-ok-relay{border-left-color:var(--success);background:var(--success-bg);color:var(--success)}
.probe-ok-relay i{color:var(--info)}
.probe-unreachable{border-left-color:var(--danger);background:var(--danger-bg);color:var(--danger)}

/* OS TILES */
.os-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:16px}
.os-tile{display:flex;flex-direction:column;align-items:center;gap:8px;padding:20px 12px;border:1px solid var(--border-strong);border-radius:var(--radius);cursor:pointer;background:var(--bg-card);font-size:14px;font-weight:500;transition:all .15s}
.os-tile:hover{border-color:var(--accent);background:var(--bg-hover)}
.os-tile input{display:none}
.os-tile.selected,.os-tile:has(input:checked){border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
.os-tile i{font-size:28px;color:var(--text-muted)}
.os-tile.selected i{color:var(--accent)}

/* KEY BLOCK */
.key-block{font-family:var(--mono);font-size:14px;background:#0a0a0a;color:#fafafa;border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;word-break:break-all;user-select:all;margin:12px 0;display:flex;align-items:center;gap:8px}
.key-block code{flex:1;color:#fafafa;background:transparent;padding:0;font-family:inherit;font-size:inherit}
.key-block .copy-btn{background:rgba(255,255,255,0.1);color:#fafafa;border:0;padding:4px 8px;border-radius:3px;cursor:pointer;font-size:12px;flex-shrink:0}
.key-block .copy-btn:hover{background:rgba(255,255,255,0.2)}

ul.plain{margin:0;padding-left:1.4em}
ul.plain li{margin-bottom:4px}

/* METRICS */
.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:16px}
.metric{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;display:flex;align-items:center;gap:12px}
.metric .icon{width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:var(--accent-soft);color:var(--accent);border-radius:var(--radius);font-size:16px;flex-shrink:0}
.metric .body{flex:1;min-width:0}
.metric .label{font-size:12px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:2px}
.metric .value{font-size:20px;font-weight:600;color:var(--text);line-height:1.1;font-feature-settings:"tnum"}
.metric .sub{font-size:12px;color:var(--text-subtle);margin-top:2px}
.metric.good .icon{background:var(--success-bg);color:var(--success)}
.metric.warn .icon{background:var(--warning-bg);color:var(--warning)}
.metric.bad .icon{background:var(--danger-bg);color:var(--danger)}

/* SECTION */
.section-title{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin:24px 0 10px;display:flex;align-items:center;gap:8px}
.section-title i{color:var(--accent);font-size:13px}

/* HELP/DOCS */
.doc-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px 28px;margin-bottom:18px}
.doc-card h3{font-size:17px;font-weight:600;margin:0 0 8px;display:flex;align-items:center;gap:8px;letter-spacing:-0.015em}
.doc-card h3 i{color:var(--accent);font-size:15px}
.doc-card h4{font-size:14px;font-weight:600;margin:20px 0 8px;color:var(--text);text-transform:uppercase;letter-spacing:0.04em}
.doc-card p{margin:8px 0;color:var(--text);line-height:1.65}
.doc-card ul,.doc-card ol{margin:8px 0;padding-left:1.4em}
.doc-card li{margin-bottom:6px;line-height:1.6}
.doc-card .flow{display:flex;align-items:center;gap:6px;padding:12px 14px;background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius);font-family:var(--mono);font-size:13px;margin:10px 0;flex-wrap:wrap;justify-content:center}
.doc-card .flow .node{background:var(--bg-card);padding:6px 10px;border:1px solid var(--border);border-radius:4px;color:var(--text)}
.doc-card .flow .node.you{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
.doc-card .flow .arrow{color:var(--text-subtle);font-size:14px}
.doc-card .flow .arrow.dashed{color:var(--warning)}
.doc-card .note{font-size:13px;color:var(--text-muted);margin-top:6px;padding-left:12px;border-left:2px solid var(--accent);font-style:italic}

.tabs{display:flex;gap:2px;background:var(--bg-elev);padding:3px;border-radius:var(--radius);border:1px solid var(--border);width:fit-content;margin-bottom:14px}
.tabs a{padding:6px 13px;border-radius:4px;text-decoration:none;color:var(--text-muted);font-size:13px;font-weight:500;display:inline-flex;align-items:center;gap:5px}
.tabs a:hover{color:var(--text)}
.tabs a.active{background:var(--bg-card);color:var(--text);box-shadow:var(--shadow)}
.tabs a i{font-size:12px}

footer{margin-top:48px;padding:24px 0;border-top:1px solid var(--border);text-align:center;font-size:12px;color:var(--text-subtle);font-family:var(--mono)}
footer a{color:var(--text-muted);text-decoration:none;margin:0 6px}
footer a:hover{color:var(--accent)}

@media (max-width:768px){
  /* 2026-08-12: v1.1.0 (TD-3) — B97 pins this breakpoint.
     Below 768px the sidebar becomes a slide-in drawer driven
     by the checkbox hack (.sidebar-toggle-input +
     .sidebar-toggle label). The hamburger button is
     display:flex on mobile (display:none on desktop). The
     sidebar starts off-screen (translateX(-100%)) and slides
     in when the checkbox is checked (:checked ~ .sidebar).
     The main content area gets a 16px left padding so the
     hamburger doesn't overlap the page title. */
  .sidebar-toggle{display:flex}
  .sidebar-toggle-input:checked ~ .sidebar-toggle{left:auto;right:12px}
  .sidebar{
    transform:translateX(-100%);transition:transform .25s;
    width:280px !important;box-shadow:var(--shadow-lg);
  }
  .sidebar-toggle-input:checked ~ .sidebar{
    transform:translateX(0);
  }
  /* When the drawer is open, dim the main content. We use
     a body class via the checkbox's :checked sibling selector
     (the body has to be a sibling, not main, because <main>
     is a sibling of <input>, not of <body>). The label is
     clickable from anywhere in the viewport — the body class
     is what gives the visual feedback. */
  .sidebar-toggle-input:checked ~ main::before{
    content:"";position:fixed;inset:0;background:rgba(0,0,0,0.4);
    z-index:90;cursor:pointer;
  }
  /* Reset the desktop's fixed 220px content offset — on mobile
     the sidebar is overlay, not a column. */
  main .shell{margin-left:0 !important}
  /* 2026-08-17: v1.3.19.2 (B120) — reset the breadcrumb's
     desktop margin-left:220px to 0 on mobile (the breadcrumb
     fills the viewport on mobile, the sidebar is a drawer).
     The .admin-breadcrumb rule below adds padding-left:60px
     to clear the 40px hamburger (B107) — both rules together
     give us: left=0 (no column), padding-left=60px (hamburger
     clearance), width=100% (full viewport). The base rule
     already has padding-left:40px, so 60px overrides it. */
  main .admin-breadcrumb{margin-left:0 !important;width:100% !important}
  header{margin-left:0 !important}
  /* 2026-08-13: if the user previously toggled the sidebar to
     collapsed (e.g. on a tablet and then resized to mobile),
     the `collapsed` class persists in the DOM via JS
     classList.toggle (no auto-reset on resize). The mobile
     drawer pattern wants the sidebar at full 280px when open
     (drawer style) and off-screen via transform when closed.
     Force-clear the collapsed class on mobile so a stuck
     state doesn't make the sidebar 52px wide inside a 280px
     drawer. The `width:280px !important` above would still
     apply, but the spec is ambiguous and the !important is
     fragile. This explicit clear removes the ambiguity. */
  .sidebar.collapsed{width:280px}
  /* Sections: always open on mobile (operator shouldn't have
     to tap 6 times to find a page). The caret is removed for
     visual cleanliness. */
  .sidebar-section[open]>summary::before{transform:none}
  .sidebar-section>summary{cursor:default}

  /* 2026-08-12: v1.1.0 (TD-3) — touch-friendly tap targets.
     Apple's HIG / Material both recommend 44x44 px min for
     primary interactive elements. Sidebar nav links + buttons
     get 12px vertical padding (was 8px on desktop) so thumb
     taps are reliable on a phone. */
  .sidebar nav a{padding:12px 14px;font-size:14px;min-height:44px}
  .sidebar .toggle{width:44px;height:44px}
  .sidebar .shead{padding:14px 16px}
  .sidebar-user .sidebar-actions button{padding:12px 14px;min-width:44px;min-height:44px}
  .sidebar nav a i{font-size:15px;width:20px}
  .theme-picker-sb summary{padding:10px 12px;min-height:44px}

  /* v0.33.1.7: header + main shell compaction (kept from the
     pre-v1.1.0 760px breakpoint; breakpoint renamed to 768px
     so it matches the iPad-portrait width). */
  header .shell{flex-wrap:wrap;height:auto;padding:8px 14px;gap:8px;padding-left:60px}
  header h1{font-size:14px}

  header .user-area{flex-basis:100%;justify-content:space-between;padding-bottom:6px}
  main{padding:20px 0 48px}
  .shell{padding:0 14px}
  .card{padding:16px}
  .metric-grid{grid-template-columns:repeat(2,1fr);gap:8px}
  .title-row h2{font-size:18px}
  /* 2026-08-13: page title offset. The .sidebar-toggle
     hamburger is fixed at top:12px,left:12px and 40×40px —
     it sits on top of the page title (h2 in .title-row) on
     narrow viewports because the .title-row also starts at
     12px from the left edge. The pre-fix CSS had a comment
     claiming "16px left padding so the hamburger doesn't
     overlap" but never actually applied it. The fix: add
     padding-left:60px to .title-row on mobile (matching the
     header .shell's existing padding-left:60px). The 60px
     gives the 40px hamburger + 8px breathing room + 12px
     existing edge gap. The right-side controls (.title-row's
     justify-content:space-between) still work because
     padding doesn't break flexbox — the h2 just gets a
     left margin. */
  .title-row{padding-left:60px}
  /* 2026-08-13: breadcrumb offset. The .admin-breadcrumb
     nav (renders the "Админ › Устройства и узлы › Все устройства"
     path on every admin page) is rendered at the top of the
     main area, BELOW the header. On mobile the .sidebar-toggle
     hamburger is fixed at top:12px,left:12px,40×40px — it
     visually overlaps the start of the breadcrumb because the
     breadcrumb's first icon (fa-house crumb-home) sits at
     x=24 (left padding 24px). The 2026-08-13 v1.3.9 fix
     added padding-left:60px to .title-row (the page h2) but
     the breadcrumb is a SIBLING nav, not a child of .title-row
     — it didn't get the same offset. The operator-reported
     symptom: "Админ" is half-hidden behind the hamburger on
     /admin/devices (and any admin page with a breadcrumb).
     The fix: add the same 60px left padding to .admin-breadcrumb
     so the breadcrumb starts AT the same x as the page title
     (both clear the 40px hamburger + 8px gap + 12px edge). */
  .admin-breadcrumb{padding-left:60px}
  /* Tables: force the existing .table-wrap (B50) to scroll
     horizontally rather than squishing columns. */
  .table-wrap{margin:0 -14px;padding:0 14px}
  /* Form buttons: bigger hit target on mobile. */
  .btn{padding:10px 16px;font-size:14px;min-height:40px}
  .btn-sm{padding:6px 12px;min-height:32px}
  /* 2026-08-13: hide the in-sidebar .toggle button on
     mobile. MUST come AFTER the earlier
     `.sidebar .toggle{width:44px;height:44px}` rule (the
     v1.1.0 touch-target rule, line ~488) so the later
     `display:none` wins the cascade. The two rules have
     equal specificity (0,0,2,0); cascade resolves by source
     order. If this rule is placed BEFORE the touch-target
     rule, the .toggle button remains visible (just at 44x44
     — the wrong behavior on mobile). The in-sidebar .toggle
     is the desktop "compact mode" button (220px ↔ 52px);
     on mobile the sidebar is a drawer controlled by the
     .sidebar-toggle-input checkbox + .sidebar-toggle
     hamburger label at top:12px,left:12px, so the in-sidebar
     .toggle is irrelevant and just confuses the user when
     they tap it (no visible effect because the
     `width:280px !important` on .sidebar overrides the
     `width:52px` from .collapsed). */
  .sidebar .toggle{display:none}
}

/* Align username/password fields in the create-user form on /admin/users. */
.user-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 12px;
  align-items: end;
}
.ufg-field {
  display: flex;
  flex-direction: column;
  min-width: 0; /* prevent grid blowout on long placeholders */
}
.ufg-label {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 4px;
  display: flex;
  align-items: center;
  gap: 4px;
}
.ufg-input,
.ufg-input-sm {
  width: 100%;
  height: 36px;
  padding: 0 10px;
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  font-family: inherit;
  font-size: 14px;
  box-sizing: border-box;
}
.ufg-input-sm {
  height: 30px;
  font-size: 13px;
}
.ufg-btn {
  height: 36px;
  align-self: end;
}

/* Per-row action menu in the users table. */
.user-actions { position: relative; }
.user-actions summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
  height: 28px;
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
}
.user-actions summary::-webkit-details-marker { display: none; }
.user-actions[open] summary { background: var(--border); }
.user-actions-menu {
  position: absolute;
  right: 0;
  top: 32px;
  z-index: 50;
  background: var(--card-bg, var(--bg));
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px;
  min-width: 220px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.25);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.user-reset-form {
  display: flex;
  gap: 6px;
  align-items: center;
}
