/* Shared by the Manual board (dashboard.html) and the Agent board
   (agent.html) - the mode picker and the permanent two-pane shell
   (setup on one side, live results on the other) both boards use. */

/* --- Persistent left sidebar (see #5, #6) -----------------------------
   Replaces the old picker-screen + top #mode-switcher strip: a
   usersniper-style logo-topped rail is the dashboard's primary nav now,
   with OVERVIEW/TOOLS (the cross-platform Manual/Agent/Sniper concepts -
   Dictionary merged into Manual as an expandable GENERATE/WORD LIST
   sub-menu, see #6) and a PLATFORMS group (one entry per platform, real
   icon weight) sitting side by side rather than the picker being the
   only way in.

   .dashboard-app is the single unified "app shell" card - sidebar and
   content are two sections of ONE bordered panel, not a sidebar next to
   a separately-floating content block (a real, reported problem: with
   each piece as its own card, the whole thing read as scattered boxes
   rather than one tool). The card itself is centered on the page with
   room around it, not pinned to the browser's physical edges -
   max-width + margin:auto centers it horizontally, the vertical margin
   gives it breathing room below the fixed .nav + .verified-strip above.
   overflow:hidden clips the sidebar's flat rectangular corners to the
   card's own border-radius rather than needing the sidebar to carry its
   own separate rounding. */
.dashboard-app {
  display: flex; align-items: stretch;
  width: calc(100% - 32px); max-width: 1360px; margin: 28px auto;
  min-height: calc(100vh - 124px - 56px);
  border: 1px solid var(--console-line);
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 32px 70px -40px oklch(0 0 0 / 0.35);
  background: var(--panel);
}
.dashboard-main { flex: 1; min-width: 0; display: flex; }
.dashboard-main > * { flex: 1; min-width: 0; }

.dashboard-sidebar {
  flex: none; width: 208px; align-self: stretch;
  position: sticky; top: 28px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
  padding: 18px 12px 20px;
  background: oklch(0.13 0.009 258);
  border-right: 1px solid var(--console-line);
}
.sidebar-logo {
  display: flex; align-items: center; padding: 6px 8px 16px; margin-bottom: 6px;
  border-bottom: 1px solid var(--console-line);
}
.sidebar-logo .brand-name { font-family: var(--serif); font-size: 19px; letter-spacing: 0.01em; color: var(--silver); }
.sidebar-logo .brand-cursor { color: var(--accent); }
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }

/* One group per section (GLOBAL, then one per platform - see #7's
   per-platform nav, each carrying its own Dash + Sniper entry). The
   divider between groups is what keeps a ~20-entry rail readable as
   sections rather than one long undifferentiated list. */
.sidebar-group { display: flex; flex-direction: column; gap: 2px; padding-bottom: 10px; }
.sidebar-group + .sidebar-group { border-top: 1px solid oklch(1 0 0 / 0.07); padding-top: 4px; }
.sidebar-group-label {
  font-family: var(--sans); font-size: 9.5px; letter-spacing: 0.13em;
  /* Each platform group sets its own --platform-color inline (that
     platform's real brand color) so the rail reads at a glance -
     GLOBAL and anything else without one falls back to the neutral. */
  color: var(--platform-color, var(--silver-faint));
  padding: 14px 10px 6px;
}

/* Filters the platform groups by name - the rail is long enough now
   (7 platforms x 2 entries) that scanning it by eye is slower than
   typing. GLOBAL is deliberately never filtered out; it's the part you
   always want reachable. */
.sidebar-search {
  width: 100%; margin: 4px 0 10px;
  padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--console-line); background: oklch(1 0 0 / 0.04);
  font-family: var(--sans); font-size: 11px; color: var(--silver);
}
.sidebar-search::placeholder { color: var(--silver-faint); }
.sidebar-search:focus { outline: none; border-color: var(--accent); }
.sidebar-empty { font-family: var(--sans); font-size: 10.5px; color: var(--silver-faint); padding: 8px 10px; }

