/* ohShield Security Dashboard — design tokens ported 1:1 from
   design_handoff_ohshield_landing_dashboard/OhShield Dashboard.dc.html.
   Theme is a data-attribute on <html> instead of the prototype's JS-driven
   inline style mutation; ink-70/55/40/etc. derive from --ink-rgb via rgba()
   so only the base + the tokens the prototype's `themes` object actually
   overrides need restating per theme. */

:root {
  --ink-rgb: 1,24,65;
  --ink: #011841;
  --on-ink: #fff;
  /* ink-85 down through ink-25 are all used as *text* color at various emphasis
     levels (labels, meta, timestamps) — raised from the original mockup's
     alphas for readability at the 9-13px sizes they're mostly used at.
     ink-18/ink-08 are background/hover washes, not text, and are left alone. */
  --ink-85: rgba(var(--ink-rgb),.88);
  --ink-70: rgba(var(--ink-rgb),.74);
  --ink-55: rgba(var(--ink-rgb),.62);
  --ink-40: rgba(var(--ink-rgb),.50);
  --ink-35: rgba(var(--ink-rgb),.44);
  --ink-30: rgba(var(--ink-rgb),.38);
  --ink-25: rgba(var(--ink-rgb),.32);
  --ink-18: rgba(var(--ink-rgb),.14);
  --ink-08: rgba(var(--ink-rgb),.08);
  --cyan: #189aa6;
  --cyan-deep: #004e8f;
  --cyan-35: rgba(24,154,166,.32);
  --cyan-16: rgba(24,154,166,.16);
  --coral: #d62817;
  --coral-deep: #a81d10;
  --coral-35: rgba(214,40,23,.3);
  --coral-16: rgba(214,40,23,.13);
  --gold: #d66f14;
  --gold-35: rgba(214,111,20,.3);
  /* Amber — matches embedColors.js's FALSE_POSITIVE (0xE0A526), reused here so
     the "administrator" tier badge stays consistent with the bot's own amber. */
  --amber: #e0a526;
  --amber-16: rgba(224,165,38,.16);
  /* "you can add ohShield here" — opportunity/invite, a 4th semantic color
     distinct from cyan's "healthy/active" meaning. Used by the admin-
     elsewhere guild-picker/onboarding shell only (see .admin-only below). */
  --mint: #2f9e58;
  --mint-deep: #1c7a40;
  --mint-35: rgba(47,158,88,.32);
  --mint-16: rgba(47,158,88,.14);
  --tri-navy: #02163f;
  --tri-cyan: #03b9cb;
  --tri-cyan-16: rgba(3,185,203,.16);
  --tri-maroon: #632037;
  --tri-base: #123047;
  --wash: rgba(222,223,222,.46);
  --wash-strong: rgba(244,244,243,.85);
  --sel-bg: rgba(196,196,192,.5);
  --hairline: rgba(var(--ink-rgb),.1);
  --bloom: inset 0 1px 0 rgba(255,255,255,.7), 0 32px 70px -30px rgba(120,86,48,.3), 0 3px 14px -6px rgba(255,251,242,.55);
  --bloom-cyan: inset 0 1px 0 rgba(255,255,255,.65), 0 32px 66px -32px rgba(18,88,84,.26), 0 3px 14px -6px rgba(219,240,236,.4);
  --bloom-warm: inset 0 1px 0 rgba(255,255,255,.65), 0 32px 66px -30px rgba(140,80,40,.28), 0 3px 14px -6px rgba(250,228,202,.4);
  --page-bg: linear-gradient(160deg,#f3f3f2 0%,#f1f2f2 38%,#f4f3f1 70%,#f2f2f1 100%);
  --glow-core: rgba(255,251,241,.98);
  --glow-mid: rgba(255,240,212,.62);
  --glow-edge: rgba(255,222,175,.26);
  --vignette: rgba(11,24,48,.11);
  --mullion: rgba(11,24,48,.055);
  --r-a: 38px 10px 38px 10px;
  --r-b: 6px;
  --r-md: 20px;
  --r-sm: 12px;
  --r-lg: 26px;
}

:root[data-theme="dark"] {
  --ink-rgb: 228,234,242;
  --ink: #e4eaf2;
  --on-ink: #0b1830;
  --cyan-deep: #1f9fc9;
  --coral-deep: #e0543c;
  --mint: #3ecf74;
  --mint-deep: #4ade80;
  --mint-35: rgba(74,222,128,.32);
  --mint-16: rgba(74,222,128,.16);
  --tri-navy: #1c3f72;
  --tri-maroon: #9c3049;
  --tri-base: #1c3554;
  --wash: rgba(30,36,48,.55);
  --wash-strong: rgba(19,23,33,.88);
  --sel-bg: rgba(42,46,55,.65);
  --bloom: inset 0 1px 0 rgba(255,255,255,.07), 0 32px 70px -30px rgba(0,0,0,.6), 0 3px 14px -6px rgba(36,214,229,.14);
  --bloom-cyan: inset 0 1px 0 rgba(255,255,255,.06), 0 32px 66px -32px rgba(0,0,0,.55), 0 3px 14px -6px rgba(36,214,229,.22);
  --bloom-warm: inset 0 1px 0 rgba(255,255,255,.06), 0 32px 66px -30px rgba(0,0,0,.55), 0 3px 14px -6px rgba(255,58,42,.18);
  --page-bg: linear-gradient(160deg,#0a0e17 0%,#0d1421 38%,#171120 70%,#0a0e16 100%);
  --glow-core: rgba(255,225,190,.16);
  --glow-mid: rgba(255,205,160,.10);
  --glow-edge: rgba(255,190,140,.05);
  --vignette: rgba(0,0,0,.55);
  --mullion: rgba(255,255,255,.05);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink-rgb: 228,234,242;
    --ink: #e4eaf2;
    --on-ink: #0b1830;
    --cyan-deep: #1f9fc9;
    --coral-deep: #e0543c;
    --mint: #3ecf74;
    --mint-deep: #4ade80;
    --mint-35: rgba(74,222,128,.32);
    --mint-16: rgba(74,222,128,.16);
    --tri-navy: #1c3f72;
    --tri-maroon: #9c3049;
    --tri-base: #1c3554;
    --wash: rgba(30,36,48,.55);
    --wash-strong: rgba(19,23,33,.88);
    --sel-bg: rgba(42,46,55,.65);
    --bloom: inset 0 1px 0 rgba(255,255,255,.07), 0 32px 70px -30px rgba(0,0,0,.6), 0 3px 14px -6px rgba(36,214,229,.14);
    --bloom-cyan: inset 0 1px 0 rgba(255,255,255,.06), 0 32px 66px -32px rgba(0,0,0,.55), 0 3px 14px -6px rgba(36,214,229,.22);
    --bloom-warm: inset 0 1px 0 rgba(255,255,255,.06), 0 32px 66px -30px rgba(0,0,0,.55), 0 3px 14px -6px rgba(255,58,42,.18);
    --page-bg: linear-gradient(160deg,#0a0e17 0%,#0d1421 38%,#171120 70%,#0a0e16 100%);
    --glow-core: rgba(255,225,190,.16);
    --glow-mid: rgba(255,205,160,.10);
    --glow-edge: rgba(255,190,140,.05);
    --vignette: rgba(0,0,0,.55);
    --mullion: rgba(255,255,255,.05);
  }
}

* { box-sizing: border-box; }
html, body { margin: 0; }
html { scrollbar-gutter: stable; }
body {
  -webkit-font-smoothing: antialiased;
  position: relative;
  min-height: 100vh;
  background: var(--page-bg);
  font-family: 'Space Grotesk', sans-serif;
  color: var(--ink);
  overflow-x: hidden;
  transition: background .3s ease;
}
::selection { background: rgba(36,214,229,.3); }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; padding: 0; }

/* ── Atmosphere (fixed background layer) ─────────────────────────────────── */
.atmosphere { position: fixed; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
.atmosphere .glow {
  position: absolute; width: 1440px; height: 1040px; left: -360px; top: -480px;
  background: radial-gradient(ellipse 58% 52% at 44% 40%, var(--glow-core) 0%, var(--glow-mid) 30%, var(--glow-edge) 50%, rgba(255,222,175,0) 74%);
  filter: blur(64px); transform-origin: 44% 40%;
}
.atmosphere .bounce-a {
  position: absolute; width: 760px; height: 520px; left: -140px; bottom: -280px; border-radius: 50%;
  background: radial-gradient(ellipse at 55% 45%, var(--coral-35), rgba(255,58,42,0) 72%);
  filter: blur(95px);
}
.atmosphere .bounce-b {
  position: absolute; width: 900px; height: 700px; right: -320px; bottom: -320px; border-radius: 50%;
  background: radial-gradient(ellipse at 50% 50%, var(--cyan-35), rgba(36,214,229,0) 72%);
  filter: blur(110px);
}
.atmosphere .vignette {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 1000px 780px at 86% 98%, var(--vignette), rgba(11,24,48,0) 68%);
}
/* A small 160px noise tile rasterized once and repeated, instead of a live
   fullscreen feTurbulence filter (which recomputes every composite and can
   stall the renderer / drop layers underneath it — see site/index.html,
   which made the same tradeoff for the landing page's grain layer). */
.film-grain {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  opacity: .4; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='saturate' values='0' result='g'/%3E%3CfeComponentTransfer in='g'%3E%3CfeFuncA type='linear' slope='0.6' intercept='0'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}
/* ── Top bar ──────────────────────────────────────────────────────────────── */
.topbar {
  position: sticky; top: 0; z-index: 30; display: flex; align-items: center; gap: 28px;
  padding: 14px 32px; background: var(--wash-strong);
  backdrop-filter: blur(22px) saturate(140%); -webkit-backdrop-filter: blur(22px) saturate(140%);
  border-bottom: 1px solid var(--hairline);
}
.topbar .logo-lockup { display: flex; flex-direction: column; gap: 5px; flex-shrink: 0; }
.topbar .logo-row { display: flex; align-items: center; gap: 6px; }
.topbar .logo-row img { width: 38px; height: 38px; object-fit: contain; filter: drop-shadow(0 4px 14px var(--cyan-35)); }
.topbar .logo-row span { font-size: 19px; font-weight: 500; letter-spacing: -0.02em; line-height: 1; }
.topbar .logo-sub { font: 9px/1 'IBM Plex Mono', monospace; letter-spacing: .16em; color: var(--ink-40); text-transform: uppercase; }
.topbar .spacer { flex: 1; }
.topbar .clock { text-align: right; flex-shrink: 0; }
.topbar .clock .time { font: 24px/1 'IBM Plex Mono', monospace; letter-spacing: .02em; color: var(--ink); text-shadow: 0 0 22px var(--cyan-35); }
.topbar .clock .date { font: 10px/1 'IBM Plex Mono', monospace; color: var(--ink-40); margin-top: 6px; letter-spacing: .06em; }
.theme-toggle-wrap { display: flex; align-items: center; gap: 9px; flex-shrink: 0; padding-left: 20px; border-left: 1px solid var(--hairline); }
.theme-toggle-wrap .label { font: 10px 'IBM Plex Mono', monospace; letter-spacing: .1em; text-transform: uppercase; }
.theme-toggle-wrap .label.dim { color: var(--ink-30); }
.theme-toggle-wrap .label.lit { color: var(--ink-70); }
.theme-switch {
  width: 42px; height: 23px; border-radius: 999px; background: var(--ink-08); position: relative;
  cursor: pointer; flex-shrink: 0; transition: background .2s ease;
}
.theme-switch.on { background: var(--cyan-deep); }
.theme-switch .knob {
  position: absolute; top: 2px; left: 2px; width: 19px; height: 19px; border-radius: 50%;
  background: var(--wash-strong); box-shadow: 0 2px 6px rgba(11,24,48,.25), inset 0 1px 0 rgba(255,255,255,.6);
  transition: left .2s ease;
}
.theme-switch.on .knob { left: 21px; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.8); }

