/* ============================================================
   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:#8a8f9f;
  --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;
/* B414 (2026-10-24) — SEMANTIC ALIASES, and why they exist.
   Templates have been calling these names for years (57 references, measured) while
   themes.css never defined them. The visible consequence was `var(--bg-subtle,#111)`:
   the fallback painted a near-black block in EVERY theme, so on the light themes the
   block's inherited dark text sat on #111 and vanished — the operator's «в новых
   полях не видно текст» report. One alias per name keeps every template's intent
   working in every theme, and B414 fails the gate if a template references a token
   this file does not define. */
--bg-subtle:var(--bg-elev);--bg-muted:var(--bg-elev);--bg-secondary:var(--bg-card);
--bg-input:var(--bg);--bg-info:var(--info-bg);--bg-warn:var(--warning-bg);
--bg-2:var(--bg-elev);--bg2:var(--bg-elev);--card-bg:var(--bg-card);--card-elevated:var(--bg-elev);
--fg:var(--text);--font-mono:var(--mono);--error:var(--danger);--warning-fg:var(--text);
--green:var(--success);--red:var(--danger);--warn:var(--warning);
--accent-success:var(--success);--accent-danger:var(--danger);--accent-warning:var(--warning);

}

/* 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:#71717a;
  --accent:#0a0a0a;--accent-fg:#ffffff;--accent-hover:#27272a;--accent-soft:rgba(10,10,10,0.06);
  --success:#15803d;--success-bg:rgba(22,163,74,0.08);
  --warning:#b45309;--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:#948b9d;
  --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:#b6a3d0;--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:#808080;
  --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:#67716b;
  --accent:#10b981;--accent-fg:#0a0a0a;--accent-hover:#059669;--accent-soft:rgba(16,185,129,0.1);
  --success:#0a7a57;--success-bg:rgba(16,185,129,0.1);
  --warning:#9a5b00;--warning-bg:rgba(217,119,6,0.08);
  --danger:#b91c1c;--danger-bg:rgba(220,38,38,0.08);
  --info:#0e7490;--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 */
/* B392 (2026-10-16) — overflow:visible, and the reason matters.
   This was `overflow:hidden`. The notification bell lives in the sidebar footer and
   its dropdown is an absolutely-positioned child, so the clip box of THIS element
   cut the panel off at the sidebar's own width (220px): the operator reported «при
   нажатии всплывающее окно перекрывается вкладкой» and the numbers explain it —
   the panel asks for up to 340px, starts ~8px from the sidebar edge, and the content
   column begins exactly at 220px, so ~38% of it was clipped (in the 52px collapsed
   state only a ~44px sliver was visible). It was NOT a z-index problem: the panel
   already had z-index:200, the highest in the sidebar.
   Nothing depends on this clip: the nav has its own scroller (`.sidebar nav`,
   overflow-y:auto), and the collapsed state hides labels/sections by display:none
   and width:0 rather than by clipping. The mobile drawer keeps its own transform. */
.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:visible;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}
/* B392 — the bell's panel needs its OWN sizing, and it must be scoped to win.
   `.theme-picker-sb .menu` above (specificity 0,2,0) was overriding `.notif-menu`
   (0,1,0), so the declared min-width:280px never applied and the effective floor was
   140px. Sizes are clamped to the viewport so the panel stays readable inside the
   280px mobile drawer and never runs off a 360px screen; the height is clamped to
   60vh so the top edge cannot end up above the viewport (the old 480px could, and
   the clipped ancestor was not a scroller, so it could not be scrolled back into view). */