.sidebar-footer {
  display: flex; align-items: baseline; gap: 8px;
  margin-top: 14px; padding: 12px 10px 0;
  border-top: 1px solid oklch(1 0 0 / 0.07);
}
.sidebar-footer-name { font-family: var(--serif); font-size: 13px; color: var(--silver-dim); }
.sidebar-footer-name .brand-cursor { color: var(--accent); }
.sidebar-footer-tag {
  font-family: var(--sans); font-size: 8.5px; letter-spacing: 0.12em; color: var(--silver-faint);
  padding: 2px 6px; border: 1px solid var(--console-line); border-radius: 4px;
}
.sidebar-item {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 9px 10px; border-radius: 8px; border: none; background: none; cursor: pointer;
  font-family: var(--sans); font-size: 11.5px; letter-spacing: 0.04em; color: var(--silver-dim);
  transition: background-color 0.12s ease, color 0.12s ease;
}
.sidebar-item { text-decoration: none; }
.sidebar-item:hover { color: var(--silver); background: oklch(1 0 0 / 0.05); }
/* The active entry fills with its own group's platform color where
   there is one (so DISCORD's active row reads blurple, INSTAGRAM's
   pink, and so on), falling back to the app accent for GLOBAL. */
.sidebar-item.active { color: oklch(0.99 0.003 254); background: var(--platform-color, var(--accent)); }
.sidebar-item-icon { display: flex; flex: none; opacity: 0.9; }
.sidebar-item.active .sidebar-item-icon { opacity: 1; }
.sidebar-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- Expandable sidebar item (MANUAL's GENERATE/WORD LIST sub-modes,
   see #6) - Dictionary merged into Manual, so picking which one you
   want happens right here in the nav instead of a separate destination
   or an in-form toggle floating below the header. */
.sidebar-item-caret { flex: none; opacity: 0.6; transition: transform 0.15s ease; }
.sidebar-item-expandable[aria-expanded="true"] .sidebar-item-caret { transform: rotate(180deg); }
.sidebar-submenu { display: flex; flex-direction: column; gap: 1px; padding: 2px 0 4px 30px; }
.sidebar-subitem {
  display: block; width: 100%; text-align: left;
  padding: 7px 10px; border-radius: 7px; border: none; background: none; cursor: pointer;
  font-family: var(--sans); font-size: 10.5px; letter-spacing: 0.04em; color: var(--silver-faint);
  transition: background-color 0.12s ease, color 0.12s ease;
}
.sidebar-subitem:hover { color: var(--silver); background: oklch(1 0 0 / 0.05); }
.sidebar-subitem.active { color: var(--accent); background: var(--accent-glow); }
.sidebar-submenu-hint {
  font-family: var(--sans); font-size: 9.5px; letter-spacing: 0.03em; color: var(--silver-faint);
  padding: 4px 10px 2px;
}

@media (max-width: 880px) {
  /* Stacked, not side-by-side - the card's own border/radius/shadow
     (set on .dashboard-app itself, unchanged here) still wraps both
     sections as one piece; the sidebar no longer needs its own
     separate rounding/margin the way it did before both were merged
     into one shared card. */
  .dashboard-app { display: block; width: calc(100% - 24px); min-height: 0; }
  .dashboard-sidebar {
    position: static; width: auto; max-height: none; overflow: visible;
    flex-direction: row; flex-wrap: wrap; gap: 4px;
    border-right: none; border-bottom: 1px solid var(--console-line);
  }
  .sidebar-logo { display: none; }
  .sidebar-group-label { padding: 8px 8px 4px; width: 100%; }
  .sidebar-item { width: auto; }
  .sidebar-item-label { white-space: nowrap; }
}

/* --- Per-platform quick view (see #5) ---------------------------------
   One shared pane re-rendered per PLATFORMS sidebar entry -
   js/platform-board.js owns its content; this is just its layout. */
.platform-board-inner { max-width: 760px; }
.platform-board-title-row { display: flex; align-items: center; gap: 14px; }
.platform-board-icon {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: 44px; height: 44px; border-radius: 12px;
  background: var(--accent-glow); color: var(--accent);
  border: 1px solid var(--accent-faint);
}
.platform-board-stats {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;
  padding: 20px; margin: 4px 0 14px;
  background: oklch(0.15 0.008 258); border: 1px solid var(--console-line); border-radius: 12px;
}
.platform-board-ctas { display: flex; flex-direction: column; gap: 10px; max-width: 420px; margin-top: 10px; }
/* This CTA sits directly on the page's light background (unlike the
   dark scan-form/scan-console panels .scan-form-submit is normally
   paired with), so it needs the light-theme ink/line tokens here, not
   --silver/--console-line - those read as barely-visible pale-on-pale
   text outside a dark panel. */
.platform-cta-secondary { background: none; border: 1px solid var(--line); color: var(--ink-soft); }
.platform-cta-secondary:hover:not(:disabled) { border-color: var(--ink-faint); background: oklch(0 0 0 / 0.04); color: var(--ink); }

/* Was padding:96px + min-height:calc(100vh-156px) centered - on any
   normal-height screen that left the 4-card grid looking like a small
   object floating in a mostly-empty frame ("so much blank space it
   looks unfinished"). Tighter padding and no forced near-viewport
   floor lets the grid's own content set the section's height instead. */
.board-picker { padding: 48px 0 64px; }
.board-picker-inner { max-width: 900px; }
.board-picker .section-heading { margin-top: 14px; margin-bottom: 44px; }
/* align-items:start, not the grid default of stretch: stretch forced
   every card to match the tallest one's height (Sniper's description
   is the longest), and .board-picker-card-cta's margin-top:auto then
   pushed each shorter card's "OPEN X" link all the way down into that
   borrowed height, leaving a large dead gap in Manual/Agent - the
   single biggest visible source of "blank space" on this screen. Each
   card now hugs its own content instead. */
.board-picker-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; align-items: start; }
.board-picker-note { font-family: var(--sans); font-size: 12.5px; color: var(--error); margin-top: 18px; }