/* ── Layout shell ─────────────────────────────────────────────────────────── */
.shell { position: relative; z-index: 1; max-width: 1560px; margin: 0 auto; padding: 32px 32px 100px; display: flex; gap: 28px; align-items: flex-start; }

/* ── Left rail ────────────────────────────────────────────────────────────── */
.rail { width: 224px; flex-shrink: 0; position: sticky; top: 92px; display: flex; flex-direction: column; gap: 22px; }
.rail-label { font: 11px/1 'IBM Plex Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-55); padding: 0 10px; margin-bottom: 10px; }
.guild-picker { position: relative; }
.guild-row {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--sel-bg); border: none; box-shadow: none; cursor: pointer;
}
.guild-avatar {
  width: 26px; height: 26px; border-radius: 50%; background: linear-gradient(140deg,var(--cyan),var(--coral));
  color: #fff; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; flex-shrink: 0;
  overflow: hidden;
}
.guild-avatar img { width: 100%; height: 100%; object-fit: cover; }
.guild-avatar.global-avatar {
  background-color: var(--tri-base);
  background-image:
    radial-gradient(circle at 15% 20%,var(--tri-navy) 0%,transparent 70%),
    radial-gradient(circle at 85% 20%,var(--tri-cyan) 0%,transparent 70%),
    radial-gradient(circle at 50% 90%,var(--tri-maroon) 0%,transparent 70%);
}
.guild-avatar.mine-avatar { background: linear-gradient(140deg,#a5eff4,#d77d50,#f13f24); }
/* "you can add ohShield here" — a dashed mint ring instead of the filled
   cyan/coral gradient, so it reads as a placeholder/opportunity rather than
   an active, already-protected guild. See renderAdminElsewhereShell. */
.guild-avatar.admin-only { background: var(--mint-16); border: 1.5px dashed var(--mint-deep); color: var(--mint-deep); }
.guild-row-text { flex: 1; min-width: 0; }
.guild-row-text .name { font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.guild-row-text .members { font: 9px 'IBM Plex Mono', monospace; color: var(--ink-40); letter-spacing: .06em; text-transform: uppercase; }
.guild-caret { font-size: 16px; font-weight: 700; line-height: 1; color: var(--ink-70); }
/* Alert indicator in the guild row (top bar) — matches the menu item indicator */
.guild-row .alert-indicator {
  width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
  position: relative;
  background: var(--ink-30); animation: none;
}
.guild-row .alert-indicator::after {
  content: ''; position: absolute; inset: -3px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,.35) 0%, transparent 60%);
  pointer-events: none;
}
.guild-row .alert-indicator.good { background: var(--cyan); box-shadow: 0 0 3px var(--cyan-35); }
.guild-row .alert-indicator.warn { background: var(--gold); box-shadow: 0 0 3px var(--gold-35); animation: heartbeat-pulse 4s ease-in-out infinite; }
.guild-menu {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0; background: var(--wash-strong);
  backdrop-filter: blur(20px); border: 1px solid var(--hairline); border-radius: var(--r-sm);
  box-shadow: 0 20px 50px -20px rgba(11,24,48,.18); padding: 6px; z-index: 40;
}
.guild-menu-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--r-sm); cursor: pointer; }
.guild-menu-item:hover { background: var(--ink-08); }
.guild-menu-item.selected { background: var(--ink-08); }
.guild-menu-item .avatar-sm { width: 22px; height: 22px; border-radius: 50%; background: var(--ink-08); color: var(--ink-55); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; flex-shrink: 0; overflow: hidden; }
.guild-menu-item.selected .avatar-sm { background: linear-gradient(140deg,var(--cyan),var(--coral)); color: #fff; }
.guild-menu-item .avatar-sm.global-avatar {
  background-color: var(--tri-base);
  background-image:
    radial-gradient(circle at 15% 20%,var(--tri-navy) 0%,transparent 70%),
    radial-gradient(circle at 85% 20%,var(--tri-cyan) 0%,transparent 70%),
    radial-gradient(circle at 50% 90%,var(--tri-maroon) 0%,transparent 70%);
}
.guild-menu-item .avatar-sm.mine-avatar { background: linear-gradient(140deg,#a5eff4,#d77d50,#f13f24); }
.guild-menu-item .gname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: var(--ink-70); }
.guild-menu-item.selected .gname { font-weight: 600; color: var(--ink); }
/* Your own access tier for that guild — owner/administrator are Discord-derived
   (gold/amber, echoing embedColors.js's own warm "important" ramp), manager is
   the app-granted edit tier (mint, distinct from both), viewer reuses the
   cyan/cyan-deep "healthy" pairing (teal bg, navy-blue text) seen elsewhere
   (.all-clear, .empty-state .check), and overview — no real grant, just
   Manage Server — gets the logo's own teal (tri-cyan, see .global-avatar)
   rather than viewer's generic cyan, so it still reads as distinct/lowest. */
.guild-menu-item .gtier { flex-shrink: 0; font: 9px 'IBM Plex Mono', monospace; letter-spacing: .06em; text-transform: uppercase; padding: 3px 7px; border-radius: 999px; }
.guild-menu-item .gtier.tier-owner { background: var(--gold-35); color: var(--gold); }
.guild-menu-item .gtier.tier-administrator { background: var(--amber-16); color: var(--amber); }
.guild-menu-item .gtier.tier-manager { background: var(--mint-16); color: var(--mint-deep); }
.guild-menu-item .gtier.tier-viewer { background: var(--cyan-16); color: var(--cyan-deep); }
.guild-menu-item .gtier.tier-overview { background: var(--tri-cyan-16); color: var(--tri-cyan); }
.guild-menu-divider { height: 1px; margin: 6px 4px; background: var(--hairline); }
/* Admin-elsewhere variant: dashed mint avatar (matches .guild-avatar.admin-only)
   and a mint tint instead of the generic grey .selected background, so the
   "you can add it here" list reads visually distinct from a real guild list.
   .gname gets flex/truncation here only — scoped to .admin-only rather than
   widening the base rule above, since real guild items never had this and
   don't need it (no trailing .badge-add sibling to make room for). */
.guild-menu-item .avatar-sm.admin-only { background: var(--mint-16); border: 1.5px dashed var(--mint-deep); color: var(--mint-deep); }
.guild-menu-item.admin-only.selected { background: var(--mint-16); }
.guild-menu-item.admin-only .gname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.guild-menu-note { font: 10px 'IBM Plex Mono', monospace; color: var(--ink-40); padding: 2px 8px 8px; line-height: 1.5; }
.badge-add { font: 9px 'IBM Plex Mono', monospace; letter-spacing: .04em; color: var(--mint-deep); background: var(--mint-16); border: 1px solid var(--mint-35); padding: 3px 7px; border-radius: 999px; flex-shrink: 0; }

/* Alert indicator — small dot left of the guild avatar, matching heartbeat pulse */
.guild-menu-item { position: relative; }
.guild-menu-item .gname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.guild-menu-item .gtier { flex-shrink: 0; }
.guild-menu-item .alert-indicator {
  width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; margin-right: 6px;
  position: relative;
  background: var(--ink-30); animation: none;
}
.guild-menu-item .alert-indicator::after {
  content: ''; position: absolute; inset: -3px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,.35) 0%, transparent 60%);
  pointer-events: none;
}
.guild-menu-item .alert-indicator.good { background: var(--cyan); box-shadow: 0 0 3px var(--cyan-35); }
.guild-menu-item .alert-indicator.warn { background: var(--gold); box-shadow: 0 0 3px var(--gold-35); animation: heartbeat-pulse 4s ease-in-out infinite; }