.theme-picker-sb .notif-menu{min-width:min(280px,calc(100vw - 24px));max-width:min(340px,calc(100vw - 24px));max-height:min(480px,60vh);overflow-y:auto}
.notif-menu-head{position:sticky;top:0;background:var(--bg-elev,#fff);border-bottom:1px solid var(--border);z-index:1}
.notif-time{margin-left:auto;color:var(--text-muted,#666);font-size:11px;white-space:nowrap}
.notif-more{padding:8px 12px;color:var(--text-muted,#666);font-size:12px;text-align:center;border-bottom:1px solid var(--border)}
.notif-menu-foot{display:flex;align-items:center;gap:8px;padding:10px 12px;color:var(--accent);font-size:13px;font-weight:600;text-decoration:none}
.notif-menu-foot:hover{background:var(--bg-hover)}
.sidebar nav a .notif-badge{margin-left:auto}
.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}
/* B411 (2026-10-22) — THE OFFSET BELONGS TO <main>, NOT TO .shell.
   The operator reported «большинство страниц на широких экранах оставляет
   справа много свободного места». Measured: .shell carries
   `max-width:1180px; margin:0 auto`, and this rule then overrode ONLY the left
   margin to 220px — so `margin-right:auto` swallowed every pixel of slack and
   the content stayed pinned to the sidebar's edge while the free space piled up
   on the right: 1366px viewport → 0px empty, 1920px → 520px, 2560px → 1160px,
   3440px → 2040px. The shell was never centred; it was left-aligned inside a
   centred 1180px box.

   Fix: the sidebar reservation moves to <main> (one column, both sides of the
   layout agree), and .shell keeps its own `margin:0 auto` — so it now centres
   inside the space that is actually available. Nothing about the 1180px reading
   width changes below 1440px, which is what the layout was tuned for. */
main{margin-left:220px;transition:margin-left .25s}
.sidebar.collapsed ~ main{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.

   B120 fixed that by MIRRORING the .shell offset onto the
   breadcrumb. B411 (2026-10-22) removed the need to mirror it:
   the reservation now lives on <main> itself, so BOTH the
   breadcrumb and .shell inherit the same left edge by being
   inside it. The mirror is therefore gone — and it must stay
   gone, because a 220px margin here would now be applied twice
   (once by <main>, once by this rule) and push the breadcrumb
   into the middle of the page. */
main .admin-breadcrumb{margin-left:0;width:100%}

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}
/* B411 — wide screens get more room, not more empty band. 1180px stays the
   reading width up to 1600px (where the old layout already ran out of room);
   above that the shell grows in two steps so data-heavy admin pages — the
   9-column history table, /admin/database, /admin/cluster — use the display
   they were given instead of leaving 500-2000px blank on the right. The
   selector is `main .shell` (specificity 0,1,1) so it wins over `.shell`. */
@media (min-width:1600px){main .shell{max-width:1400px}}
@media (min-width:2200px){main .shell{max-width:1600px}}

/* 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}
/* B392: the sizing/scroll of the bell panel lives in the scoped rule next to
   `.theme-picker-sb .menu` (which would otherwise win on specificity). This rule
   keeps only the shared box styling, so there is exactly one place that decides how
   wide and how tall the panel may be. */
.notif-menu{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)}
/* B356.1 (2026-10-07): the bot is reaching api.telegram.org through an SSH
   tunnel to a peer relay because the portal's own egress is blocked. Green
   (notifications ARE being delivered) with the info-tinted icon, so it is never
   mistaken for the kernel subnet-route case above. */
.probe-ok-relay-tunnel{border-left-color:var(--info);background:var(--success-bg);color:var(--success)}
.probe-ok-relay-tunnel 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}

/* B435 (2026-10-28) — THE FOOTER MUST CARRY THE SAME 220px OFFSET AS <main>.
   `<footer>` is a SIBLING of <main> (layout.html), not its child, so the desktop rule
   `main{margin-left:220px}` never applied to it: the footer's left half was painted UNDER
   the fixed 220px sidebar, and the operator read that as «меню и футтер наплывают друг на
   друга». The same offset is applied here (the mobile @media block below resets it to 0,
   where the sidebar is an overlay drawer rather than a column). */
footer{margin-left:220px;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);
    /* B418 (2026-10-25) — THE DRAWER MUST BE THE VISIBLE VIEWPORT, NOT 100vh.
       On a phone `100vh` is the LARGE viewport (browser UI hidden), so the drawer's
       last block — theme / language / logout — was painted BELOW the visible edge;
       and because the drawer is position:fixed, scrolling the page could not bring it
       into view until the URL bar collapsed (the operator: «кнопки в меню в самом низу
       появляются только если пролистать страницу в самый низ»). Anchor it instead:
       top:0 + bottom:0 with height:auto makes it exactly the visible viewport, and
       max-height:100dvh caps it where the dynamic unit is supported. */
    top:0;bottom:0;height:auto;max-height:100dvh;
    /* B418: and it must NOT clip. `overflow:hidden` would cut the bell / theme /
       language popovers, which are absolutely-positioned children of `.sidebar-actions`
       and hang PAST the 280px edge — B392 measured that cut at ~38 % of the panel and
       the operator saw it as «перекрывается вкладкой». Nothing can spill anyway now
       that the box is anchored to the viewport, the nav is the only scroller
       (flex:1 + min-height:0) and `.sidebar-user` is flex-shrink:0. */
    padding-bottom:env(safe-area-inset-bottom,0px);
  }
  .sidebar-toggle-input:checked ~ .sidebar{
    transform:translateX(0);
  }
  /* B418: the drawer's dim layer is a REAL element (a label for the same checkbox),
     not `main::before`. The pseudo-element covered the viewport visually but could not
     close the drawer, and the footer — a SIBLING of <main>, not its child — was still
     reachable by a tap while the menu was open (the operator: «меню перекрывает
     футтер»). A label gives the same dimming, closes the drawer on tap, and blocks the
     footer underneath it. */
  .sidebar-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:90;cursor:pointer}
  .sidebar-toggle-input:checked ~ .sidebar-backdrop{display:block}
  /* While the drawer is open the page behind must not scroll: on a phone that scroll is
     what used to reveal the drawer's own bottom block. `:has()` is unsupported on old
     browsers, where this simply does not apply (nothing breaks). */
  body:has(.sidebar-toggle-input:checked){overflow:hidden}
  /* The nav is the only scroller inside a flex column; without min-height:0 a flex item
     refuses to shrink below its content and the bottom block is pushed out of view. */
  .sidebar nav{min-height:0}
  /* Reset the desktop's fixed 220px content offset — on mobile
     the sidebar is overlay, not a column. B411 moved that offset
     from .shell to <main>, so the reset follows it: resetting
     .shell alone would leave the whole page shifted 220px right. */
  main{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}
  /* B435: the footer is <main>'s sibling, so it needs the same reset the desktop rule above
     demands — otherwise the 220px offset added for desktop would clip it on a phone. */
  footer{margin-left:0 !important;padding-left:14px !important;padding-right:14px !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;
}

/* ── B326 (2026-09-25) — narrow screens: a table must never push the PAGE sideways ────
   Operator report: «таблицы становятся широкими и большими … некоторые таблицы начинают
   ломаться и не поддерживают скролл уходя за экран отображения».

   The B1–B320-style audit of the panel (docs/gate-regression-power.md's sibling review)
   measured why: `body{overflow-x:hidden}` (themes.css, body rule) means there is NO
   page-level horizontal scrollbar at all — a wide table is not "scrollable off-screen",
   its right-hand columns are silently CLIPPED and unreachable. The only scroller is
   `.table-wrap{overflow-x:auto}`, which is opt-in: 64 of the 84 tables in the templates do
   not use it. The min-content width is inflated further by `table{width:100%}` plus
   `th{white-space:nowrap}` (one page has 830px of headers alone).

   Fixing 64 templates by hand is a large, error-prone sweep, so the fix here is systemic and
   markup-free, and only under the mobile breakpoint:
     * the CARD becomes the horizontal scroll container — every table inside a card scrolls
       instead of clipping, and the table keeps its normal table layout (unlike the
       `table{display:block}` trick, which would also drop `width:100%`);
     * `th{white-space:normal}` removes most of the forced min-width;
     * long tokens (keys, DSNs, UUIDs) wrap instead of defining a column;
     * form controls and button/header rows cannot exceed the card.
   Desktop is untouched. */
@media (max-width:768px){
  .card{overflow-x:auto}
  /* B357: 62 of 89 tables have no .table-wrap, and body{overflow-x:hidden} (themes.css:209)
     CLIPS their overflow instead of scrolling it — a wide table simply lost its right-hand
     columns on a phone. One rule on the mobile shell gives every bare table a scroller. */
  .shell{overflow-x:auto}
  th{white-space:normal}
  th,td{padding:8px 8px;font-size:13px}
  /* B357 (2026-10-06) — «П о л ь з о в а т е л ь»: `overflow-wrap:anywhere` REDUCES the
     intrinsic min-content width of a cell to ONE character, so under the auto table layout
     of `table{width:100%}` (:460) the column collapses to ~29px and every value prints one
     glyph per line — measured on /admin/devices at 390px: `100.64.0.3` filled 10 lines at
     1.00 chars/line. `break-word` does NOT shrink min-content: the cell keeps its longest
     token, the table exceeds the card, and the scroller (`.card{overflow-x:auto}` above)
     does the work instead of destroying the text. The same reasoning applies to every
     `word-break:break-all` a template set inline — those are gone (B357). */
  td code,td .mono,td pre,code,.mono,pre{overflow-wrap:break-word}
  /* Addresses and timestamps are what the operator reads first, and they have no
     meaningful break opportunity: keep them on one line and let the table scroll. */
  td .mono,td code{white-space:nowrap}
  select,input,textarea{max-width:100%}
  .btn-row,.title-row{flex-wrap:wrap}
  /* The create-user form is a hard 3-column grid with no mobile override. */
  .user-form-grid{grid-template-columns:1fr}
  /* B326.1 (2026-09-25) — the operator's «проблемы с отображением форм» half: several forms
     carry an INLINE `display:grid;grid-template-columns:repeat(2|3,1fr)` (admin/cluster.html
     x2, admin/exit_rules.html), which no media query can reach because the columns live in a
     style attribute. An attribute selector collapses every inline fixed-column grid to one
     column on narrow screens — markup-free, and the desktop layout is untouched. `!important`
     is required to beat the inline declaration. */
  [style*="grid-template-columns"]{grid-template-columns:1fr!important}
  /* The same for inline min-widths on controls (audit §2: 119 inline px widths). */
  select[style*="min-width"],input[style*="min-width"]{min-width:0!important}
}