/* OVERVIEW's at-a-glance strip (see #5's density ask) - real numbers
   from GET /me and GET /scans (dashboard.js's loadOverviewStats()),
   the same data scans.html/the platform quick-view already draw on,
   not decorative placeholders. Sits above the mode grid so the home
   pane reads as an actual dashboard rather than just a menu. */
.overview-stats {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px;
  padding: 20px 22px; margin: 22px 0 28px;
  background: oklch(0.15 0.008 258); border: 1px solid var(--console-line); border-radius: 12px;
}
.overview-stats .scan-console-stat-value { color: var(--silver); font-size: 19px; }
.overview-stats .scan-console-stat-label { color: var(--silver-faint); }
@media (max-width: 560px) {
  .overview-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.board-picker-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 14px;
  background: var(--panel); border: 1px solid var(--line); padding: 34px 30px;
  text-align: left; cursor: pointer; box-shadow: 0 24px 50px -32px oklch(0.3 0 0 / 0.14);
}
.board-picker-card:hover { border-color: var(--accent-faint); }
.board-picker-card-label { font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em; color: var(--ink); }
.board-picker-card-desc { font-family: var(--sans); font-size: 13.5px; line-height: 1.6; color: var(--ink-soft); margin: 0; }
.board-picker-card-cta { font-family: var(--sans); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-faint); margin-top: 16px; padding-top: 6px; white-space: nowrap; }
.board-picker-card-agent {
  background-color: oklch(0.19 0.011 258);
  background-image:
    radial-gradient(700px 550px at 10% -25%, oklch(0.86 0.004 255 / 0.14), transparent 60%),
    radial-gradient(650px 550px at 106% 122%, oklch(0.62 0.19 254 / 0.30), transparent 60%);
  background-repeat: no-repeat;
  background-size: 220% 220%, 220% 220%;
  animation: panelDrift 15s ease-in-out infinite alternate, glowPulse 5.5s ease-in-out infinite;
  border-color: var(--console-line);
}
.board-picker-card-agent .board-picker-card-label { color: var(--silver); }
.board-picker-card-agent .board-picker-card-desc { color: var(--silver-dim); }
.board-picker-card-agent .board-picker-card-cta { color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-on) .board-picker-card-agent { animation: none !important; }
}