.nav-list { position: relative; display: flex; flex-direction: column; gap: 3px; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 10px; border-radius: var(--r-sm); cursor: pointer; }
.nav-item:not(.active):hover { background: var(--ink-08); }
.nav-item.active { background: var(--sel-bg); border: none; box-shadow: none; }
.nav-item .num { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); width: 16px; }
.nav-item.active .num { color: var(--coral); }
.nav-item .label { flex: 1; font-size: 14px; color: var(--ink-70); }
.nav-item.active .label { font-weight: 600; color: var(--ink); }
.nav-item .value { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); }
.nav-item .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--coral); }
/* Per-item lock for the admin-elsewhere shell (see renderNav's
   adminElsewhereMode) — everything but Overview, individually dimmed rather
   than the whole rail via .rail-locked. No cursor override and no hover/click
   feedback — the dimmed/grayscale look is the only signal. */
.nav-item.locked { opacity: .42; filter: grayscale(.6); }

/* Rail sections shown-but-inert for a user with no accessible/manageable
   guild — visible so the shell doesn't feel broken, but visually inert and
   non-interactive since there's no real guild/page data behind them. */
.rail-locked { opacity: .4; pointer-events: none; filter: grayscale(.7); }

.rail-hairline { height: 1px; background: var(--hairline); margin: 0 10px; }
.status-list { display: flex; flex-direction: column; gap: 10px; padding: 0 10px; font: 12px/1.3 'IBM Plex Mono', monospace; color: var(--ink-55); }
.status-row { display: flex; align-items: center; gap: 8px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.status-dot.cyan { background: var(--cyan); }
.status-dot.gold { background: var(--gold); }
.status-dot.coral { background: var(--coral); }
.status-dot.neutral { background: var(--ink-30); }

/* Heartbeat dot — one of the .status-dot rows (so it shares that font/layout).
   4s cycle: 3s held bright, a 0.5s fade out, then a 0.5s fade back in — a
   visible pulse rather than the earlier barely-there dip. A soft glow (using
   each color's existing translucent "-35" token) gives it presence at rest.
   Color reflects the current gateway ping (see dashboard.js's updateLatency):
   cyan is healthy, gold is elevated, coral is poor — reusing the same three
   tones the rest of the dashboard already uses for those meanings, rather
   than inventing new ones. Grey/no glow/no pulse means no sample yet, or the
   sample is stale. */
.heartbeat-dot { background: var(--ink-30); animation: none; }
.heartbeat-dot.good { background: var(--cyan); box-shadow: 0 0 4px var(--cyan-35); animation: heartbeat-pulse 4s ease-in-out infinite; }
.heartbeat-dot.warn { background: var(--gold); box-shadow: 0 0 4px var(--gold-35); animation: heartbeat-pulse 4s ease-in-out infinite; }
.heartbeat-dot.bad  { background: var(--coral); box-shadow: 0 0 4px var(--coral-35); animation: heartbeat-pulse 4s ease-in-out infinite; }
@keyframes heartbeat-pulse {
  0%, 75% { opacity: 1; }     /* held bright for 3s of the 4s cycle */
  87.5% { opacity: .22; }     /* faded out over the next 0.5s */
  100% { opacity: 1; }        /* faded back in over the final 0.5s */
}

/* ── Main content ─────────────────────────────────────────────────────────── */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 34px; }
.section-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
.section-eyebrow { font: 12px 'IBM Plex Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-55); }
.section-meta { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); }
.section-meta.clickable { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.section-meta.clickable:hover { color: var(--ink-70); }
.section-rule { height: 1px; background: var(--hairline); margin-bottom: 20px; }
.section-rule.short { width: calc(100% - 100px); }
.section-rule.shorter { width: calc(100% - 140px); }
.section-rule.right { width: calc(100% - 60px); margin-left: auto; margin-bottom: 24px; }

/* ── Overview: hero plate ─────────────────────────────────────────────────── */
.hero-plate { position: relative; height: 320px; border-radius: var(--r-a); overflow: hidden; background: var(--wash); box-shadow: var(--bloom-cyan); }
.hero-bg { position: absolute; inset: 0; overflow: hidden; }
.hero-bg.dusk { background: linear-gradient(140deg,#011841 0%,#013e73 42%,#24d6e5 78%,#dff6f9 100%); }
.hero-bg .orb1 { position: absolute; width: 260px; height: 260px; border-radius: 50%; left: 10%; top: -60px; background: radial-gradient(circle, rgba(255,255,255,.5), rgba(255,255,255,0) 70%); filter: blur(40px); }
.hero-bg.dusk .orb2 { position: absolute; width: 200px; height: 200px; border-radius: 50%; right: 18%; top: 20px; background: radial-gradient(circle, var(--coral), rgba(255,58,42,0) 70%); filter: blur(50px); opacity: .55; }
.hero-bg.dusk .orb3 { position: absolute; width: 320px; height: 320px; border-radius: 50%; right: -80px; bottom: -140px; background: radial-gradient(circle, var(--gold), rgba(255,140,42,0) 70%); filter: blur(60px); opacity: .5; }
.hero-bg .grid { position: absolute; inset: 0; background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.08) 0 1px, transparent 1px 64px), repeating-linear-gradient(0deg, rgba(255,255,255,.08) 0 1px, transparent 1px 64px); }
.hero-bg .scrim { position: absolute; inset: 0; background: linear-gradient(0deg, rgba(6,14,28,.8) 0%, rgba(6,14,28,.15) 55%, rgba(6,14,28,0) 78%); }
.hero-eyebrow { position: absolute; top: 18px; left: 24px; display: flex; align-items: center; gap: 9px; font: 11px 'IBM Plex Mono', monospace; letter-spacing: .12em; text-transform: uppercase; color: rgba(255,255,255,.8); }
/* login.html's decorative "live" indicator (see globalHero.js's opts.heartbeat) —
   ring-ping + core-pulse blink, matching the reference sign-in mockup. Distinct
   from .heartbeat-dot's fade animation, which is driven by a real latency sample. */
.hero-heartbeat { position: relative; display: inline-flex; width: 8px; height: 8px; flex-shrink: 0; }
.hero-heartbeat-ring { position: absolute; inset: -5px; border-radius: 50%; background: var(--cyan); opacity: .45; animation: heroHeartbeatRing 2.4s ease-in-out infinite; }
.hero-heartbeat-core { position: absolute; inset: 0; border-radius: 50%; background: var(--cyan); animation: heroHeartbeatCore 2.4s ease-in-out infinite; }
@keyframes heroHeartbeatCore { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .7; transform: scale(1.12); } }
@keyframes heroHeartbeatRing { 0% { opacity: .5; transform: scale(.7); } 70% { opacity: 0; transform: scale(2.1); } 100% { opacity: 0; transform: scale(2.1); } }
.hero-card {
  position: absolute; top: 16px; right: 16px; width: 284px; background: rgba(6,18,26,.34);
  backdrop-filter: blur(14px); border: 1px solid rgba(255,255,255,.14); border-radius: 14px; padding: 12px 16px 10px;
}
.hero-card-top { display: flex; justify-content: space-between; align-items: center; font: 9px 'IBM Plex Mono', monospace; letter-spacing: .06em; color: rgba(255,255,255,.5); text-transform: uppercase; margin-bottom: 9px; }
.hero-card-mid { display: flex; flex-direction: column; gap: 5px; margin-bottom: 7px; }
.hero-card-mid .chart-title { font: 10px 'IBM Plex Mono', monospace; letter-spacing: .06em; text-transform: uppercase; color: rgba(255,255,255,.75); white-space: nowrap; }
.chart-legend { display: flex; align-self: flex-end; gap: 9px; font: 9px 'IBM Plex Mono', monospace; color: rgba(255,255,255,.6); }
.chart-legend span { display: inline-flex; align-items: center; gap: 4px; }
.chart-legend i { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.chart-legend i.coral { background: var(--coral); }
.chart-legend i.cyan { background: var(--cyan); }
.hero-card svg { display: block; overflow: visible; }
.hero-card .day-labels { display: flex; justify-content: space-between; margin-top: 5px; }
.hero-card .day-labels span { font: 8px 'IBM Plex Mono', monospace; color: rgba(255,255,255,.4); }
.hero-bottom { position: absolute; left: 0; right: 0; bottom: 0; padding: 26px 30px 28px; display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.hero-greeting { font-size: 32px; font-weight: 500; letter-spacing: -0.01em; color: #fff; }
.hero-greeting .name { color: var(--gold); }
.hero-subcaption { margin-top: 8px; font: 12px 'IBM Plex Mono', monospace; color: rgba(255,255,255,.75); letter-spacing: .02em; }
/* Modifier only — .hero-subcaption alone stays plain text everywhere else
   (real Overview, GlobalHero) where it isn't a control. Used solely by
   renderAdminElsewhereMain's invite line, rendered as an <a>. */
.hero-subcaption.invite-link { display: inline-block; cursor: pointer; text-decoration: none; text-underline-offset: 3px; transition: color .12s ease; }
.hero-subcaption.invite-link:hover, .hero-subcaption.invite-link:focus-visible { color: #fff; text-decoration: underline; text-decoration-color: rgba(255,255,255,.5); }
.hero-subcaption.invite-link:focus-visible { outline: 2px solid #fff; outline-offset: 3px; border-radius: 2px; }

/* ── Overview: stat strip ─────────────────────────────────────────────────── */
.stat-strip { position: relative; margin-top: 26px; padding: 26px 0 34px; background: var(--wash); border-radius: var(--r-b); box-shadow: var(--bloom); display: grid; grid-template-columns: 1fr 1fr 1fr; }
.stat-cell { padding: 0 22px; display: flex; flex-direction: column; gap: 14px; }
.stat-cell.divider { border-left: 1px solid var(--hairline); }
.stat-cell-head { display: flex; justify-content: space-between; align-items: baseline; }
.stat-cell-head .label { font: 11px 'IBM Plex Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-40); }
.stat-cell-head .period { font: 10px 'IBM Plex Mono', monospace; color: var(--ink-25); }
.stat-value { font-size: 34px; font-weight: 500; letter-spacing: -.02em; }
.mini-bars-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; }
.stat-caption { font: 11px 'IBM Plex Mono', monospace; white-space: nowrap; }
.stat-caption.up { color: var(--cyan-deep); }
.stat-caption.neutral { color: var(--ink-40); }
.stat-caption.coral { color: var(--coral); }
.mini-bars { display: flex; align-items: flex-end; gap: 3px; height: 22px; }
.mini-bars .bar { width: 8px; border-radius: 2px; transition: height .3s ease; }

/* ── Overview: defense.status ─────────────────────────────────────────────── */
.two-col { display: grid; gap: 20px; align-items: start; }
.two-col.coverage { grid-template-columns: 1fr 1.28fr; }
.two-col.log-signal { grid-template-columns: 1fr 1fr; }
.two-col.imagescan { grid-template-columns: 1fr 1.4fr; }
.two-col.scamreports { grid-template-columns: 1fr 0.85fr; }
.two-col.automod { grid-template-columns: 0.85fr 1.15fr; }
.col { padding: 22px 4px 0; border-top: 1px solid var(--hairline); display: flex; flex-direction: column; gap: 20px; }
/* Dimmed placeholder variant — reuses .col/.coverage-row/.track/.config-row
   as-is (real dash values, 0-width tracks) rather than a parallel class set,
   so it stays visually/structurally in sync with the real defense.status
   columns automatically. See renderAdminElsewhereMain's ghost* helpers. */
.col.ghost { opacity: .58; }
.col-head { display: flex; justify-content: space-between; align-items: baseline; }
.col-head h2 { font-size: 19px; font-weight: 500; margin: 0; }
.col-head .meta { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-35); }
.coverage-row { display: flex; flex-direction: column; gap: 8px; }
.coverage-row-top { display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
.coverage-row-top .lbl { display: flex; align-items: center; gap: 8px; color: var(--ink-85); }
.coverage-row-top .lbl i { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.coverage-row-top .val { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-40); }
.track { height: 7px; border-radius: 4px; background: var(--ink-08); overflow: hidden; }
.track .fill { height: 100%; border-radius: 4px; transition: width .3s ease; }
.cta-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 16px;
  border-radius: var(--r-sm); border: 1px solid var(--coral-35); color: var(--coral); font-size: 13px; font-weight: 600; cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.cta-banner:hover { background: var(--coral); color: #fff; }
/* overview/viewer tier: same layout, but not a real control — dimmed, no
   hover fill, cursor signals "can't click this" instead of "click me". */
.cta-banner.locked { opacity: .55; cursor: not-allowed; }
.cta-banner.locked:hover { background: none; color: var(--coral); }
.all-clear { display: flex; align-items: center; gap: 10px; padding: 13px 16px; border-radius: var(--r-sm); background: var(--cyan-16); color: var(--cyan-deep); font-size: 13px; font-weight: 600; }

/* ── system.config rows ───────────────────────────────────────────────────── */
.config-list { display: flex; flex-direction: column; }
/* min-height (not padding alone) so every row — plain text, a channel
   <select>, or a wrapped pill list — settles at the same height instead of
   each control's own natural size dictating it row by row. */
.config-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; min-height: 44px; border-bottom: 1px solid var(--hairline); gap: 12px; }
.config-row .k { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-55); }
.config-row .v { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-85); text-align: right; }
.pill { font: 11px/18px 'IBM Plex Mono', monospace; color: var(--ink-70); background: var(--ink-08); padding: 4px 10px; border-radius: 999px; }
.pill.static { color: var(--ink-40); background: var(--ink-08); }
/* A pill with its own inline remove control (excluded roles / moderator
   roles) — the × sits inside the pill rather than as a separate element so
   removal stays visually tied to the exact chip it removes. */
.pill.removable { display: inline-flex; align-items: center; gap: 6px; padding-right: 6px; }
.pill-x { width: 14px; height: 14px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; line-height: 1; color: var(--ink-40); cursor: pointer; flex-shrink: 0; }
.pill-x:hover { background: var(--coral-deep); color: #fff; }
.segmented { display: flex; gap: 6px; }
.segmented .seg { padding: 5px 14px; border-radius: 999px; font: 11px 'IBM Plex Mono', monospace; }
.segmented .seg.on { background: var(--ink-85); color: var(--on-ink); }
.segmented .seg.off { background: var(--ink-08); color: var(--ink-40); }

/* ── Toggle switch (rule / automod-mode / verification-mode) ────────────────── */
/* Fast (.15s) slide+color transition — dashboard.js's wireToggle flips the
   .on class the instant it's clicked (before the write even resolves) so
   this actually plays instead of being skipped by the page's full re-render.
   The :active knob scale is a second, purely tactile micro-animation on top
   of that — quick and small enough to read as "barely visible" rather than
   a bounce. */
.toggle { width: 34px; height: 19px; border-radius: 999px; position: relative; cursor: pointer; flex-shrink: 0; transition: background .15s ease; background: var(--ink-08); }
.toggle .knob { position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; background: var(--wash-strong); box-shadow: 0 1px 3px rgba(11,24,48,.3); transition: left .15s ease, transform .1s ease; }
.toggle:active .knob { transform: scale(.86); }
.toggle.on { background: var(--cyan-deep); }
.toggle.on .knob { left: 17px; background: #fff; box-shadow: none; }
.toggle.big { width: 38px; height: 21px; }
.toggle.big .knob { width: 17px; height: 17px; }
.toggle.big.on .knob { left: 19px; }
.toggle.disabled { cursor: default; opacity: .55; }
.toggle.disabled:active .knob { transform: none; }

/* ── Logs page ────────────────────────────────────────────────────────────── */
.filter-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.chip { display: flex; align-items: center; gap: 7px; padding: 8px 14px; border-radius: 999px; font: 11px 'IBM Plex Mono', monospace; letter-spacing: .04em; cursor: pointer; background: var(--ink-08); color: var(--ink-55); }
.chip:hover { background: var(--ink-18); }
.chip.active { background: var(--ink); color: var(--on-ink); }
.chip .count { opacity: .6; }
.filter-note { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); margin-bottom: 22px; }
.log-list { position: relative; padding-top: 4px; border-top: 1px solid var(--hairline); }
.log-row { display: flex; align-items: center; gap: 16px; padding: 14px 26px; border-bottom: 1px solid var(--hairline); }
.log-row .time { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); width: 56px; flex-shrink: 0; }
.log-row .sev { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.log-row .sev.action { background: var(--coral); }
.log-row .sev.auto { background: var(--cyan); }
.log-row .sev.system { background: var(--ink-30); }
.log-row .sev.resolved { background: var(--cyan-deep); }
.log-row .log-thumb-wrap { flex-shrink: 0; }
.log-row .body { flex: 1; min-width: 0; }
.log-row .label { font: 13px 'IBM Plex Mono', monospace; color: var(--ink-85); }
.log-row .detail { font-size: 11px; color: var(--ink-40); margin-top: 3px; }
/* A later ban/ignore in response to this row's alert resolves the row in
   place (see resolveActivityByMessageId) — this is that outcome folded into
   the same row instead of a second, disconnected entry. */
.log-row .resolved-badge { color: var(--cyan-deep); }
.log-row .actor { font-size: 11px; color: var(--ink-40); flex-shrink: 0; }
.log-row .log-actions { display: flex; gap: 8px; flex-shrink: 0; }

/* ── Rules page ───────────────────────────────────────────────────────────── */
.rule-list { position: relative; padding-top: 4px; border-top: 1px solid var(--hairline); }
.rule-row { display: flex; align-items: center; gap: 18px; padding: 16px 26px; border-bottom: 1px solid var(--hairline); }
/* Two-level hierarchy: the master switch (level 1) reads as a distinct,
   elevated card via the tint + bigger .toggle.big; every level-2 row below
   it (categories, and the domain-feed/majestic/shortener rows further down
   the page) indents to read as nested under it. Visual grouping only — the
   domain feed/shortener/majestic rows are independent detections gated by
   the master switch, not by the "Obfuscated link" category next to them. */
.rule-row.master { background: var(--ink-08); border-radius: var(--r-sm); flex-direction: column; align-items: stretch; gap: 10px; }
.rule-row.master .name { font-size: 15px; font-weight: 700; }
.master-top, .master-penalty-row { display: flex; align-items: center; gap: 18px; }
.master-penalty-spacer { width: 38px; flex-shrink: 0; }
.master-penalty-inner { display: flex; align-items: center; gap: 10px; }
.use-global-label { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-55); }
.rule-row.sub { padding-left: 46px; }
.rule-row .body { flex: 1; min-width: 0; }
.rule-row .name { font-size: 14px; font-weight: 600; color: var(--ink-85); }
.rule-row .body .desc { font-size: 12px; color: var(--ink-55); line-height: 1.4; margin-top: 3px; }
.rule-row .cat { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.rule-row .cat span { display: flex; align-items: center; gap: 5px; font: 10px 'IBM Plex Mono', monospace; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-40); }
.rule-row .cat i { width: 6px; height: 6px; border-radius: 50%; }
.rule-row .cat i.global { background: var(--cyan); }
.rule-row .matches { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-35); width: 90px; flex-shrink: 0; text-align: right; }
.rule-row .severity { width: 76px; flex-shrink: 0; text-align: right; font: 10px 'IBM Plex Mono', monospace; letter-spacing: .06em; text-transform: uppercase; }
.rule-row .severity.high { color: var(--coral); }
.rule-row .severity.medium { color: var(--gold); }
.rule-row .severity.low { color: var(--ink-35); }

/* ── Rules page: configurable penalty (per category + master reset) ─────────
   Square tick/blank check, distinct from the pill .toggle above: per-row it's
   always clickable — a plain on/off for whether this category takes its
   configured action at all, or is alert-only (see .pen-offnote). The
   master's is the same visual plus a third "mixed" mark, reflecting whether
   every row's duration currently matches the global default (tick), none do
   (blank), or it's split (mixed) — the per-row Reset button below is the
   row-level equivalent of this control's bulk "reset every override" action. */
.row-check, .tri-check { width: 17px; height: 17px; border-radius: 4px; border: 1px solid var(--ink-30); background: var(--wash-strong); flex-shrink: 0; display: flex; align-items: center; justify-content: center; position: relative; cursor: pointer; }
.row-check.on, .tri-check.on { background: var(--cyan-deep); border-color: var(--cyan-deep); }
.row-check.on::after, .tri-check.on::after { content: ''; width: 8px; height: 5px; border-left: 2px solid #fff; border-bottom: 2px solid #fff; transform: rotate(-45deg) translate(1px,-1px); }
.tri-check.mixed { background: var(--ink-30); border-color: var(--ink-30); }
.tri-check.mixed::after { content: ''; width: 8px; height: 2px; background: #fff; }
.row-check.pen-disabled, .tri-check.pen-disabled, .pen-click.pen-disabled, .pen-reset.pen-disabled { cursor: default; opacity: .5; pointer-events: none; }

.penalty-block { width: 100%; margin-top: 8px; padding: 10px 12px; background: var(--wash); border-radius: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pen-lead { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-85); }
.pen-click { font: 12px 'IBM Plex Mono', monospace; color: var(--cyan-deep); cursor: pointer; border-bottom: 1px dashed var(--cyan-deep); position: relative; display: inline-block; }
.pen-click:hover { color: var(--ink); }
.pen-reset { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-55); background: var(--ink-08); padding: 4px 11px; border-radius: 999px; cursor: pointer; }
.pen-reset:hover { background: var(--ink-18); color: var(--ink); }
.pen-offnote { font: 11px 'IBM Plex Mono', monospace; color: var(--coral); }

/* Compact popover anchored directly under the clicked value/unit text —
   a scrollable preset list plus a manual keyboard-entry input (value
   popovers only; unit popovers just list minutes/hours). */
.pen-popover { display: none; position: absolute; top: calc(100% + 6px); left: 0; z-index: 20; min-width: 132px; background: var(--wash-strong); backdrop-filter: blur(20px); border: 1px solid var(--hairline); border-radius: var(--r-sm); box-shadow: 0 20px 50px -20px rgba(11,24,48,.25); padding: 8px; cursor: default; }
.pen-popover.open { display: block; }
.pen-popover input.pen-manual { width: 100%; font: 12px 'IBM Plex Mono', monospace; background: var(--ink-08); border: 1px solid transparent; border-radius: 8px; padding: 6px 8px; color: var(--ink-85); margin-bottom: 6px; box-sizing: border-box; }
.pen-popover input.pen-manual:focus-visible { outline: none; border-color: var(--cyan-deep); }
.pen-popover .pen-list { max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; }
.pen-popover .pen-opt { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-70); padding: 6px 8px; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.pen-popover .pen-opt:hover { background: var(--ink-08); color: var(--ink); }

/* ── Image Scan page ──────────────────────────────────────────────────────── */
.kv-list { display: flex; flex-direction: column; padding-top: 6px; border-top: 1px solid var(--hairline); }
.kv-row { display: flex; justify-content: space-between; font-size: 13px; padding: 10px 0; border-top: 1px solid var(--hairline); }
.kv-row:first-child { border-top: none; }
.kv-row .k { color: var(--ink-55); }
.kv-row .v { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-85); }
.role-pills { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
/* Administrators section on the Team page: each Administrator-permission
   role as its own group, with the members holding that role listed
   underneath — left-aligned (unlike the flex-end .role-pills used
   elsewhere) since it reads top-down as a role → members breakdown. */
.admin-role-groups { display: flex; flex-direction: column; gap: 16px; }
.admin-role-group .role-pills { justify-content: flex-start; }
.admin-role-members { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.admin-role-empty { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); margin-top: 8px; }
.scan-list-row { display: flex; align-items: center; gap: 16px; padding: 13px 4px; border-bottom: 1px solid var(--hairline); }
.scan-thumb { width: 44px; height: 44px; border-radius: 8px; flex-shrink: 0; background-image: repeating-linear-gradient(45deg, var(--ink-08) 0 2px, transparent 2px 8px); display: flex; align-items: center; justify-content: center; font: 8px 'IBM Plex Mono', monospace; color: var(--ink-30); text-transform: uppercase; }
/* Real thumbnails (Discord-hosted, reposted evidence — see storeImages() in
   imageScanner.js) instead of the placeholder above, on a Logs/Image Drop
   row that actually has stored images. Click opens every image from that
   entry — see openImagePreview. */
.scan-thumb.has-images { width: auto; background-image: none; justify-content: flex-start; gap: 3px; cursor: pointer; }
.scan-thumb.has-images img { width: 32px; height: 32px; border-radius: 6px; object-fit: cover; flex-shrink: 0; }
.scan-thumb .thumb-overflow { font: 10px 'IBM Plex Mono', monospace; color: var(--ink-40); padding-left: 2px; }
/* Swapped in for an <img> whose Discord CDN link has expired or been deleted
   (see wireThumbFallbacks) — same hatch treatment as the no-images .scan-thumb
   above, just sized per context instead of a fixed 44px box. */
.thumb-broken { background-image: repeating-linear-gradient(45deg, var(--ink-08) 0 2px, transparent 2px 8px); display: flex; align-items: center; justify-content: center; text-align: center; font: 7px 'IBM Plex Mono', monospace; color: var(--ink-30); text-transform: uppercase; line-height: 1.3; }
.scan-thumb.has-images .thumb-broken { width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; padding: 2px; }
.image-preview-grid .thumb-broken { width: 100%; aspect-ratio: 1; border-radius: var(--r-sm); padding: 8px; font-size: 10px; }
.scan-list-row .body { flex: 1; min-width: 0; }
.scan-list-row .chan { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-85); }
.scan-list-row .action { font-size: 11px; color: var(--ink-40); margin-top: 3px; }
.scan-list-row .time { font-size: 11px; color: var(--ink-30); width: 56px; text-align: right; flex-shrink: 0; }

/* ── Scam Reports page ────────────────────────────────────────────────────── */
.stat-row-4 { position: relative; padding: 22px 0 6px; border-top: 1px solid var(--hairline); display: grid; grid-template-columns: repeat(4,1fr); margin-bottom: 28px; }
.stat-row-4 .cell { padding: 0 22px; display: flex; flex-direction: column; gap: 10px; }
.stat-row-4 .cell.divider { border-left: 1px solid var(--hairline); }
.stat-row-4 .cell .label { font: 11px 'IBM Plex Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-40); }
.stat-row-4 .cell .value { font-size: 30px; font-weight: 500; }
.stat-row-4 .cell .value.coral { color: var(--coral); }
.queue-list { position: relative; padding: 4px 4px 14px; border-top: 1px solid var(--hairline); }
.queue-row { padding: 14px 0; border-bottom: 1px solid var(--hairline); display: flex; flex-direction: column; gap: 8px; }
.queue-row-top { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.queue-row-top .domain { font: 13px 'IBM Plex Mono', monospace; color: var(--ink-85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-row-top .time { font: 10px 'IBM Plex Mono', monospace; color: var(--ink-35); flex-shrink: 0; }
.queue-row .meta { font-size: 11px; color: var(--ink-40); }
.mark-reviewed { align-self: flex-start; margin-top: 2px; padding: 6px 14px; border-radius: 999px; background: var(--ink-08); font-size: 11px; font-weight: 600; color: var(--ink-70); cursor: pointer; }
.mark-reviewed:hover { background: var(--cyan-deep); color: #fff; }
.mark-reviewed.disabled { cursor: default; color: var(--ink-30); }
.mark-reviewed.disabled:hover { background: var(--ink-08); color: var(--ink-30); }
/* Shared sizing for "submit" buttons paired with an add-row input/select
   (grant access, flag domain, ...) — .mark-reviewed alone hugs its own label
   width, which reads as inconsistent when several sit in different rows
   across the dashboard; a fixed min-width plus centering makes them read as
   one consistent button, not one sized per label. */
.add-row-submit { min-width: 128px; text-align: center; }
.resolved-row { padding: 12px 0; border-bottom: 1px solid var(--hairline); display: flex; flex-direction: column; gap: 5px; }
.resolved-row-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.resolved-row-top .domain { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.resolved-row-top .action { font-size: 11px; color: var(--cyan-deep); flex-shrink: 0; }
.resolved-row .meta { font-size: 11px; color: var(--ink-40); }
.empty-state { padding: 50px 10px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.empty-state .check { width: 48px; height: 48px; border-radius: 50%; background: var(--cyan-16); color: var(--cyan-deep); display: flex; align-items: center; justify-content: center; font-size: 18px; }
.empty-state.small .check { width: 40px; height: 40px; font-size: 16px; }
.empty-state .title { font-size: 16px; font-weight: 600; }
.empty-state.small .title { font-size: 13px; color: var(--ink-70); }
.empty-state .sub { font-size: 13px; color: var(--ink-55); }
.empty-state.small .sub { font-size: 12px; color: var(--ink-40); }

/* ── Automod page ─────────────────────────────────────────────────────────── */
.automod-card { padding: 22px 24px; background: var(--wash); border-radius: var(--r-b); box-shadow: var(--bloom); display: flex; flex-direction: column; gap: 14px; }
.automod-card .head { display: flex; justify-content: space-between; align-items: center; }
.automod-card .head h2 { font-size: 17px; font-weight: 500; margin: 0; }
.automod-stat { font-size: 40px; font-weight: 500; color: var(--coral); letter-spacing: -.02em; }
.automod-card .desc { font-size: 13px; color: var(--ink-55); line-height: 1.4; }
.week-bars { display: flex; align-items: flex-end; gap: 4px; height: 36px; padding-top: 8px; border-top: 1px solid var(--hairline); }
.week-bars .col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 2px; height: 100%; padding: 0; border: none; }
.week-bars .col .seg { width: 100%; background: var(--coral); border-radius: 2px; }
.legend-row { display: flex; gap: 14px; font: 10px 'IBM Plex Mono', monospace; color: var(--ink-40); }
.legend-row span { display: flex; align-items: center; gap: 5px; }
.legend-row i { width: 7px; height: 7px; border-radius: 2px; display: inline-block; }
.relay-row { display: flex; align-items: center; gap: 16px; padding: 14px 4px; border-bottom: 1px solid var(--hairline); }
.relay-row .body { flex: 1; min-width: 0; }
.relay-row .rule { font: 13px 'IBM Plex Mono', monospace; color: var(--ink-85); }
.relay-row .meta { font-size: 11px; color: var(--ink-40); margin-top: 3px; }
.relay-row .time { font-size: 11px; color: var(--ink-30); flex-shrink: 0; }
/* ── Custom select (role pickers) ─────────────────────────────────────────── */
/* Native <select> chrome (the closed-state box) is fully re-skinned to match
   pill/chip components elsewhere; the open dropdown list itself is still
   OS-rendered — no cross-browser way around that without a JS-built listbox,
   which role pickers (small, flat, no grouping/search need) don't warrant.
   Channel pickers do need that — see .chsel below. */
.channel-picker { display: inline-flex; align-items: center; gap: 8px; }
.select-field { position: relative; display: inline-flex; align-items: center; }
.select-field::after {
  content: ''; position: absolute; right: 13px; top: 50%; width: 9px; height: 9px;
  transform: translateY(-50%); pointer-events: none; background: var(--ink-55);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.5 3.2l3.5 3.6 3.5-3.6' stroke='%23000' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.5 3.2l3.5 3.6 3.5-3.6' stroke='%23000' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
}
.select-field select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  font: 12px 'IBM Plex Mono', monospace; letter-spacing: .02em;
  background: var(--ink-08); color: var(--ink-85);
  border: 1px solid transparent; border-radius: 999px;
  padding: 8px 32px 8px 14px; min-width: 168px; max-width: 100%;
  cursor: pointer; transition: background .15s ease, border-color .15s ease;
}
.select-field select:hover { background: var(--ink-18); }
.select-field select:focus-visible { outline: none; border-color: var(--cyan-deep); background: var(--wash-strong); }
.select-field select:disabled { opacity: .5; cursor: not-allowed; }
.select-field select:disabled:hover { background: var(--ink-08); }
.select-field.compact select { min-width: 0; }

/* ── Role picker (searchable, flat list) ────────────────────────────────────── */
.rsel { position: relative; display: inline-flex; align-items: center; }
.rsel-trigger {
  appearance: none; font: 12px/20px 'IBM Plex Mono', monospace; letter-spacing: .02em;
  background: var(--ink-08); color: var(--ink-85);
  border: 1px solid transparent; border-radius: 999px;
  padding: 8px 32px 8px 14px; min-width: 168px; max-width: 100%;
  text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  cursor: pointer; position: relative; transition: background .15s ease, border-color .15s ease;
}
.rsel-trigger::after {
  content: ''; position: absolute; right: 13px; top: 50%; width: 9px; height: 9px;
  transform: translateY(-50%); pointer-events: none; background: var(--ink-55); transition: transform .15s ease;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.5 3.2l3.5 3.6 3.5-3.6' stroke='%23000' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.5 3.2l3.5 3.6 3.5-3.6' stroke='%23000' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
}
.rsel-trigger:hover { background: var(--ink-18); }
.rsel.open .rsel-trigger { border-color: var(--cyan-deep); background: var(--wash-strong); }
.rsel.open .rsel-trigger::after { transform: translateY(-50%) rotate(180deg); }
.rsel.compact .rsel-trigger { min-width: 0; }
.rsel.disabled .rsel-trigger { opacity: .5; cursor: not-allowed; }

.rsel-panel {
  display: none; position: absolute; left: 0; z-index: 30;
  top: calc(100% + 6px); bottom: auto;
  width: 240px; max-width: max(240px, 60vw);
  background: var(--wash-strong); backdrop-filter: blur(20px); border: 1px solid var(--hairline);
  border-radius: var(--r-sm); box-shadow: 0 20px 50px -20px rgba(11,24,48,.25); padding: 8px; cursor: default;
}
.rsel-panel.flip-up { bottom: calc(100% + 6px); top: auto; }
.rsel-panel.flip-down { top: calc(100% + 6px); bottom: auto; }
.rsel.open .rsel-panel { display: block; }
.rsel-search {
  width: 100%; font: 12px/20px 'IBM Plex Mono', monospace; background: var(--ink-08); border: 1px solid transparent;
  border-radius: 8px; padding: 7px 10px; color: var(--ink-85); margin-bottom: 6px; box-sizing: border-box;
}
.rsel-search:focus-visible { outline: none; border-color: var(--cyan-deep); }
.rsel-list { max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; transform: translateZ(0); will-change: transform; }
.rsel-opt { font: 12px/1.3 'IBM Plex Mono', monospace; color: var(--ink-70); padding: 6px 8px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; min-height: 28px; }
.rsel-opt:hover, .rsel-opt.hl { background: var(--ink-08); color: var(--ink); }
.rsel-opt.active { color: var(--cyan-deep); }
.rsel-opt.rsel-hidden { display: none; }
.rsel-empty { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); padding: 10px 8px; text-align: center; }

/* ── Channel picker (searchable, category-grouped) ────────────────────────── */
/* A JS-built listbox (see channelPickerHtml/wireChannelPicker in
   dashboard.js) instead of a native <select> — it needs to group channels
   under category headers and filter live as you type (matching Discord's
   own onboarding channel-select), neither of which a native <select> can do
   without OS-inconsistent chrome. Same closed-state pill look as
   .select-field select, and the same floating-panel treatment as
   .pen-popover, so it reads as one family with both. */
.chsel { position: relative; display: inline-flex; align-items: center; }
.chsel-trigger {
  appearance: none; font: 12px/20px 'IBM Plex Mono', monospace; letter-spacing: .02em;
  background: var(--ink-08); color: var(--ink-85);
  border: 1px solid transparent; border-radius: 999px;
  padding: 8px 32px 8px 14px; min-width: 168px; max-width: 100%;
  text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  cursor: pointer; position: relative; transition: background .15s ease, border-color .15s ease;
}
.chsel-trigger::after {
  content: ''; position: absolute; right: 13px; top: 50%; width: 9px; height: 9px;
  transform: translateY(-50%); pointer-events: none; background: var(--ink-55); transition: transform .15s ease;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.5 3.2l3.5 3.6 3.5-3.6' stroke='%23000' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.5 3.2l3.5 3.6 3.5-3.6' stroke='%23000' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
}
.chsel-trigger:hover { background: var(--ink-18); }
.chsel.open .chsel-trigger { border-color: var(--cyan-deep); background: var(--wash-strong); }
.chsel.open .chsel-trigger::after { transform: translateY(-50%) rotate(180deg); }
.chsel.compact .chsel-trigger { min-width: 0; }
.chsel.disabled .chsel-trigger { opacity: .5; cursor: not-allowed; }

.chsel-panel {
  display: none; position: absolute; left: 0; z-index: 30;
  top: calc(100% + 6px); bottom: auto;
  width: 240px; max-width: max(240px, 60vw);
  background: var(--wash-strong); backdrop-filter: blur(20px); border: 1px solid var(--hairline);
  border-radius: var(--r-sm); box-shadow: 0 20px 50px -20px rgba(11,24,48,.25); padding: 8px; cursor: default;
}
.chsel-panel.flip-up { bottom: calc(100% + 6px); top: auto; }
.chsel-panel.flip-down { top: calc(100% + 6px); bottom: auto; }
.chsel.open .chsel-panel { display: block; }
.chsel-search {
  width: 100%; font: 12px/20px 'IBM Plex Mono', monospace; background: var(--ink-08); border: 1px solid transparent;
  border-radius: 8px; padding: 7px 10px; color: var(--ink-85); margin-bottom: 6px; box-sizing: border-box;
}
.chsel-search:focus-visible { outline: none; border-color: var(--cyan-deep); }
/* transform+will-change promote this scrolling list to its own compositing
   layer — without it, Chrome (on some GPU/driver combos) fails to repaint
   correctly on scroll when a scrollable element sits inside a
   backdrop-filter ancestor (.chsel-panel above), leaving stale/torn pixels
   that read as garbled channel names. Isolating the layer sidesteps that
   repaint bug; the visible frosted-glass look is unchanged. */
.chsel-list { max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; transform: translateZ(0); will-change: transform; }
.chsel-group-label { font: 10px/18px 'IBM Plex Mono', monospace; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-35); padding: 8px 8px 3px; cursor: pointer; user-select: none; }
.chsel-group-label:first-child { padding-top: 2px; }
.chsel-group-label:hover { color: var(--ink-70); }
.chsel-group-label[data-collapsed="true"] { opacity: 0.6; }
.chsel-group-label[data-collapsed="true"]::before { content: '▸ '; font-size: 8px; margin-right: 4px; transition: transform 0.15s ease; }
.chsel-group-label[data-collapsed="false"]::before { content: '▾ '; font-size: 8px; margin-right: 4px; transition: transform 0.15s ease; }
.chsel-opt { font: 12px/1.3 'IBM Plex Mono', monospace; color: var(--ink-70); padding: 6px 8px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; min-height: 28px; }
.chsel-opt:hover, .chsel-opt.hl { background: var(--ink-08); color: var(--ink); }
.chsel-opt.active { color: var(--cyan-deep); }
.chsel-opt.chsel-hidden { display: none; }
.chsel-opt.chsel-collapsed { display: none; }
.chsel-empty { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-30); padding: 10px 8px; text-align: center; }

.text-field {
  font: 12px 'IBM Plex Mono', monospace; letter-spacing: .02em;
  background: var(--ink-08); color: var(--ink-85);
  border: 1px solid transparent; border-radius: 999px;
  padding: 8px 14px; min-width: 220px; max-width: 100%;
  transition: background .15s ease, border-color .15s ease;
}
.text-field::placeholder { color: var(--ink-35); }
.text-field:hover { background: var(--ink-18); }
.text-field:focus-visible { outline: none; border-color: var(--cyan-deep); background: var(--wash-strong); }
/* Compact numeric inputs (threshold / timeout minutes / max attempts / etc.) —
   the base .text-field's 220px min-width is sized for text like a domain or
   a Discord id, way too wide for a 2-4 digit number sitting inline with a
   unit label. */
.text-field.narrow { min-width: 0; width: 64px; text-align: right; padding: 8px 10px; }
/* A row of inline editable controls inside a .kv-row's .v (right-aligned) —
   number input(s) plus a short unit label, e.g. "[4] images / 10min". */
.inline-edit { display: inline-flex; align-items: center; gap: 8px; }
.inline-edit .unit { font: 11px 'IBM Plex Mono', monospace; color: var(--ink-40); white-space: nowrap; }

/* ── Verification page ────────────────────────────────────────────────────── */
.mode-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-top: 1px solid var(--hairline); }
.mode-row:first-child { border-top: none; }
.mode-row .name { font-size: 13px; color: var(--ink-85); }
.verif-row { display: flex; align-items: center; gap: 16px; padding: 13px 4px; border-bottom: 1px solid var(--hairline); }
.verif-row .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cyan); flex-shrink: 0; }
.verif-row .body { flex: 1; min-width: 0; }
.verif-row .user { font: 13px 'IBM Plex Mono', monospace; color: var(--ink-85); }
.verif-row .mode { font-size: 11px; color: var(--ink-40); margin-top: 3px; }
.verif-row .time { font-size: 11px; color: var(--ink-30); width: 56px; text-align: right; flex-shrink: 0; }

/* ── Review modal ─────────────────────────────────────────────────────────── */
.modal-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(11,24,48,.4); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; padding: 24px; }
.modal-box { width: 100%; max-width: 560px; max-height: 80vh; overflow: auto; background: var(--wash-strong); backdrop-filter: blur(24px); border: 1px solid var(--hairline); border-radius: var(--r-lg); padding: 28px; box-shadow: 0 30px 90px -20px rgba(11,24,48,.22), var(--bloom); }
.modal-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 18px; }
.modal-head h2 { font-size: 20px; font-weight: 500; margin: 0; }
.modal-head .sub { font-size: 12px; color: var(--ink-40); margin-top: 4px; }
.modal-close { width: 30px; height: 30px; border-radius: 50%; background: var(--ink-08); display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 14px; color: var(--ink-55); flex-shrink: 0; }
.modal-close:hover { background: var(--ink-18); }
/* openImagePreview's grid — every image from one flagged entry, each also a
   link to the full-size Discord-hosted original. */