/* Free/standard/plus tiers - DICTIONARY and AGENT are locked behind an upgrade
   (see app/routes/dictionary.py and app/agent_chat.py's tier gates).
   Applied by dashboard.js once /me resolves the account's tier - every
   picker card is a real <button disabled>, so pointer-events:none here
   is just belt-and-suspenders on top of that, not load-bearing. */
.board-picker-card-disabled {
  cursor: default; pointer-events: none; opacity: 0.45;
  animation: none !important;
}
.board-picker-card-disabled .board-picker-card-cta::after { content: " (upgrade to unlock)"; }

/* The permanent split shell - fills most of the viewport so the boards
   read as an app screen, not a form sitting in a sea of background. */
.board-shell { padding: 28px 0 40px; flex: 1; display: flex; flex-direction: column; }
.board-shell-inner.container { max-width: 1760px; padding-left: 32px; padding-right: 32px; flex: 1; }
.board-shell-inner {
  display: grid; grid-template-columns: minmax(360px, 440px) 1fr; gap: 96px;
  /* Setup stays "start" (its own natural content height) - a fixed set
     of chips/inputs doesn't need to stretch, and forcing it to match a
     taller row just left dead space under the form. Results is the one
     pane deliberately told to stretch (align-self on .board-pane-
     results below): the live scan console is a feed that can only
     benefit from more visible height, and "the scanning box doesn't
     reach the bottom of the page" was the actual reported problem, not
     the setup form's own size. min-height is the floor for viewports
     where .board-shell's own flexed height ends up shorter than this
     (e.g. a very short window). */
  align-items: start; min-height: 560px;
}
.board-pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.board-pane-results { align-self: stretch; }
.board-pane .dashboard-header { margin-bottom: 20px; }

.board-pane-setup .scan-form,
.board-pane-setup .agent-chat {
  max-width: none; flex: 1; height: 100%;
}
.board-pane-results .scan-console {
  max-width: none; flex: 1; height: 100%;
}
.board-pane-results .scan-console-feed { flex: 1; max-height: none; }
.scan-console-idle { font-family: var(--sans); font-size: 12px; color: var(--silver-faint); }

/* Internal label row for the setup panels - .scan-console already has
   .scan-console-head as its own internal header, this is the matching
   piece for .scan-form/.agent-chat so both sides of the split are
   named at a glance, not just colored differently. */
.board-panel-head { display: flex; align-items: center; justify-content: space-between; padding-bottom: 16px; margin-bottom: 18px; border-bottom: 1px solid var(--console-line); }

/* --- Agent board shell -----------------------------------------------
   AGENT used to sit inside the same two-independent-floating-boxes
   layout Manual/Dictionary use (.board-shell-inner) - each side is its
   own bordered/shadowed card with a big gap between them, which reads
   fine when both sides are genuinely separate concerns (a form and its
   results), but for a chat it just looked like two random boxes rather
   than one app, especially with an empty "NO ACTIVE SCAN" strip sitting
   next to the conversation with nothing to show yet. This shell is the
   single outer frame both live inside - one border, one shadow, no gap -
   and starts single-column (chat only, full width, capped and centered
   like a real chat product) until a scan actually starts, at which
   point js/agent-chat.js adds .has-results and the results side slides
   in as a real second column - closer to how ChatGPT/Claude only show a
   side panel once there's something to put in it. */
.agent-board-shell {
  display: grid; grid-template-columns: 1fr; min-height: 560px;
  border-radius: 16px; overflow: hidden; border: 1px solid var(--console-line);
  box-shadow: 0 30px 80px -24px oklch(0.62 0.19 254 / 0.4), 0 30px 60px -30px oklch(0 0 0 / 0.55);
}
.agent-board-shell:not(.has-results) { max-width: 820px; margin: 0 auto; }
.agent-board-shell.has-results { grid-template-columns: minmax(340px, 420px) 1fr; }
.agent-board-chat-col { display: flex; min-width: 0; }
.agent-board-results-col { display: none; min-width: 0; border-left: 1px solid var(--console-line); }
.agent-board-shell.has-results .agent-board-results-col { display: flex; }
.agent-board-shell .agent-chat,
.agent-board-shell .scan-console {
  flex: 1; min-width: 0; min-height: 0; max-width: none; width: 100%;
  border: none; border-radius: 0; box-shadow: none;
}
.agent-board-shell .scan-console::before { display: none; }
@media (max-width: 720px) {
  .agent-board-shell:not(.has-results) { max-width: none; }
}

/* Real chat, not a terminal - message bubbles with an avatar on the
   agent's side, a rounded composer with an icon send button, no
   monospace prompt/blinking cursor. Same dark console shell as
   .scan-form/.scan-console (still this site's one visual identity) so
   it doesn't look like a different product, just a different kind of
   panel inside it. */
.agent-chat {
  display: flex; flex-direction: column;
  position: relative; overflow: hidden;
  background-color: oklch(0.17 0.012 258);
  background-image:
    radial-gradient(800px 620px at 12% -20%, oklch(0.86 0.004 255 / 0.11), transparent 58%),
    radial-gradient(700px 620px at 108% 120%, oklch(0.62 0.19 254 / 0.24), transparent 58%),
    linear-gradient(155deg, oklch(0.22 0.013 258) 0%, oklch(0.15 0.011 258) 55%, oklch(0.19 0.013 254) 100%);
  background-repeat: no-repeat;
  background-size: 220% 220%, 220% 220%, 100% 100%;
  animation: panelDrift 10.7s ease-in-out infinite alternate, glowPulse 4.3s ease-in-out infinite;
  border: 1px solid var(--console-line);
  box-shadow: 0 30px 80px -24px oklch(0.62 0.19 254 / 0.4), 0 30px 60px -30px oklch(0 0 0 / 0.55);
}
.agent-chat-log {
  flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 16px;
  padding: 26px 24px 14px;
}
.agent-chat-msg { display: flex; align-items: flex-end; gap: 10px; max-width: 84%; animation: chatMsgIn 0.2s ease-out; }
.agent-chat-msg-agent { align-self: flex-start; }
.agent-chat-msg-user { align-self: flex-end; }
.agent-chat-msg-system { align-self: center; max-width: 92%; }
.agent-chat-avatar {
  width: 24px; height: 24px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: oklch(0.32 0.008 255); color: var(--silver);
}
.agent-chat-bubble {
  padding: 11px 15px; border-radius: 15px;
  font-family: var(--sans); font-size: 13.5px; line-height: 1.55;
}
.agent-chat-msg-agent .agent-chat-bubble {
  background: oklch(0.235 0.012 258); color: var(--silver); border-bottom-left-radius: 4px;
}
.agent-chat-msg-user .agent-chat-bubble {
  background: var(--accent); color: oklch(0.99 0.003 254); border-bottom-right-radius: 4px;
}
.agent-chat-system-text {
  font-family: var(--sans); font-size: 11.5px; letter-spacing: 0.01em; color: var(--silver-faint);
  text-align: center; display: block; padding: 4px 12px;
}
@keyframes chatMsgIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-on) .agent-chat-msg { animation: none !important; }
  html:not(.motion-on) .agent-chat { animation: none !important; }
}

.agent-proposal-card {
  border: 1px solid var(--accent-faint); border-radius: 14px;
  background: oklch(0.19 0.02 258 / 0.6);
  padding: 14px 16px; min-width: 260px;
}
.agent-proposal-summary {
  font-family: var(--sans); font-size: 13px; color: var(--silver);
  line-height: 1.6; margin-bottom: 12px;
}
.agent-proposal-actions { display: flex; gap: 10px; }
.agent-proposal-confirm, .agent-proposal-dismiss {
  font-family: var(--sans); font-size: 12px; letter-spacing: 0.02em;
  background: none; border-radius: 999px; padding: 8px 16px; cursor: pointer; flex: none;
}
.agent-proposal-confirm {
  color: oklch(0.99 0.003 254); background: var(--accent); border: 1px solid var(--accent);
}
.agent-proposal-confirm:hover:not(:disabled) { background: var(--accent-dim); }
.agent-proposal-dismiss {
  color: var(--silver-faint); border: 1px solid var(--console-line);
}
.agent-proposal-dismiss:hover:not(:disabled) { color: var(--silver); }
.agent-proposal-confirm:disabled, .agent-proposal-dismiss:disabled { opacity: 0.5; cursor: default; }