.image-preview-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.image-preview-grid img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--r-sm); display: block; }
[hidden] { display: none !important; }

/* ── Team page ─────────────────────────────────────────────────────────────── */
.team-add-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 12px 0; }
.team-add-error { font-size: 11px; color: var(--coral); min-height: 14px; }
.mark-reviewed.remove:hover { background: var(--coral-deep); color: #fff; }

/* ── Network overview (global stats) ──────────────────────────────────────── */
/* Full hero variant reuses .hero-plate/.hero-card as-is (see globalHero.js);
   this just gives it a sane max-width when centered standalone on the login
   page instead of stretching full-bleed inside .main. */
.login-wrap .global-hero-wrap { width: 100%; max-width: 640px; }
.login-wrap .global-hero-wrap:empty { display: none; }

/* ── Loading / misc ───────────────────────────────────────────────────────── */
.loading-line { font: 12px 'IBM Plex Mono', monospace; color: var(--ink-40); padding: 40px 0; text-align: center; }

@media (max-width: 980px) {
  .shell { flex-direction: column; }
  .rail { width: 100%; position: static; }
  .two-col.coverage, .two-col.log-signal, .two-col.imagescan, .two-col.scamreports, .two-col.automod, .two-col.team { grid-template-columns: 1fr; }
  .stat-strip { grid-template-columns: 1fr; }
  .stat-row-4 { grid-template-columns: 1fr 1fr; }
}