/* A blank log with no messages yet gives no clue where to type - a
   labeled, visibly-bordered input box (not just a borderless inline
   field) is the fix, same "inputs must look interactive" principle as
   the manual form's own boxed fields. */
.agent-chat-input-wrap {
  padding: 10px 20px 18px; border-top: 1px solid var(--console-line);
  background: oklch(0.13 0.01 258 / 0.4);
}
/* The per-conversation message cap tells the user to "start a new one",
   so there has to be a control that does it - a small ghost button
   sitting above the composer rather than a second way to send. */
.agent-chat-input-head { display: flex; justify-content: flex-end; margin-bottom: 6px; }
.agent-chat-input-row {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--console-line); background: oklch(0.13 0.01 258 / 0.6);
  border-radius: 999px; padding: 6px 6px 6px 18px; transition: border-color 0.15s ease;
}
.agent-chat-input-row:focus-within { border-color: var(--accent-faint); }
.agent-chat-input {
  flex: 1; font-family: var(--sans); font-size: 13.5px; color: var(--silver);
  background: none; border: none; outline: none; caret-color: var(--accent);
}
.agent-chat-input::placeholder { color: var(--silver-faint); }
.agent-chat-send {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%; flex: none; cursor: pointer;
  background: var(--accent); color: oklch(0.99 0.003 254); border: none;
}
.agent-chat-send:hover:not(:disabled) { background: var(--accent-dim); }
.agent-chat-send:disabled { opacity: 0.5; cursor: default; }
.agent-chat-newconv {
  font-family: var(--sans); font-size: 10.5px; letter-spacing: 0.04em; color: var(--silver-faint);
  background: none; border: 1px solid var(--console-line); border-radius: 999px; padding: 5px 12px; cursor: pointer;
}
.agent-chat-newconv:hover:not(:disabled) { color: var(--silver); border-color: var(--silver-dim); }
.agent-chat-newconv:disabled { opacity: 0.5; cursor: default; }
.agent-chat .scan-form-error { padding: 0 20px 14px; }

.board-picker-card-sniper { border-color: oklch(0.72 0.16 155 / 0.4); }
.board-picker-card-sniper:hover { border-color: oklch(0.72 0.16 155); }
.board-picker-card-sniper .board-picker-card-cta { color: oklch(0.55 0.16 155); }

/* Sniper used to split into two separate dark boxes side by side (the
   ADD A WATCH form on the left, the pulse on the right) with the
   ACTIVE WATCHES/FOUND lists sitting unboxed on the plain page
   background below - "make the select part of that gray box... and
   what's below that... implement that into the gray box" asked for
   all of it - the form, the live pulse, and both lists - to read as
   one console instead of a form next to a results panel. .sniper-console
   is that single panel: same dark background/glow/sweep language as
   .scan-form/.scan-console elsewhere on this page, just one instance
   big enough to hold all three sections stacked inside it. */
.sniper-console {
  position: relative; overflow: hidden;
  background-color: oklch(0.17 0.011 258);
  background-image:
    radial-gradient(900px 600px at 10% -18%, oklch(0.86 0.004 255 / 0.11), transparent 58%),
    radial-gradient(800px 640px at 106% 118%, oklch(0.62 0.19 254 / 0.22), transparent 58%),
    linear-gradient(155deg, oklch(0.22 0.012 258) 0%, oklch(0.15 0.01 258) 55%, oklch(0.19 0.012 254) 100%);
  background-repeat: no-repeat;
  background-size: 220% 220%, 220% 220%, 100% 100%;
  animation: panelDrift 13.3s ease-in-out infinite alternate, glowPulse 4.8s ease-in-out infinite;
  border: 1px solid var(--console-line);
  box-shadow: 0 30px 70px -28px oklch(0.62 0.19 254 / 0.22), 0 24px 50px -32px oklch(0 0 0 / 0.4);
  padding: 32px 34px 8px;
  display: flex; flex-direction: column; gap: 28px;
}
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-on) .sniper-console { animation: none !important; }
}

.sniper-console-add { display: flex; flex-direction: column; gap: 12px; }
.sniper-console-add-fields { display: flex; gap: 12px; flex-wrap: wrap; align-items: stretch; }
.sniper-console-add-fields .scan-form-input { flex: 1; min-width: 160px; }
.sniper-console-add-fields .scan-form-submit { flex: none; white-space: nowrap; }
.sniper-console-add-foot { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.sniper-console-add-foot .scan-form-hint { flex: 1; min-width: 220px; margin: 0; }
.sniper-console-lists { padding-bottom: 24px; }

.sniper-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 10px 0;
  border-bottom: 1px solid var(--console-line); font-family: var(--sans); font-size: 13px;
}
.sniper-row-name { flex: 1; color: var(--silver); }
.sniper-row-date { color: var(--silver-faint); font-size: 11px; }
.sniper-remove-btn {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; color: var(--silver-faint);
  background: none; border: 1px solid var(--console-line); padding: 4px 8px; cursor: pointer;
}
.sniper-remove-btn:hover { color: var(--silver); border-color: var(--silver-dim); }
.sniper-notify-btn {
  font-family: var(--sans); font-size: 12px; letter-spacing: 0.03em; color: var(--accent-dim);
  background: none; border: 1px solid var(--accent-faint); padding: 9px 16px; cursor: pointer; flex: none;
}
.sniper-notify-btn:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }

/* Sniper's live-checking pulse - the centerpiece visual for this pane,
   distinct from the manual scanner's "decrypting" reveal (dashboard.css's
   .scan-console-slot) on purpose: a watch has no candidate list to
   decrypt through, just one exact handle rechecked forever, so the
   effect is built around that instead - a constant loop of "checking"
   settling into a red taken flash for whichever real watch is up next
   (see js/sniper-board.js - it only ever cycles this account's own
   active watches now, never placeholder data). This is the one place
   on the site that uses the literal red --error signal rather than the
   blue accent everywhere else, because that contrast IS the point of a
   sniper watch. Sized as its own bigger card (not the small strip it
   used to be) since it's the one part of this console that's actually
   alive from second to second - it should read that way at a glance. */
.sniper-pulse {
  position: relative; overflow: hidden;
  background-color: oklch(0.14 0.012 258 / 0.7);
  background-image:
    radial-gradient(600px 300px at 8% -30%, oklch(0.86 0.004 255 / 0.10), transparent 60%),
    radial-gradient(560px 320px at 104% 130%, oklch(0.62 0.19 254 / 0.20), transparent 60%);
  background-repeat: no-repeat; background-size: 220% 220%, 220% 220%;
  animation: panelDrift 13s ease-in-out infinite alternate, glowPulse 5s ease-in-out infinite;
  border: 1px solid var(--console-line); border-radius: 12px; padding: 28px 32px;
}
.sniper-pulse-head { display: flex; align-items: center; gap: 9px; margin-bottom: 18px; }
.sniper-pulse-radar { position: relative; width: 9px; height: 9px; flex: none; }
.sniper-pulse-radar span {
  position: absolute; inset: 0; border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 0 oklch(0.62 0.19 254 / 0.5);
  animation: sniperRadarPing 1.8s ease-out infinite;
}
.sniper-pulse-label { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em; color: var(--silver-faint); }
.sniper-pulse-main { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.sniper-pulse-name { font-family: var(--mono); font-size: 26px; color: var(--silver); }
/* Retriggered from JS (a classList remove+reflow+add) on every tick,
   not just on page load - a bare `animation` property here would only
   ever play once, since swapping the name via .textContent doesn't by
   itself restart a CSS animation. */
.sniper-pulse-name.is-updating { animation: chatMsgIn 0.25s ease-out; }
.sniper-pulse-status {
  font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: 0.14em;
  padding: 6px 14px; border-radius: 6px; color: var(--silver-dim); background: oklch(1 0 0 / 0.06);
  transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.sniper-pulse-status.is-checking { color: var(--silver-dim); animation: sniperCheckingPulse 0.9s ease-in-out infinite; }
.sniper-pulse-status.is-taken { color: oklch(0.68 0.14 25); background: var(--error-glow); box-shadow: 0 0 0 1px oklch(0.5 0.13 25 / 0.4); }
/* Only two tiles here (not the full four the scan console gets) - a
   watch has no per-check rate or error signal to show honestly (see
   js/sniper-board.js's comment on the pulse's illustrative cadence), so
   this reports only what's real: how many of this account's watches
   are currently available vs. still taken, from the same loadWatches()
   data the lists above already render. */
.sniper-pulse-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--console-line); }

@keyframes sniperRadarPing {
  0% { box-shadow: 0 0 0 0 oklch(0.62 0.19 254 / 0.5); }
  100% { box-shadow: 0 0 0 10px oklch(0.62 0.19 254 / 0); }
}
@keyframes sniperCheckingPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-on) .sniper-pulse { animation: none !important; }
  html:not(.motion-on) .sniper-pulse-radar span { animation: none !important; }
  html:not(.motion-on) .sniper-pulse-status.is-checking { animation: none !important; }
  html:not(.motion-on) .sniper-pulse-name { animation: none !important; }
}

/* Live scan dashboard - stat tiles, progress ring, and a rate-limit
   banner, shared by the Manual/Dictionary scan console (dashboard.css's
   .scan-console, driven by js/scan-player.js) and the Sniper pulse
   (.sniper-pulse above, driven by js/sniper-board.js) - one component,
   two dark hosts, same --silver/--console-line vocabulary either way
   rather than a separate visual language per host. */
.scan-console-limit-banner {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em; color: oklch(0.72 0.14 70);
  background: oklch(0.72 0.14 70 / 0.12); border-bottom: 1px solid oklch(0.72 0.14 70 / 0.3);
  padding: 10px 22px;
}
.scan-console-stats { display: flex; align-items: center; gap: 20px; padding: 16px 22px; border-bottom: 1px solid var(--console-line); }
.scan-console-ring-wrap { position: relative; width: 64px; height: 64px; flex: none; display: flex; align-items: center; justify-content: center; }
.scan-console-ring { width: 64px; height: 64px; transform: rotate(-90deg); }
.scan-console-ring-track { fill: none; stroke: var(--console-line); stroke-width: 4; }
.scan-console-ring-fill {
  fill: none; stroke: var(--accent); stroke-width: 4; stroke-linecap: round;
  transition: stroke-dashoffset 0.4s ease;
}
.scan-console-ring-label {
  position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-family: var(--mono); line-height: 1.3;
}
.scan-console-ring-label span:first-child { font-size: 13px; color: var(--silver); }
.scan-console-ring-count { font-size: 8.5px; color: var(--silver-faint); }
.scan-console-stat-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; flex: 1; min-width: 0; }
.scan-console-stat-tile { display: flex; flex-direction: column; gap: 2px; text-align: center; }
.scan-console-stat-value { font-family: var(--mono); font-size: 17px; color: var(--silver); }
.scan-console-stat-label { font-family: var(--sans); font-size: 8.5px; letter-spacing: 0.08em; color: var(--silver-faint); }
.scan-console-stat-available .scan-console-stat-value { color: var(--success); }
.scan-console-stat-errors .scan-console-stat-value { color: oklch(0.68 0.14 25); }
@media (max-width: 560px) {
  .scan-console-stats { flex-wrap: wrap; }
  .scan-console-stat-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
