/* ── INTERKNOT DASHBOARD DESIGN SYSTEM ─────────────────────────────────
   Hazard-terminal aesthetic pulled from InterKnot's own in-world OS:
   near-black chassis, signal-cyan as the dominant "system nominal" accent,
   magenta reserved for alerts/offline/danger states. Sharp panels with
   bracket corners instead of soft rounded cards, monospace for
   anything that reads as "data" (labels, stats, IDs), Inter for
   anything that reads as prose. No gradients outside the scanline
   texture below - flat fills only, to match the terminal read. */

@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600&display=swap');

:root {
  --void: #0B0F14;
  --panel: #151B24;
  --line: #263242;
  --pink: #FF405E;
  --cyan: #00D9FF;
  --amber: #ff8a1e;
  --highlight: #B45CFF;
  --text: #F5F7FA;
  --text-dim: #8A94A6;
  --green: #4ade80;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--void);
  color: var(--text);
  font-family: 'Inter', system-ui, sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 1.5rem 1rem 3rem;
}

/* faint scanline texture - subtle, not a full CRT gimmick */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px,
    transparent 1px, transparent 3px
  );
  z-index: 0;
}

.mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.faq-emoji { width: 1.1em; height: 1.1em; vertical-align: -0.2em; object-fit: contain; }

a { color: inherit; }
a:focus-visible, button:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

/* ── STATUS BAR ─────────────────────────────────────────────────────── */
.statusbar {
  width: 100%;
  max-width: 640px;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  margin-bottom: 1.25rem;
  z-index: 1;
}
.statusbar .dot {
  width: 7px;
  height: 7px;
  flex-shrink: 0;
  background: var(--pink);
  box-shadow: 0 0 6px var(--pink);
}
.statusbar .dot.online { background: var(--cyan); box-shadow: 0 0 6px var(--cyan); }
.statusbar .boot-line { white-space: nowrap; overflow: hidden; }
.statusbar .cursor {
  display: inline-block;
  width: 6px;
  height: 12px;
  background: var(--cyan);
  margin-left: 2px;
  vertical-align: middle;
  animation: blink 1s steps(1) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .statusbar .cursor { animation: none; }
}
@keyframes blink { 50% { opacity: 0; } }

/* ── PANEL ──────────────────────────────────────────────────────────── */
.panel {
  position: relative;
  width: 100%;
  max-width: 640px;
  background: var(--panel);
  border: 1px solid var(--line);
  padding: 2rem;
  z-index: 1;
}
.panel::before, .panel::after {
  content: '';
  position: absolute;
  width: 10px;
  height: 10px;
}
.panel::before { top: -1px; left: -1px; border-top: 2px solid var(--pink); border-left: 2px solid var(--pink); }
.panel::after { bottom: -1px; right: -1px; border-bottom: 2px solid var(--cyan); border-right: 2px solid var(--cyan); }

/* Opt-in wider panel for pages whose content is a card grid rather than
   prose - applied per-page (see assault.ejs, assault-mine.ejs) rather than
   raising the base .panel width, since text-heavy pages (rules,
   guidelines, terms) get harder to read past ~75 characters/line. */
.panel--wide { max-width: 900px; }

.panel-nav {
  display: flex;
  justify-content: space-between;
  margin-bottom: 1.75rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.panel-nav a { color: var(--text-dim); text-decoration: none; }
.panel-nav a:hover { color: var(--cyan); }

/* ── SECTION LABEL (echoes the codebase's own ─── comment style) ─────── */
.section-label {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 0.75rem;
}
.section-label::after { content: ''; flex: 1; height: 1px; background: var(--line); }

/* ── FIELD SUBSECTION (tier groupings within a larger form section) ──── */
.field-subsection { margin-bottom: 1.25rem; }
.field-subsection-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--text);
  margin-bottom: 0.5rem;
}
.field-subsection-label .copy { font-size: 0.72rem; color: var(--text-dim); }
.text-dim { color: var(--text-dim); }

/* ── STAT GRID ──────────────────────────────────────────────────────── */
.stat-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  margin-bottom: 1.75rem;
}
.stat-cell { background: var(--panel); padding: 1.15rem 1.35rem; }
.stat-cell .value {
  font-family: 'JetBrains Mono', monospace;
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--cyan);
  line-height: 1;
}
.stat-cell .label {
  font-size: 0.68rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-top: 0.35rem;
}

/* ── COMMAND LOG ────────────────────────────────────────────────────── */
.log-list { border: 1px solid var(--line); margin-bottom: 1.75rem; }
.log-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1.1rem;
  font-size: 0.82rem;
  border-bottom: 1px solid var(--line);
}
.log-row:last-child { border-bottom: none; }
.log-row .name { font-family: 'JetBrains Mono', monospace; color: var(--text); }
.log-row .type, .score-card-head .type, .changelog-entry .type {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--pink);
  border: 1px solid var(--pink);
  border-radius: 6px;
  padding: 0.1rem 0.35rem;
  margin: 0 0.5rem 0.3rem 0;
  display: inline-block;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: top;
}
.log-row .type.prefix { color: var(--cyan); border-color: var(--cyan); }

/* Semantic status colors: the pink/red default is meant for "needs
   attention" states, not every pill on the site. Without this every action
   and status (approved, denied, ban, unban...) read as the same color,
   which made lists like suggestions and mod logs hard to scan at a glance. */
.type.approved, .type.implemented, .type.unban, .type.unmute, .type.open {
  color: var(--green); border-color: var(--green);
}
.type.pending, .type.review, .type.warn, .type.auto-timeout {
  color: var(--amber); border-color: var(--amber);
}
.type.denied, .type.ban, .type.kick, .type.mute, .type.rejected {
  color: var(--pink); border-color: var(--pink);
}
.type.closed, .type.removewarn, .type.clearwarns {
  color: var(--text-dim); border-color: var(--line);
}
.type.low { color: var(--green); border-color: var(--green); }
.type.medium { color: var(--amber); border-color: var(--amber); }
.type.high, .type.urgent { color: var(--pink); border-color: var(--pink); }
/* Changelog entry types (dashboard/changelogAdmin.js, public /changelog
   page + the admin list at /server/changelog) - same semantic-color idea
   as the status/priority pills above, so the four kinds stay scannable
   in a newest-first list instead of reading as one undifferentiated feed. */
.type.feature { color: var(--green); border-color: var(--green); }
.type.bugfix { color: var(--amber); border-color: var(--amber); }
.type.rule { color: var(--pink); border-color: var(--pink); }
.type.rotation { color: var(--cyan); border-color: var(--cyan); }
.log-row .counts { color: var(--text-dim); font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; }

.role-slot {
  border: 1px solid var(--line);
  padding: 0.6rem 0.7rem;
  margin-bottom: 0.5rem;
}

/* ── FACTION ROSTER ─────────────────────────────────────────────────── */
.faction-row {
  padding: 0.9rem 1.1rem;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid var(--faction-color, var(--line));
}
.faction-row:last-child { border-bottom: none; }
.faction-row-head { display: flex; align-items: center; justify-content: space-between; }
.faction-swatch {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 0.45rem;
  background: var(--faction-color, var(--text-dim));
  box-shadow: 0 0 5px var(--faction-color, transparent);
}
.faction-emoji { width: 1.1em; height: 1.1em; vertical-align: -0.2em; object-fit: contain; }
.faction-members {
  margin-top: 0.45rem;
  font-size: 0.75rem;
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
  line-height: 1.7;
}
.faction-members .empty { font-style: italic; }

.faction-avatar-list { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.faction-avatar {
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 1px solid var(--line);
  object-fit: cover;
  flex-shrink: 0;
}
.faction-avatar-fallback {
  display: flex; align-items: center; justify-content: center;
  background: var(--panel);
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
}
.faction-avatar-more {
  font-size: 0.68rem;
  color: var(--text-dim);
  padding: 0 0.3rem;
}

.empty-state {
  color: var(--text-dim);
  font-size: 0.85rem;
  padding: 1.5rem 1rem;
  border: 1px dashed var(--line);
  text-align: center;
  margin-bottom: 1.75rem;
}

/* ── SKELETON LOADERS ───────────────────────────────────────────────
   Placeholder for art pulled from external CDNs (boss portraits, agent/
   W-Engine/disk icons, element badges, leaderboard avatars) - a
   shimmering panel/line-toned bar shown until the real image is
   actually in the browser cache, then /skeleton.js reveals it by
   removing .skel-loading. Two shapes, matched to how these images are
   already marked up:
     <img class="... skel-loading" src="...">            same element,
       the shimmer sits behind it as a plain CSS background and gets
       naturally painted over once the real image loads.
     <div class="... skel-loading" data-skel-src="..." style="background-
     image:url(...)">   background-image elements (boss banners, card
       thumbnails) can't have their background-image deferred without
       losing it for no-JS visitors, so this overlays the shimmer via
       ::after (on the same positioned box) and /skeleton.js preloads
       data-skel-src separately just to know when to lift the overlay.
   Degrades safely with JS off: the overlay/background never gets
   removed, so the shimmer just stays put instead of a broken loading
   state - existing image/no-image fallbacks are unaffected either way. */
.skel-loading { background: linear-gradient(100deg, var(--panel) 28%, var(--line) 50%, var(--panel) 72%); background-size: 200% 100%; animation: skel-shimmer 1.4s ease-in-out infinite; }
[data-skel-src].skel-loading::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, var(--panel) 28%, var(--line) 50%, var(--panel) 72%);
  background-size: 200% 100%;
  animation: skel-shimmer 1.4s ease-in-out infinite;
}
@keyframes skel-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .skel-loading, [data-skel-src].skel-loading::after { animation: none; }
}

/* ── COOKIE CONSENT BANNER ─────────────────────────────────────────────
   Shown once, pinned to the bottom of the viewport, on every public
   league page (included from partials/leagueBar.ejs) until the visitor
   picks Accept or Reject - see partials/cookieBanner.ejs and
   public/cookieConsent.js. Same fixed-overlay/shadow treatment as
   .toast-stack above, just anchored to the opposite edge so the two
   can never collide. Hidden by default in the markup (no flash of it
   before JS has had a chance to check for a saved choice) and stays
   hidden with JS off, same as a toast would - nothing on the site
   needs a saved choice to function. */
.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 1rem;
  padding: 1rem 1.25rem;
  background: var(--panel);
  border-top: 1px solid var(--line);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
}
.cookie-banner[hidden] { display: none; }
.cookie-banner-copy {
  color: var(--text-dim);
  font-size: 0.8rem;
  line-height: 1.5;
  max-width: 640px;
  margin: 0;
}
.cookie-banner-copy a { color: var(--cyan); text-decoration: none; }
.cookie-banner-copy a:hover { text-decoration: underline; text-underline-offset: 2px; }
.cookie-banner-actions { display: flex; gap: 0.6rem; flex-shrink: 0; }
@media (max-width: 560px) {
  .cookie-banner { flex-direction: column; text-align: center; }
  .cookie-banner-actions { justify-content: center; width: 100%; }
  .cookie-banner-actions .btn { flex: 1 1 auto; }
}

/* ── IDENTITY / GUILD HEADER ───────────────────────────────────────── */
.identity { display: flex; align-items: center; gap: 0.85rem; margin-bottom: 1.75rem; }
.identity img { width: 44px; height: 44px; border: 1px solid var(--line); }
.identity h1 { margin: 0; font-size: 1.15rem; font-weight: 600; }
.identity .meta { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-dim); margin-top: 0.2rem; }

/* ── BUTTONS ────────────────────────────────────────────────────────── */
.btn {
  display: inline-block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 0.65rem 1.1rem;
  border: 1px solid var(--line);
  color: var(--text);
  background: transparent;
  transition: border-color 0.15s ease-out, color 0.15s ease-out, background-color 0.15s ease-out, transform 0.12s ease-out;
}
.btn:hover { border-color: var(--cyan); color: var(--cyan); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn-primary { border-color: var(--pink); color: var(--pink); }
.btn-primary:hover { background: var(--pink); color: var(--void); }
.btn:disabled, .btn:disabled:hover {
  border-color: var(--line);
  color: var(--text-dim);
  background: transparent;
  cursor: not-allowed;
  opacity: 0.6;
}

.copy { color: var(--text-dim); font-size: 0.85rem; line-height: 1.6; margin: 0 0 1.5rem; }
.copy strong { color: var(--text); font-weight: 600; }

.alert {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  color: var(--amber);
  border: 1px solid var(--amber);
  padding: 0.6rem 0.9rem;
  margin-bottom: 1.25rem;
}

.notice {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  color: var(--cyan);
  border: 1px solid var(--cyan);
  padding: 0.6rem 0.9rem;
  margin: 0 0 1.25rem;
}

.notice--error {
  color: var(--pink);
  border-color: var(--pink);
}

.notice--success {
  color: var(--green);
  border-color: var(--green);
}

/* ── TOAST (flash messages) ────────────────────────────────────────────
   `.notice--flash` messages (save confirmations, errors from a redirect)
   get pulled out of the page flow and shown as a floating toast instead,
   so a settings save reads as "done" without the abrupt jump of a plain
   inline paragraph appearing above the fold on reload. */
.toast-stack {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  max-width: min(90vw, 380px);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  background: var(--panel);
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.22s ease-out, transform 0.22s ease-out;
}
.toast.toast--in { opacity: 1; transform: translateY(0); }
.toast.toast--out { opacity: 0; transform: translateY(-8px); }
.toast-close {
  background: none;
  border: none;
  color: inherit;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  margin-left: auto;
  opacity: 0.6;
}
.toast-close { transition: opacity 0.12s ease-out; }
.toast-close:hover { opacity: 1; }

code {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.85em;
  color: var(--cyan);
}

/* ── FORMS ──────────────────────────────────────────────────────────── */
.settings-form {
  border: 1px solid var(--line);
  padding: 1.25rem;
  margin-bottom: 1.25rem;
}

.field-row { margin-bottom: 1rem; }
.field-row:last-of-type { margin-bottom: 0; }
.field-row label {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.4rem;
}
.field-row label.slider-label { display: flex; justify-content: space-between; align-items: baseline; }
.slider-value {
  color: var(--pink);
  font-weight: 700;
  font-size: 0.8rem;
  text-transform: none;
  letter-spacing: normal;
}

.chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.3rem;
}
.chip {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  color: var(--text-dim);
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  text-decoration: none; /* chips that are <a> links (create-mode toggle) were underlined */
}
.chip:hover { border-color: var(--pink); color: var(--text); }
.chip-active {
  color: var(--pink);
  border-color: var(--pink);
  background: rgba(255, 64, 94, 0.1);
  font-weight: 700;
}
.slider-ticks {
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  color: var(--text-dim);
  padding: 0 1px;
}

.field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem 1rem;
  margin: 0.75rem 0 1.25rem;
}

input[type="text"], input[type="number"], input[type="date"], input[type="datetime-local"], input[type="url"], input[type="search"], select, textarea {
  width: 100%;
  background: var(--void);
  border: 1px solid var(--line);
  color: var(--text);
  padding: 0.55rem 0.7rem;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.85rem;
  color-scheme: dark;
}
textarea { resize: vertical; font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; }
input[type="text"]:focus, input[type="number"]:focus, input[type="date"]:focus, input[type="datetime-local"]:focus, input[type="url"]:focus, input[type="search"]:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--cyan);
}
/* Safari/Chrome add a built-in clear ("x") button to type=search inputs
   with its own UA sizing - webkit-only pseudo-element, harmless no-op
   everywhere else. */
input[type="search"]::-webkit-search-cancel-button { cursor: pointer; }

/* ── AGENT PICKER (Deadly Assault team select) ─────────────────────────
   Portrait-tile replacement for a plain <select>. Tiles render in
   whatever order agentNames arrives in - alphabetical, sorted server-side
   in app.js (see assault-admin.ejs comment above the picker script). */
.agent-picker-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem 1rem;
  margin: 0.75rem 0 1.25rem;
}
.agent-picker { position: relative; min-width: 0; }
.agent-picker-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--void);
  border: 1px solid var(--line);
  color: var(--text);
  padding: 0.4rem 0.6rem;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.85rem;
  text-align: left;
  cursor: pointer;
}
.agent-picker-trigger:hover, .agent-picker-trigger:focus, .agent-picker-trigger.is-open {
  outline: none;
  border-color: var(--cyan);
}
.agent-picker-trigger:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.agent-picker-trigger:disabled:hover { border-color: var(--line); }
.agent-picker-avatar {
  width: 28px;
  height: 28px;
  border-radius: 3px;
  border: 1px solid var(--line);
  flex-shrink: 0;
  background-color: var(--panel);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}
/* overflow-x/y set separately (instead of the shorthand `overflow: hidden`)
   so the name still truncates with an ellipsis horizontally, but the
   "Required" tag's pill background/padding - taller than this label's own
   line box - isn't clipped along the bottom edge. */
.agent-picker-label { flex: 1; min-width: 0; overflow-x: hidden; overflow-y: visible; text-overflow: ellipsis; white-space: nowrap; }
.agent-picker-placeholder { color: var(--text-dim); }
.agent-picker-caret { color: var(--text-dim); font-size: 0.65rem; flex-shrink: 0; }

/* Locked W-Engine slot (DA challenge requires a specific engine for this
   agent) - same trigger look as the normal picker button, just not
   interactive, with a plain text "required" tag instead of a caret. */
.agent-picker-trigger--locked { cursor: default; }
.agent-picker-trigger--locked:hover { border-color: var(--line); }
.agent-picker-required-tag {
  color: var(--text-dim);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-left: 0.35rem;
}

/* Live team-cost readout in the submit form - over the challenge's cost
   cap just gets bold text, not a color change (this page intentionally
   skips the rest of the dashboard's accent-color coding). */
.cost-over { font-weight: 700; text-decoration: underline; }

.agent-picker-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: var(--void);
  display: flex;
  flex-direction: column;
}
/* Same [hidden]-vs-author-CSS cascade note as above: this rule has to
   exist explicitly, an unconditional `display: flex` on the base
   selector would silently win over the browser's default
   `[hidden] { display: none }` at equal specificity. */
.agent-picker-overlay[hidden] { display: none; }
.agent-picker-overlay-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--line);
  flex-shrink: 0;
}
.agent-picker-overlay-title {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--cyan);
}
.agent-picker-overlay-close {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--line);
  color: var(--text-dim);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
}
.agent-picker-overlay-close:hover, .agent-picker-overlay-close:focus {
  outline: none;
  border-color: var(--cyan);
  color: var(--cyan);
}
.agent-picker-overlay-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
/* Locked onto <body> while any overlay is open, so the page underneath
   can't scroll behind it (there's nothing to see back there anyway -
   the overlay covers the full viewport). */
body.agent-picker-lock { overflow: hidden; }

/* Apply/Cancel footer - tapping a tile only stages a pick now (see the
   picker script's comment for why), so this is the actual commit action. */
.agent-picker-overlay-foot {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  padding: 0.7rem 1rem;
  border-top: 1px solid var(--line);
  flex-shrink: 0;
}

.agent-picker-search {
  border: none !important;
  border-bottom: 1px solid var(--line) !important;
  flex-shrink: 0;
}
.agent-filter-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  padding: 0.45rem 0.5rem 0;
  flex-shrink: 0;
}
.agent-filter-group:first-of-type { padding-top: 0.55rem; }
.agent-filter-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  flex-basis: 100%;
  margin-bottom: 0.1rem;
}
.agent-filter-chip {
  background: var(--void);
  border: 1px solid var(--line);
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.28rem 0.55rem;
  cursor: pointer;
}
.agent-filter-chip:hover { border-color: var(--cyan); color: var(--text); }
.agent-filter-chip.is-active { border-color: var(--cyan); color: var(--cyan); background: rgba(0, 217, 255, 0.08); }
.agent-filter-clear {
  background: none;
  border: none;
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  text-decoration: underline;
  cursor: pointer;
  padding: 0.28rem 0.3rem;
  margin-left: auto;
}
.agent-filter-clear:hover { color: var(--cyan); }
.agent-picker-list {
  overflow-y: auto;
  min-height: 0;
  flex: 1 1 auto;
  /* Flex-wrap, not grid: with a CSS grid here, tiles hidden by a filter
     (display:none, toggled in applyAgentFilters) were leaving their row
     roughly where it used to be instead of collapsing, so filtering down
     to a handful of agents left big blank bands between the surviving
     rows. Flex-wrap with fixed-width tiles just reflows them tight
     against whatever's still visible, no leftover space. */
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 0.35rem;
  padding: 0.5rem;
}
.agent-tile {
  display: flex;
  flex: 0 0 72px;
  /* Flex items default to min-width:auto, which lets unbreakable content
     grow a box past its flex-basis. "Metanukimorphosis" (one long word,
     nothing to wrap on) was doing exactly that - its tile rendered wider
     than the fixed 72px every other tile gets, which pushed that tile's
     centered icon/label off to one side of its own (now-wider) box and
     made the whole row after it look shifted right compared to the row
     above. min-width: 0 forces every tile back to the real 72px
     regardless of label length; overflow-wrap on .agent-tile-name below
     is what then lets a long single word actually break to fit that
     width instead of overflowing. */
  min-width: 0;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  background: none;
  border: 1px solid transparent;
  border-radius: 2px;
  padding: 0.3rem 0.15rem;
  cursor: pointer;
  font-family: 'Inter', system-ui, sans-serif;
}
.agent-tile:hover:not(:disabled), .agent-tile:focus:not(:disabled) {
  outline: none;
  border-color: var(--cyan);
}
.agent-tile.is-selected { border-color: var(--pink); background: rgba(255, 64, 94, 0.08); }
.agent-tile:disabled { opacity: 0.28; cursor: not-allowed; }
.agent-tile img, .agent-tile-noimg {
  width: 44px;
  height: 44px;
  border-radius: 4px;
  border: 1px solid var(--line);
  /* contain, not cover: several W-Engine icons on the asset mirror are
     wide/rectangular banners rather than square, so cover was cropping
     into the middle of the art (e.g. slicing the text off "Marcato
     Desire"'s chest). Contain always shows the whole icon, letterboxed
     against the tile's own background on non-square art. */
  object-fit: contain;
  background: var(--void);
}
.agent-tile-name {
  font-size: 0.62rem;
  line-height: 1.15;
  text-align: center;
  color: var(--text-dim);
  /* Lets a long single-word name (Metanukimorphosis, Girtablullu, ...)
     actually wrap within the tile's fixed width instead of overflowing
     it - see min-width: 0 on .agent-tile above, which is what makes the
     tile enforce that width in the first place. */
  overflow-wrap: break-word;
  word-break: break-word;
}
.agent-tile.is-selected .agent-tile-name { color: var(--text); }
.agent-tile-empty {
  grid-column: 1 / -1;
  text-align: center;
  color: var(--text-dim);
  font-size: 0.8rem;
  padding: 1rem 0;
}

/* Boss portraits are full character art, not small square icons like the
   agent/engine/disk tiles this grid is shared with, so they read better
   noticeably bigger - scoped to #bossList only, doesn't touch the other
   pickers' tile sizing. */
/* Shared with the Rotation Builder's 3 boss pickers ([id^="rbBossList"])
   below - same full-art portrait tiles, just one overlay instance per
   challenge slot instead of a single #bossList. */
#bossList.agent-picker-list,
[id^="rbBossList"].agent-picker-list { gap: 0.6rem; }
#bossList .agent-tile,
[id^="rbBossList"] .agent-tile { flex-basis: 108px; gap: 0.35rem; padding: 0.4rem 0.2rem; }
#bossList .agent-tile img,
#bossList .agent-tile-noimg,
[id^="rbBossList"] .agent-tile img,
[id^="rbBossList"] .agent-tile-noimg {
  width: 100px;
  height: 100px;
  /* Boss art is full-bleed character portraits, not letterboxed icons
     like the agent/engine/disk tiles this grid shares CSS with -
     "contain" (the shared .agent-tile img default) pillarboxes them
     against the void background, showing bars down the sides. Cover
     fills the tile instead, same as every other full-art thumbnail in
     the app (run-card-avatar, run-agent-avatar, etc.). */
  object-fit: cover;
}
#bossList .agent-tile-name,
[id^="rbBossList"] .agent-tile-name { font-size: 0.68rem; }

.toggle-row {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-size: 0.85rem;
  cursor: pointer;
  margin-bottom: 0.6rem;
}
.toggle-row input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--cyan);
  cursor: pointer;
}
.toggle-row--inline { margin-bottom: 0; justify-content: center; }

/* Read-only weakness/resistance chips on the assault-admin "Boss stats"
   preview (see #bossWeaknessChips/#bossResistanceChips in
   assault-admin.ejs) - deliberately NOT .toggle-row/.toggle-row--inline,
   which are built for a checkbox+label row and force display:flex +
   justify-content:center on their own block-level box. Reusing those for
   plain read-only chips stacked each one centered on its own line instead
   of flowing left-to-right next to "Weakness:"/"Resistance:". This is the
   same icon+text pairing but as an inline-flex chip so several sit side
   by side and wrap normally. */
.element-chip-list { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.9rem; vertical-align: middle; }
.element-chip { display: inline-flex; align-items: center; gap: 0.3rem; }

.btn-small {
  padding: 0.35rem 0.6rem;
  font-size: 0.65rem;
}

.btn-link {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.85rem;
  line-height: 1;
  padding: 0;
}
.btn-link:hover { color: var(--pink); }

.inline-add-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.75rem;
}
.inline-add-form select, .inline-add-form input[type="text"] { flex: 1; }

.filter-form {
  display: flex;
  align-items: flex-end;
  gap: 1rem;
  flex-wrap: wrap;
}
.filter-form .field-row { margin-bottom: 0; min-width: 180px; flex: 1; }

/* ── LOG CONFIG TABLE ───────────────────────────────────────────────── */
.log-config-table { border: 1px solid var(--line); }
.log-config-head, .log-config-row {
  display: grid;
  grid-template-columns: 1fr 80px 1.4fr;
  gap: 0.75rem;
  align-items: center;
  padding: 0.55rem 0.9rem;
}
.log-config-head {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  border-bottom: 1px solid var(--line);
}
.log-config-row { border-bottom: 1px solid var(--line); font-size: 0.82rem; }
.log-config-row:last-child { border-bottom: none; }

/* ── CASE LOG ROWS ──────────────────────────────────────────────────── */
.log-row--case { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.case-meta { display: flex; align-items: center; gap: 1.2rem; row-gap: 0.5rem; flex-wrap: wrap; color: var(--text-dim); font-size: 0.72rem; }
.case-meta .case-user, .case-meta .case-mod { display: inline-flex; align-items: center; gap: 0.4rem; }
.avatar-sm { width: 18px; height: 18px; border-radius: 2px; border: 1px solid var(--line); vertical-align: middle; object-fit: cover; background: var(--void); }

/* Select + live preview icon (DA challenge disk set pickers) - keeps the
   icon inline beside the select instead of wrapping onto its own line
   below it, which is what a plain 100%-width <select> + inline <img>
   would otherwise do the moment the icon appears (or reserve a blank
   line for once it's hidden again). */
.select-with-preview { display: flex; align-items: center; gap: 0.4rem; }
.select-with-preview select { flex: 1; min-width: 0; }
.select-with-preview img { flex-shrink: 0; }
.ignored-user { display: inline-flex; align-items: center; gap: 0.4rem; }

.player-header { display: flex; align-items: center; gap: 0.65rem; margin-top: 1.75rem; }
.player-header-avatar { width: 36px; height: 36px; border-radius: 4px; border: 1px solid var(--line); object-fit: cover; flex-shrink: 0; }
.player-header-avatar--fallback {
  display: flex; align-items: center; justify-content: center;
  background: var(--panel); color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;
}
.case-meta .dim { color: var(--text-dim); font-size: 0.65rem; }
.case-reason { color: var(--text); font-size: 0.82rem; }

.staff-note {
  border-left: 2px solid var(--amber);
  padding: 0.4rem 0.7rem;
  font-size: 0.8rem;
  color: var(--text);
  background: rgba(255, 138, 30, 0.06);
}
.staff-note-label {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--amber);
  margin-bottom: 0.2rem;
}

.suggestion-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.suggestion-actions form { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; min-width: 0; }
.suggestion-actions input[type="text"] { flex: 1 1 140px; min-width: 90px; }

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 1rem;
}

/* ── SCORE CARDS (faction scores pending queue) ───────────────────────── */
.score-card-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.75rem; }
.score-card {
  border: 1px solid var(--line);
  border-left: 3px solid var(--faction-color, var(--line));
  padding: 0.9rem 1rem;
}
.score-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.4rem;
}
.score-card-user { display: inline-flex; align-items: center; gap: 0.45rem; }
.score-card-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-top: 0.75rem;
}
.score-card-actions form { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
.reject-form input[type="text"] {
  flex: 1 1 120px;
  min-width: 90px;
}

/* ── MOBILE ─────────────────────────────────────────────────────────────
   Everything above is written assuming enough width for flex rows to sit
   side by side and 2-column grids to stay legible. Below ~480px (most
   phones in portrait) that stops being true, so: shrink the panel's own
   padding, collapse 2-column grids to 1, and let any row that was relying
   on space-between (log-row, filter-form, score-card-head, etc.) wrap
   instead of squeezing/overflowing horizontally. */
@media (max-width: 480px) {
  body { padding: 1rem 0.5rem 2rem; }
  .panel { padding: 1.1rem 0.9rem; }

  .field-grid { grid-template-columns: 1fr; }
  .agent-picker-row { grid-template-columns: minmax(0, 1fr); }

  .log-config-head, .log-config-row {
    grid-template-columns: 1fr;
    gap: 0.35rem;
    padding: 0.6rem 0.75rem;
  }
  .log-config-head span:nth-child(2),
  .log-config-head span:nth-child(3) { display: none; }
  .log-config-row { padding-bottom: 0.75rem; }

  .log-row {
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
  }
  .log-row .counts { align-self: flex-start; }
  .log-row form,
  .log-row .toggle-row--inline { align-self: flex-start; }

  .inline-add-form { flex-direction: column; }
  .filter-form { flex-direction: column; align-items: stretch; }
  .filter-form .field-row { min-width: 0; }

  .score-card-actions { flex-direction: column; align-items: stretch; }
  .score-card-actions form { justify-content: space-between; }

  .suggestion-actions { flex-direction: column; align-items: stretch; }
  .suggestion-actions form { justify-content: space-between; }

  .identity { align-items: flex-start; }
  .stat-grid { grid-template-columns: 1fr 1fr; }
}

/* Narrow phones (iPhone SE / small Android, ~360px and under): even a 2x2
   stat-grid gets cramped once a label like "Awaiting review" has to share a
   row with a 3-digit value, and the overview module-grid's 2-column fallback
   starts wrapping card descriptions mid-word. Drop both to a single column
   here instead of shrinking type further. */
@media (max-width: 400px) {
  .stat-grid { grid-template-columns: 1fr; }
  .module-grid { grid-template-columns: 1fr; }
}

/* Filter bars pack several controls onto one line - fine on desktop, but
   the 480px cutoff above leaves a wide dead zone (tablets, split-screen,
   a phone in landscape) where they're still forced into one row. The
   per-row action forms (score-card-actions, suggestion-actions) don't need
   a breakpoint for this anymore - their inputs use flex-basis + min-width
   instead of a fixed px width, so they wrap and shrink at any width. */
@media (max-width: 640px) {
  .filter-form { flex-direction: column; align-items: stretch; }
  .filter-form .field-row { min-width: 0; }
}

/* Long snowflake IDs, screenshot URLs, panel IDs etc. must be able to break
   rather than force the whole page to scroll horizontally on a phone. */
.mono, .copy, code { overflow-wrap: anywhere; }

/* ═══════════════════════════════════════════════════════════════════════
   APP SHELL: sidebar + topbar dashboard layout, used by every /server*
   page (partials/head.ejs + sidebar.ejs + topbar.ejs + foot.ejs). The
   standalone centered `.panel` styles above stay untouched for the public
   landing page (home.ejs) and the 403 page, which don't get a sidebar.
   ═══════════════════════════════════════════════════════════════════════ */

body.dashboard-body {
  display: block;
  padding: 0;
  align-items: initial;
}

.app-shell {
  position: relative;
  display: flex;
  min-height: 100vh;
  z-index: 1;
}

/* ── SIDEBAR ────────────────────────────────────────────────────────── */
.sidebar {
  width: 250px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border-right: 1px solid var(--line);
  height: 100vh;
  position: sticky;
  top: 0;
  overflow-y: auto;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 1.25rem 1.1rem;
  border-bottom: 1px solid var(--line);
  position: relative;
  text-decoration: none;
  flex-shrink: 0;
}
/* hazard-stripe rule instead of a soft gradient underline */
.sidebar-brand::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 3px;
  background: repeating-linear-gradient(
    -45deg, var(--cyan) 0 8px, var(--void) 8px 16px
  );
}
.brand-mark {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'JetBrains Mono', monospace;
  font-weight: 700;
  font-size: 0.8rem;
  color: var(--void);
  background: var(--cyan);
  clip-path: polygon(0 0, 100% 0, 100% 75%, 75% 100%, 0 100%);
  overflow: hidden;
}
.brand-mark-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.brand-text { display: flex; flex-direction: column; line-height: 1.3; }
.brand-name { font-size: 0.82rem; font-weight: 700; letter-spacing: 0.04em; color: var(--text); }
.brand-sub { font-size: 0.62rem; color: var(--text-dim); letter-spacing: 0.06em; text-transform: uppercase; }

.sidebar-guild {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.9rem 1.1rem;
  border-bottom: 1px solid var(--line);
  flex-shrink: 0;
}
.guild-icon { width: 34px; height: 34px; border: 1px solid var(--line); object-fit: cover; flex-shrink: 0; }
.guild-icon--fallback {
  display: flex; align-items: center; justify-content: center;
  font-size: 0.72rem; color: var(--text-dim); background: var(--void);
}
.guild-info { display: flex; flex-direction: column; min-width: 0; gap: 0.15rem; }
.guild-name { font-size: 0.8rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.guild-status { display: flex; align-items: center; gap: 0.4rem; font-size: 0.63rem; color: var(--text-dim); letter-spacing: 0.05em; text-transform: uppercase; }
.guild-status .dot { width: 6px; height: 6px; background: var(--pink); box-shadow: 0 0 6px var(--pink); flex-shrink: 0; }
.guild-status .dot.online { background: var(--cyan); box-shadow: 0 0 6px var(--cyan); }

.sidebar-nav {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.85rem 0.7rem;
  overflow-y: auto;
}
.nav-group-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--highlight);
  padding: 0.9rem 0.5rem 0.3rem;
}
.nav-link {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.55rem 0.6rem;
  border-radius: 3px;
  font-size: 0.83rem;
  color: var(--text-dim);
  text-decoration: none;
  border-left: 2px solid transparent;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.nav-link:hover { color: var(--text); background: rgba(255,255,255,0.03); }
.nav-link.active {
  color: var(--cyan);
  background: rgba(0,217,255,0.08);
  border-left-color: var(--cyan);
}
.nav-icon {
  width: 16px; height: 16px; flex-shrink: 0;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.nav-badge {
  margin-left: auto;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  font-weight: 700;
  color: var(--void);
  background: var(--pink);
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  line-height: 1.5;
}

.sidebar-user {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 1.1rem;
  border-top: 1px solid var(--line);
  flex-shrink: 0;
}
.user-avatar { width: 30px; height: 30px; border-radius: 999px; border: 1px solid var(--line); flex-shrink: 0; }
.user-info { display: flex; flex-direction: column; min-width: 0; flex: 1; gap: 0.1rem; }
.user-name { font-size: 0.78rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-role { font-size: 0.6rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.06em; }
.user-logout {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex-shrink: 0;
  color: var(--text-dim); border: 1px solid var(--line); text-decoration: none;
  transition: color 0.12s, border-color 0.12s;
}
.user-logout:hover { color: var(--pink); border-color: var(--pink); }
.user-logout svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

.sidebar-toggle-input, .sidebar-backdrop { display: none; }

/* ── MAIN / TOPBAR / CONTENT ───────────────────────────────────────── */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 1rem 1.75rem;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--void);
  z-index: 2;
}
.topbar-title { flex: 1; min-width: 0; }
.topbar-title h1 { margin: 0; font-size: 1.05rem; letter-spacing: 0.01em; font-weight: 600; }
.topbar-subtitle { margin: 0.15rem 0 0; font-size: 0.78rem; color: var(--text-dim); }
/* Header counts as pills (Proxy League review page). Smaller than the
   in-page pills so three fit beside the hamburger and icons on a phone;
   number uses the NEPL accent, same as the My submissions summary. */
.topbar-pills { margin-top: 0.3rem; gap: 0.3rem; }
.topbar-pills .da-sub-pill { font-size: 0.68rem; padding: 0.15rem 0.45rem; }
.topbar-pills .da-sub-pill strong { color: var(--accent, var(--sel, var(--text))); margin-left: 0.15rem; }
.topbar-status {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.68rem; letter-spacing: 0.04em; color: var(--text-dim);
  white-space: nowrap;
}
.topbar-status .dot { width: 6px; height: 6px; background: var(--pink); box-shadow: 0 0 6px var(--pink); }
/* Accent-aware like the rest of the site's --accent-driven bits - falls
   back to the brand cyan when no accent is picked, so nothing changes for
   anyone who's never touched the accent picker. */
.topbar-status .dot.online { background: var(--accent, var(--cyan)); box-shadow: 0 0 6px var(--accent, var(--cyan)); }

/* ── ASK THE BOT (topbar search bar) ───────────────────────────────── */
/* ── ASK THE BOT (overview page panel, above Activity) ────────────────── */
.ask-bot-form {
  display: flex; align-items: center; gap: 0.6rem;
  background: var(--void); border: 1px solid var(--line);
  border-radius: 8px; padding: 0.55rem 0.8rem;
  transition: border-color 0.12s;
}
.ask-bot-form:focus-within { border-color: var(--cyan); }
.ask-bot-icon { width: 16px; height: 16px; flex-shrink: 0; fill: none; stroke: var(--text-dim); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.ask-bot-form input[type="text"] {
  flex: 1; min-width: 0; background: none; border: none; outline: none;
  color: var(--text); font-size: 0.88rem; font-family: inherit;
}
.ask-bot-form input[type="text"]::placeholder { color: var(--text-dim); }
.ask-bot-submit {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; flex-shrink: 0; padding: 0;
  background: none; border: none; color: var(--text-dim); cursor: pointer;
  transition: color 0.12s;
}
.ask-bot-submit:hover { color: var(--cyan); }
.ask-bot-submit svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.ask-bot-panel {
  margin-top: 0.85rem; padding: 0.85rem 1rem;
  background: var(--void); border: 1px solid var(--line); border-radius: 8px;
  font-size: 0.85rem; line-height: 1.5; max-height: 320px; overflow-y: auto;
}
.ask-bot-loading { color: var(--text-dim); }
.ask-bot-answer { color: var(--text); white-space: pre-wrap; }
.ask-bot-error { color: var(--pink); }

.hamburger {
  display: none;
  width: 30px; height: 30px;
  align-items: center; justify-content: center;
  border: 1px solid var(--line);
  cursor: pointer;
  flex-shrink: 0;
}
.hamburger svg { width: 16px; height: 16px; fill: none; stroke: var(--text); stroke-width: 1.8; stroke-linecap: round; }

.content { flex: 1; padding: 1.75rem; }
.content-inner { max-width: 1080px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem; }

/* `.panel` keeps its bracket-corner look inside the dashboard, it just
   drops the old 640px single-card width restriction. */
.content-inner .panel { max-width: none; width: 100%; margin: 0; }

/* ── OVERVIEW HERO ──────────────────────────────────────────────────── */
.hero {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.hero img.identity-icon { width: 56px; height: 56px; border: 1px solid var(--line); flex-shrink: 0; }
.hero-fallback-icon {
  width: 56px; height: 56px; flex-shrink: 0; border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace; color: var(--text-dim); background: var(--void);
}
.hero h1 { margin: 0; font-size: 1.3rem; }
.hero .meta { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--text-dim); margin-top: 0.3rem; }

/* ── MODULE GRID (overview page "app cards") ───────────────────────── */
.module-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}
.module-card {
  background: var(--panel);
  padding: 1.1rem 1.15rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  text-decoration: none;
  color: var(--text);
  transition: background 0.12s;
  position: relative;
}
.module-card:hover { background: #1c2431; }
.module-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.5rem; }

/* Hex-container icon: two stacked clip-path hexagons (line-color base,
   panel-color inset) instead of a square/rounded icon box - echoes the
   hex-terminal panelling used throughout InterKnot's in-world UI.
   Accent color is set per-section via --module-accent on .module-grid. */
.module-card-icon {
  position: relative;
  width: 32px; height: 32px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  clip-path: polygon(25% 4%, 75% 4%, 100% 50%, 75% 96%, 25% 96%, 0% 50%);
  background: var(--line);
  color: var(--module-accent, var(--cyan));
}
.module-card-icon::before {
  content: '';
  position: absolute; inset: 1.5px;
  clip-path: polygon(25% 4%, 75% 4%, 100% 50%, 75% 96%, 25% 96%, 0% 50%);
  background: var(--panel);
}
.module-card:hover .module-card-icon::before { background: #1c2431; }
.module-card-icon svg {
  position: relative; z-index: 1;
  width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
.module-card-name { font-size: 0.88rem; font-weight: 600; }
.module-card-desc { font-size: 0.75rem; color: var(--text-dim); line-height: 1.5; flex: 1; }
.module-pill {
  align-self: flex-start;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.12rem 0.45rem;
  border: 1px solid var(--line);
  color: var(--text-dim);
}
.module-pill.on { color: var(--module-accent, var(--cyan)); border-color: var(--module-accent, var(--cyan)); }
.module-pill.off { color: var(--text-dim); }

/* Live count readout - replaces the floating corner badge for cards that
   have real pending/open data, so the number reads as part of the card
   face instead of a decorative notification dot. */
.module-card-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--module-accent, var(--pink));
  white-space: nowrap;
}
.module-card-count .n { font-size: 0.95rem; }

/* Per-section accent: applied on the .module-grid wrapper so every card
   and icon inside inherits the section's color without repeating it
   per-card in the markup. */
.module-grid.accent-mod { --module-accent: var(--pink); }
.module-grid.accent-community { --module-accent: var(--cyan); }
.module-grid.accent-admin { --module-accent: var(--amber); }
.module-grid.accent-casino { --module-accent: var(--highlight); }

/* Wide variant: for the one or two cards per section that actually carry a
   live number worth reading at a glance (open tickets, pending scores).
   Spans the full grid row instead of sitting in the tile grid, so the
   count reads like a stat rather than a corner badge on an equal-size
   square - the number IS the content here, not a decoration on it. */
.module-card--wide {
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: center;
  gap: 1.1rem;
}
.module-card--wide .module-card-top { flex: 0 0 auto; }
.module-card--wide .module-card-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}
.module-card--wide .module-card-figure {
  flex: 0 0 auto;
  text-align: right;
  font-family: 'JetBrains Mono', monospace;
  font-size: 1.9rem;
  font-weight: 700;
  line-height: 1;
  color: var(--module-accent, var(--pink));
}
.module-card--wide .module-card-figure .unit {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}
@media (max-width: 640px) {
  .module-card--wide { flex-wrap: wrap; }
  .module-card--wide .module-card-figure { text-align: left; flex-basis: 100%; }
}

/* ── AGENT CARD GRID (card game collection, with art) ─────────────────
   Replaces a plain text log-list of "name x count" with actual character
   art per owned agent, rarity color-coded along the top edge the same way
   faction rows are color-coded down the left edge. */
.agent-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(126px, 1fr));
  gap: 0.85rem;
  margin-bottom: 1.75rem;
}
.agent-card {
  border: 1px solid var(--line);
  border-top: 3px solid var(--rarity-color, var(--line));
  background: var(--void);
  overflow: hidden;
}
.agent-card-img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  background: var(--panel);
}
.agent-card-img-fallback {
  width: 100%;
  aspect-ratio: 3 / 4;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
  font-size: 1.1rem;
}
.agent-card-info { padding: 0.55rem 0.65rem 0.65rem; }
.agent-card-name {
  font-size: 0.72rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.agent-card-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 0.15rem;
}
.agent-card-rarity {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  color: var(--rarity-color, var(--text-dim));
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.agent-card-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  color: var(--text-dim);
}

/* ── ROSTER ROW (leaderboards with an avatar instead of just a name) ── */
.roster-row {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.7rem 1.1rem;
  border-bottom: 1px solid var(--line);
}
.roster-row:last-child { border-bottom: none; }
.roster-rank {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  color: var(--text-dim);
  width: 2.2ch;
  flex-shrink: 0;
}
.roster-rank--top { color: var(--highlight); font-weight: 700; }
.roster-avatar { width: 26px; height: 26px; border-radius: 3px; border: 1px solid var(--line); flex-shrink: 0; }
.roster-avatar-fallback {
  width: 26px; height: 26px; flex-shrink: 0;
  border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  color: var(--text-dim);
  background: var(--panel);
}
.roster-name { flex: 1; min-width: 0; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roster-info { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 0.1rem; }
.roster-info .roster-name { flex: none; }
.roster-sub {
  font-size: 0.7rem;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.roster-value { font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-dim); flex-shrink: 0; white-space: nowrap; }

/* ── SHARE BAR (rarity breakdown, faction share of total members) ────── */
.share-row { padding: 0.55rem 1rem; border-bottom: 1px solid var(--line); }
.share-row:last-child { border-bottom: none; }
.share-row-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
  font-size: 0.78rem;
}
.share-bar-track { height: 6px; background: var(--line); }
.share-bar-fill { height: 100%; background: var(--bar-color, var(--cyan)); min-width: 2px; }

/* ── BAR CHART (analytics 7-day trend) ─────────────────────────────── */
.bar-chart {
  display: flex;
  align-items: flex-end;
  gap: 0.6rem;
  height: 140px;
  padding: 0.5rem 0.25rem 0;
  border-bottom: 1px solid var(--line);
  margin-bottom: 1.75rem;
}
.bar-chart-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  height: 100%;
  gap: 0.4rem;
}
.bar-chart-count { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--text-dim); }
.bar-chart-bar {
  width: 100%;
  max-width: 34px;
  background: var(--cyan);
  min-height: 3px;
  opacity: 0.85;
}
.bar-chart-label { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; color: var(--text-dim); text-transform: uppercase; }

/* ── MOBILE SIDEBAR DRAWER ─────────────────────────────────────────── */
@media (max-width: 860px) {
  .hamburger { display: flex; }

  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    transform: translateX(-100%);
    transition: transform 0.2s ease;
    z-index: 20;
    width: 260px;
    box-shadow: 0 0 40px rgba(0,0,0,0.5);
  }
  .sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.55);
    z-index: 15;
  }
  .sidebar-toggle-input:checked ~ .sidebar-backdrop { display: block; }
  .sidebar-toggle-input:checked ~ .sidebar { transform: translateX(0); }

  .content { padding: 1.1rem; }
  .topbar { padding: 0.85rem 1.1rem; }
}

@media (max-width: 480px) {
  .content { padding: 0.9rem; }
  .content-inner .panel { padding: 1.1rem 0.9rem; }
  .module-grid { grid-template-columns: 1fr 1fr; }
  .hero img.identity-icon, .hero-fallback-icon { width: 44px; height: 44px; }
}

/* ── SETTINGS SECTIONS (collapsible per-game groups) ───────────────────
   The balance-settings form has 7+ games worth of fields - on a phone
   that's an endless scroll of inputs with no way to jump to just the one
   game you came to tune. <details>/<summary> gives each game its own
   closed-by-default drawer, no JS needed, and still degrades to a normal
   (if long) form if CSS fails to load. */
.settings-section {
  border: 1px solid var(--line);
  margin-bottom: 0.85rem;
  background: var(--panel);
}
.settings-section > summary {
  padding: 0.85rem 1.1rem;
  cursor: pointer;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text);
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.settings-section > summary::-webkit-details-marker { display: none; }
.settings-section > summary::after {
  content: '+';
  font-size: 1rem;
  color: var(--module-accent, var(--cyan));
  flex-shrink: 0;
}
.settings-section[open] > summary::after { content: '\2212'; }
.settings-section > summary:hover { background: #1c2431; }
.settings-section-body { padding: 0.3rem 1.1rem 1.1rem; }

/* ── CHIP SHOP (buy-Chips-with-real-money page) ─────────────────────── */
.chip-hero {
  display: flex;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  margin-bottom: 1.5rem;
}
.chip-hero-cell {
  flex: 1;
  background: var(--panel);
  padding: 1rem 1.2rem;
  text-align: center;
}
.chip-hero-cell .value { font-family: 'JetBrains Mono', monospace; font-size: 1.4rem; font-weight: 700; color: var(--highlight); }
.chip-hero-cell .value.dennies { color: var(--pink); }
.chip-hero-cell .label { font-size: 0.68rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.2rem; }

.pack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  margin-bottom: 1.75rem;
}
.pack-card {
  background: var(--panel);
  padding: 1.25rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  text-align: center;
  position: relative;
}
.pack-card.pack-card--best::before {
  content: 'BEST VALUE';
  position: absolute;
  top: 0; left: 0; right: 0;
  background: var(--highlight);
  color: var(--void);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-align: center;
  padding: 0.2rem;
}
.pack-card.pack-card--best { padding-top: 2rem; }
.pack-card-chips { font-family: 'JetBrains Mono', monospace; font-size: 1.3rem; font-weight: 700; color: var(--highlight); }
.pack-card-bonus { font-size: 0.7rem; color: var(--green); }
.pack-card-price { font-family: 'JetBrains Mono', monospace; font-size: 1.05rem; color: var(--text); margin-top: 0.2rem; }
.pack-card form { margin-top: 0.3rem; }
.pack-card .btn { width: 100%; }

.exchange-box {
  border: 1px solid var(--line);
  padding: 1.1rem 1.2rem;
  margin-bottom: 1.5rem;
}
.exchange-box .field-row { display: flex; gap: 0.6rem; align-items: flex-end; margin-bottom: 0; }
.exchange-box .field-row input { flex: 1; }

/* Standalone (non-dashboard-shell) page shell for the player-facing shop -
   regular members hitting /shop shouldn't see the owner's admin sidebar,
   so this reuses the plain statusbar + centered .panel look from home.ejs
   instead of the app-shell layout. */
.standalone-shell { width: 100%; max-width: 640px; }



/* ── COMMAND CHANNEL MATRIX ─────────────────────────────────────────────
   One row per channel, one checkbox column per bot section. Column count
   comes from --cc-cols (set inline from the sections list) so adding a
   section in utils/commandChannels.js adds a column with no CSS change. */
.cc-matrix { border: 1px solid var(--line); margin-bottom: 0.5rem; transition: opacity 0.15s; }
.cc-matrix--off { opacity: 0.55; }
.cc-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) repeat(var(--cc-cols, 3), 84px);
  gap: 0.5rem;
  align-items: center;
  padding: 0.55rem 0.9rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.82rem;
}
.cc-row:last-child { border-bottom: none; }
.cc-row[hidden] { display: none; }
.cc-head {
  align-items: end;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}
.cc-col-head { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; text-align: center; }
.cc-status { font-size: 0.58rem; letter-spacing: 0.04em; text-transform: none; color: var(--text-dim); opacity: 0.8; }
.cc-status--set { color: var(--cyan); opacity: 1; }
.cc-legend-desc { margin: 0; text-align: right; max-width: 70%; }
.cc-cell { padding: 0.25rem 0; }
.cc-cell input[type="checkbox"] { width: 18px; height: 18px; }

@media (max-width: 480px) {
  .cc-row { grid-template-columns: minmax(0, 1fr) repeat(var(--cc-cols, 3), 58px); gap: 0.25rem; padding: 0.55rem 0.6rem; }
  .cc-head { font-size: 0.58rem; letter-spacing: 0.03em; }
  .cc-legend-desc { text-align: left; max-width: 100%; }
}

/* ── BUILD CARD (per-agent Mindscape/W-Engine/Disk summary) ───────────
   Flat fill, sharp corners, no gradient - same rule as everything else
   in this file. One team = a row of 3, wraps to 1-per-line on mobile. */
.build-card-row {
  display: flex;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  flex-wrap: wrap;
  margin-top: 0.5rem;
}
.build-card {
  background: var(--panel);
  flex: 1 1 220px;
  min-width: 200px;
  display: flex;
  flex-direction: column;
}
.build-card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.85rem 0.9rem;
}
.build-card-name {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--text);
}
.build-card-mindscape {
  font-family: 'JetBrains Mono', monospace;
  color: var(--cyan);
  font-size: 0.85rem;
}
.build-card-subtitle {
  color: var(--text-dim);
  font-size: 0.75rem;
  margin-top: 0.25rem;
}
.build-card-portrait {
  width: 56px;
  height: 56px;
  border: 1px solid var(--line);
  object-fit: cover;
  flex-shrink: 0;
}
.build-card-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.6rem 0.9rem;
  border-top: 1px solid var(--line);
}
.build-card-engine {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}
.build-card-engine-icon {
  width: 32px;
  height: 32px;
  border: 1px solid var(--line);
  object-fit: cover;
  flex-shrink: 0;
}
.build-card-engine-name {
  font-size: 0.8rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.build-card-muted { color: var(--text-dim); }
.build-card-disks {
  display: flex;
  gap: 0.3rem;
  flex-shrink: 0;
}
.build-card-disk-icon {
  width: 26px;
  height: 26px;
  border: 1px solid var(--line);
  object-fit: cover;
}

/* ── DA BOSS BANNER (challenge page header) ─────────────────────────────
   Boss art as a full-height side panel rather than a small thumbnail
   pinned in the corner - runs the full height of whatever the name/HP/
   weakness/resistance column needs, with a cyan corner-bracket accent
   (the same "hazard terminal" targeting-reticle motif used elsewhere)
   instead of a gradient fade, since the design system is flat fills only.
   Still renders fine with no art at all - see bossImageUrl() in
   zzz/bossData.js (null until portraits are added; the img panel is
   omitted from the markup entirely in that case, content just takes the
   full width). */
.boss-banner {
  position: relative;
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line);
  background: var(--void);
  margin-bottom: 1rem;
  overflow: hidden;
}
.boss-banner-content {
  flex: 1 1 auto;
  min-width: 0;
  padding: 1rem 1.1rem;
}
.boss-banner-img {
  /* Art sits on the right, big, and melts into the card with a mask fade
     (a mask rather than a gradient overlay, so it works on any background
     colour and the art itself stays untouched). width is capped so the
     source image (~480px wide) is never stretched past its native size
     on wide screens - that's what keeps it sharp. The face sits in the top
     third of the portraits, so anchor the crop there. */
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(58%, 420px);
  background-size: cover;
  background-position: center 18%;
  background-repeat: no-repeat;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 60%);
  mask-image: linear-gradient(to right, transparent 0%, #000 60%);
  pointer-events: none;
}
.boss-banner--art { min-height: 190px; }
.boss-banner--art .boss-banner-content {
  position: relative;
  z-index: 1;
  max-width: 62%;
}
.boss-banner-name {
  margin: 0 0 0.4rem;
  font-size: 1.2rem;
  letter-spacing: 0.02em;
}
.boss-banner-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.2rem;
  font-size: 0.78rem;
}
/* Plain inline span by default, so the small-caps label and the
   larger-font element badges next to it (each with a 16px icon) align on
   their text baselines instead of a shared center - label and icon end up
   sitting at visibly different heights. inline-flex + center keeps "Weak"/
   "Resist" and every icon+name badge after it on one true visual line.
   flex-wrap lets extra badges (a 2nd/3rd weakness) drop to their own line
   as whole units when the banner's stats column is squeezed narrow next
   to the boss art (see .boss-banner--art .boss-banner-content's
   max-width above) - without it, this being a flex item nested inside
   another flex container (.boss-banner-stats) let some mobile browsers
   shrink it below its label/badge text's own minimum width and break
   mid-word ("Weak" -> "Wea"/"k") instead of wrapping between them. The
   white-space: nowrap on .boss-stat-label and .element-badge below is
   the other half of that fix - it makes a mid-word break impossible
   regardless of how narrow the available space gets. */
.boss-stat {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 0.2rem;
}
.boss-stat-label {
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.62rem;
  margin-right: 0.45rem;
  white-space: nowrap;
}
.element-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  margin-right: 0.4rem;
  white-space: nowrap;
}
.element-badge:last-child { margin-right: 0; }
.element-icon { width: 16px; height: 16px; }

/* ── DA CHALLENGE META (team / cost cap / required engines / deadline) ──
   Was a single run-on <p> with manual <br>/&nbsp; separators and inline
   icons crammed next to text - readable-ish on a wide screen but turned
   to soup on mobile. Rows + chips instead, matching .boss-banner-stats'
   label+badge pattern directly above this block. */
.challenge-meta {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0 0 1rem;
}
.challenge-meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.challenge-meta-label {
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.62rem;
  flex: 0 0 auto;
}
.challenge-meta-value { font-size: 0.85rem; }
.challenge-meta-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.challenge-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 0.15rem 0.55rem 0.15rem 0.15rem;
  background: var(--void);
  font-size: 0.8rem;
}
.challenge-chip-icon {
  width: 20px;
  height: 20px;
  border-radius: 2px;
  /* contain, not cover: W-Engine icons are wide/rectangular banners (see
     .agent-tile img above), so cropping them to a 20px square cut off
     the art on the sides. Agent portraits are already square, so contain
     renders identically to cover for those - safe for both uses of this
     chip. */
  object-fit: contain;
  background: var(--panel);
  flex-shrink: 0;
}
.challenge-chip-sub {
  color: var(--text-dim);
  font-size: 0.7rem;
  margin-left: 0.15rem;
}

@media (max-width: 480px) {
  .boss-banner--art { min-height: 170px; }
}

/* ── RUN CARDS (public DA leaderboard entries) ─────────────────────────
   Collapsed by default - rank, player, team portraits + mindscape chips
   and the score badge up front, everything else (full build cards, proof
   link, note) tucked behind a click. Native <details>/<summary> so it
   works with zero JS and stays keyboard/screen-reader accessible. */
.run-list { display: flex; flex-direction: column; gap: 0.6rem; border: none; }
.run-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}
.run-card[open] { border-color: var(--text-dim); }

.run-card-summary {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  padding: 1.1rem 1.25rem;
  cursor: pointer;
  list-style: none;
}
.run-card-summary::-webkit-details-marker { display: none; }
.run-card-summary::marker { content: ''; }

.run-card-summary-top { display: flex; align-items: center; gap: 0.6rem; }
.run-card-rank {
  color: var(--text-dim);
  font-size: 0.85rem;
  flex-shrink: 0;
}

.run-card-user { font-weight: 700; font-size: 1.15rem; overflow-wrap: anywhere; min-width: 0; margin-right: auto; }

/* #1 leaderboard name: animated rainbow gradient fill + neon glow, like a
   glowing marquee sign. Scoped to .run-card--top (rank 1 only, see the
   `i === 0` class on the leaderboard loop in assault-challenge.ejs) so it
   never fires for rank 2+ or anywhere else in the app. */
.run-card--top .run-card-user {
  display: inline-block;
  background: linear-gradient(90deg,
    #7ee8fa, #a78bfa, #f472b6, #fb923c, #facc15, #4ade80, #7ee8fa);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter:
    drop-shadow(0 0 4px rgba(167, 139, 250, 0.65))
    drop-shadow(0 0 10px rgba(244, 114, 182, 0.45));
  animation: run-card-top-shimmer 6s linear infinite;
}

@keyframes run-card-top-shimmer {
  to { background-position: 300% 0; }
}

/* #2 and #3: same gradient-fill-as-glow idea as #1, deliberately turned
   down a notch per rank - silver for #2, bronze for #3 - so the top 3
   read as a tapering podium instead of #1 being the only one that looks
   special and everyone else being flat white text. */
.run-card--second .run-card-user {
  display: inline-block;
  background: linear-gradient(90deg, #dfe4ee, #9aa3b8, #f2f4f8, #aab2c4, #dfe4ee);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 0 5px rgba(180, 190, 205, 0.5));
  animation: run-card-top-shimmer 9s linear infinite;
}

.run-card--third .run-card-user {
  display: inline-block;
  background: linear-gradient(90deg, #cd7f32, #e8b273, #b06a24, #cd7f32);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 0 3px rgba(205, 127, 50, 0.4));
  /* No shimmer animation at all - #3 is the quietest of the three, a
     static bronze fill rather than a moving one. */
}

@media (prefers-reduced-motion: reduce) {
  .run-card--top .run-card-user,
  .run-card--second .run-card-user {
    animation: none;
  }
}

.run-card-summary-main { min-width: 0; }
.run-card-team { display: flex; align-items: center; gap: 0.85rem; flex-wrap: wrap; }
.run-card-avatar-wrap { position: relative; display: inline-flex; flex-shrink: 0; }
.run-card-avatar {
  width: 58px;
  height: 58px;
  border-radius: 10px;
  border: 1px solid var(--line);
  object-fit: cover;
}
.run-card-mindscape {
  position: absolute; bottom: -6px; right: -6px;
  background: var(--void); color: var(--text);
  border: 1px solid var(--line);
  border-radius: 5px;
  font-family: 'JetBrains Mono', monospace; font-size: 0.62rem;
  line-height: 1; padding: 2px 4px;
}
.run-card-substats {
  color: var(--text-dim);
  font-size: 0.78rem;
  align-self: center;
  margin-left: 0.15rem;
}

.run-card-cost-row { display: flex; gap: 0.6rem; }
.run-card-cost-box {
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.35rem 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 5.5rem;
}
.run-card-cost-label {
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.6rem;
}
.run-card-cost-value {
  font-weight: 700;
  font-size: 1.05rem;
}

.run-card-summary-side { display: flex; align-items: center; gap: 0.6rem; flex-shrink: 0; }
.run-card-score {
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font-weight: 700;
  font-size: 1.35rem;
  font-family: 'JetBrains Mono', monospace;
  padding: 0.5rem 0.9rem;
  white-space: nowrap;
}
.run-card-chevron { color: var(--text-dim); font-size: 1.3rem; line-height: 1; transition: transform 0.2s ease-out; flex-shrink: 0; }
.run-card[open] .run-card-chevron { transform: rotate(180deg); }
/* set by motion.js while the body is animating shut - <details> keeps [open]
   until the animation ends, so without this the chevron would lag behind */
.run-card.run-card--closing .run-card-chevron { transform: none; }

.run-card-body { padding: 0.85rem 0.9rem 0.9rem; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 0.75rem; }
.run-card-footer { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.run-card-note { color: var(--text-dim); font-size: 0.78rem; }
.rules-list { margin: 0; padding-left: 1.2rem; display: flex; flex-direction: column; gap: 0.55rem; }
.rules-list li { color: var(--text); font-size: 0.85rem; line-height: 1.4; }
.report-inline-form { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.6rem; max-width: 420px; }
/* The [hidden] attribute is how toggleReportForm() (assault-challenge.ejs)
   shows/hides this form - it's set on the <form> in the markup so the
   form starts closed, and only the "Report run" button flips it open.
   Without this rule the flex display above wins over the browser's
   default [hidden]{display:none} (author CSS beats the UA stylesheet),
   so the form would render open on page load instead of waiting for the
   button click. */
.report-inline-form[hidden] { display: none; }
.report-inline-form textarea { resize: vertical; }

/* One row per agent inside an expanded run card - portrait, name +
   mindscape + W-Engine on a single line, disk icons at the end. Matches
   the compact per-character line of the in-game share card instead of
   the taller two-tier .build-card tiles used on the admin review queue. */
.run-agent-list { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.run-agent-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.65rem 0.8rem;
  border-bottom: 1px solid var(--line);
}
.run-agent-row:last-child { border-bottom: none; }
.run-agent-avatar { width: 42px; height: 42px; border: 1px solid var(--line); border-radius: 6px; object-fit: cover; flex-shrink: 0; }
.run-agent-info { flex: 1 1 auto; min-width: 0; }
.run-agent-name { font-weight: 600; font-size: 0.88rem; overflow-wrap: anywhere; }
.run-agent-mindscape { color: var(--text); font-family: 'JetBrains Mono', monospace; margin-right: 0.35rem; }
.run-agent-engine {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--text-dim);
  font-size: 0.76rem;
  margin-top: 0.15rem;
  overflow-wrap: anywhere;
}
.run-agent-engine-icon { width: 18px; height: 18px; border: 1px solid var(--line); border-radius: 4px; object-fit: contain; background: var(--panel); flex-shrink: 0; }
.run-agent-disks { display: flex; gap: 0.3rem; flex-shrink: 0; }
.run-agent-disk-icon { width: 28px; height: 28px; border: 1px solid var(--line); border-radius: 5px; object-fit: cover; }

@media (max-width: 480px) {
  .run-card-summary { padding: 0.9rem 1rem; }
  .run-card-user { font-size: 1rem; }
  .run-card-avatar { width: 46px; height: 46px; }
  .run-card-score { font-size: 1.05rem; padding: 0.4rem 0.65rem; }
  .run-agent-avatar { width: 36px; height: 36px; }
  .run-agent-disks { gap: 0.2rem; }
  .run-agent-disk-icon { width: 22px; height: 22px; }
}

/* ── ROUNDED CORNERS, SCOPED TO THE DA CHALLENGE PAGE ────────────────────
   The rest of the dashboard runs the sharp-cornered "hazard terminal"
   look on purpose (see the design-system note up top: flat panels, 90°
   corners, bracket accents instead of radius). This page asked to break
   from that specifically, so it's scoped under body.page-assault-challenge
   rather than changing .panel/.btn/etc. globally and dragging every other
   page along with it. The bracket-corner accents on .panel don't read
   right against a rounded edge, so they're dropped here in favor of a
   plain rounded border - everything else just gains a matching radius. */
.page-assault-challenge .panel { border-radius: 14px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35); }
.page-assault-challenge .panel::before,
.page-assault-challenge .panel::after { display: none; }
.page-assault-challenge .boss-banner { border-radius: 12px; }
.page-assault-challenge .run-card { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); }
.page-assault-challenge .empty-state { border-radius: 10px; }
.page-assault-challenge .notice { border-radius: 8px; }
.page-assault-challenge .alert { border-radius: 8px; }
.page-assault-challenge .btn,
.page-assault-challenge .btn-small { border-radius: 8px; }
.page-assault-challenge .avatar-sm { border-radius: 5px; }
.page-assault-challenge .element-icon { border-radius: 3px; }
.page-assault-challenge .challenge-chip { border-radius: 8px; }
.page-assault-challenge .challenge-chip-icon { border-radius: 5px; }
.page-assault-challenge input[type="text"],
.page-assault-challenge input[type="number"],
.page-assault-challenge input[type="date"],
.page-assault-challenge input[type="datetime-local"],
.page-assault-challenge input[type="url"],
.page-assault-challenge input[type="search"],
.page-assault-challenge select,
.page-assault-challenge textarea { border-radius: 7px; }
.page-assault-challenge .agent-picker-trigger { border-radius: 8px; }
.page-assault-challenge .agent-picker-avatar { border-radius: 6px; }
.page-assault-challenge .agent-picker-overlay-close { border-radius: 8px; }
.page-assault-challenge .agent-picker-required-tag { border-radius: 6px; }
.page-assault-challenge .agent-tile { border-radius: 8px; }
.page-assault-challenge .agent-tile img,
.page-assault-challenge .agent-tile-noimg { border-radius: 6px; }
.page-assault-challenge .report-inline-form textarea { border-radius: 7px; }

/* ── NEW-SUBMITTER CHECKLIST ──────────────────────────────────────────────
   A lightweight three-line reminder shown inline above the Submit button,
   only to someone who has never had a run on this league before (see
   isNewSubmitter in dashboard/assaultMember.js). Deliberately quieter than
   .notice and separate from both the Rules page and the full "Submission
   rules" overlay - just enough to catch the handful of most-missed proof
   requirements before a first-timer wastes a review slot on them. */
.new-submitter-checklist {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.75rem 1rem;
}
.new-submitter-checklist-title {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  color: var(--text-dim);
}
.new-submitter-checklist-list {
  margin: 0;
  padding-left: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.new-submitter-checklist-list li {
  font-size: 0.8rem;
  color: var(--text);
}

/* ── DA CHALLENGE PAGE: TONED-DOWN ACCENTS ────────────────────────────────
   Same scoping idea as the rounded-corners block above. The rest of the
   dashboard keeps the terminal look (mono uppercase labels, pink/cyan
   highlights, glow); this page drops it for plain sentence-case Inter
   and one neutral "selected" treatment instead of pink/cyan:
     - labels are sentence case, no tracking, no trailing rule lines
     - selected chips/tiles are a light neutral fill, not a pink outline
     - hover/focus are a lighter border, not cyan
     - no monospace, no scanline texture
   Red and green stay on error/success notices - those carry meaning. */
.page-assault-challenge {
  --sel: #E6EAF0;          /* selected fill */
  --sel-ink: #0B0F14;      /* text on the selected fill */
  --hover-line: #4B5A70;   /* border on hover/focus */
  --label: #B4BDCA;
}
.page-assault-challenge::before { display: none; }
.page-assault-challenge a:focus-visible,
.page-assault-challenge button:focus-visible { outline-color: var(--sel); }

/* type: Inter only, numbers stay aligned */
.page-assault-challenge .mono,
.page-assault-challenge .run-card-score,
.page-assault-challenge .run-card-mindscape,
.page-assault-challenge .run-agent-mindscape,
.page-assault-challenge .chip,
.page-assault-challenge .btn,
.page-assault-challenge .notice,
.page-assault-challenge .alert,
.page-assault-challenge .agent-filter-chip,
.page-assault-challenge .agent-filter-label,
.page-assault-challenge .agent-filter-clear,
.page-assault-challenge textarea {
  font-family: 'Inter', system-ui, sans-serif;
  font-variant-numeric: tabular-nums;
}

/* labels */
.page-assault-challenge .field-row label {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--label);
  text-transform: none;
  letter-spacing: 0;
  margin-bottom: 0.45rem;
}
.page-assault-challenge .section-label {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
  text-transform: none;
  letter-spacing: 0;
}
.page-assault-challenge .section-label:not(.past-challenges-toggle)::after { display: none; }
.page-assault-challenge .challenge-meta-label,
.page-assault-challenge .boss-stat-label,
.page-assault-challenge .run-card-cost-label {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.75rem;
  color: var(--label);
  text-transform: none;
  letter-spacing: 0;
}
.page-assault-challenge .boss-banner-name {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 1.4rem;
  font-weight: 700;
  letter-spacing: 0;
}

/* per-agent heading in the submit form */
.agent-head {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.agent-head .player-header-avatar { width: 44px; height: 44px; border-radius: 10px; }
.agent-head-name { font-size: 1.05rem; font-weight: 600; }
.agent-head-class { color: var(--text-dim); font-size: 0.85rem; }

/* chips: one neutral selected state */
.page-assault-challenge .chip {
  font-size: 0.82rem;
  font-weight: 500;
  min-width: 2.75rem;
  padding: 0.45rem 0.7rem;
  text-align: center;
  border-radius: 8px;
  background: transparent;
}
.page-assault-challenge .chip:hover { border-color: var(--hover-line); color: var(--text); }
/* The selected look itself (color/border/background) is set once, below,
   by the accent-aware block further down this file - it also covers the
   hover state, since a chip that's already selected shouldn't flip to a
   different fill just because the pointer is resting on it. This used to
   duplicate that with a plain white/black --sel style scoped to :hover
   only, which - at equal specificity - won the fill and text color back
   from the accent block while the accent block's border color still won,
   leaving a selected chip flash white-with-a-red-border on hover. */

/* pickers */
.page-assault-challenge .agent-picker-trigger { padding: 0.5rem 0.7rem; }
.page-assault-challenge .agent-picker-trigger:hover,
.page-assault-challenge .agent-picker-trigger:focus,
.page-assault-challenge .agent-picker-trigger.is-open { border-color: var(--hover-line); }
.page-assault-challenge .agent-picker-trigger--locked:hover { border-color: var(--line); }
.page-assault-challenge .agent-picker-caret {
  font-size: 0;
  width: 7px;
  height: 7px;
  margin-right: 3px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
}
.page-assault-challenge .agent-picker-required-tag {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.72rem;
  text-transform: none;
  letter-spacing: 0;
  margin-left: 0.5rem;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
}

/* fields */
.page-assault-challenge input[type="text"]:focus,
.page-assault-challenge input[type="number"]:focus,
.page-assault-challenge input[type="date"]:focus,
.page-assault-challenge input[type="datetime-local"]:focus,
.page-assault-challenge input[type="url"]:focus,
.page-assault-challenge input[type="search"]:focus,
.page-assault-challenge select:focus,
.page-assault-challenge textarea:focus { border-color: var(--hover-line); }

/* picker overlays */
.page-assault-challenge .agent-picker-overlay-title {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 1rem;
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
}
.agent-picker-overlay-sub { color: var(--text-dim); font-weight: 400; margin-left: 0.35rem; }
.page-assault-challenge .agent-picker-overlay-close:hover,
.page-assault-challenge .agent-picker-overlay-close:focus { border-color: var(--hover-line); color: var(--text); }
.page-assault-challenge .agent-filter-label,
.page-assault-challenge .agent-filter-chip {
  font-size: 0.75rem;
  text-transform: none;
  letter-spacing: 0;
}
.page-assault-challenge .agent-filter-chip { border-radius: 999px; padding: 0.3rem 0.7rem; }
.page-assault-challenge .agent-filter-chip:hover { border-color: var(--hover-line); }
.page-assault-challenge .agent-filter-chip.is-active {
  background: var(--sel);
  border-color: var(--sel);
  color: var(--sel-ink);
}
.page-assault-challenge .agent-filter-clear:hover { color: var(--text); }
.page-assault-challenge .agent-tile:hover:not(:disabled),
.page-assault-challenge .agent-tile:focus:not(:disabled) { border-color: var(--hover-line); }
.page-assault-challenge .agent-tile.is-selected {
  border-color: var(--sel);
  background: rgba(230, 234, 240, 0.08);
}

/* buttons and notices */
.page-assault-challenge .btn {
  font-size: 0.85rem;
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
}
.page-assault-challenge .btn-small { font-size: 0.8rem; padding: 0.45rem 0.8rem; }
.page-assault-challenge .btn:hover { border-color: var(--hover-line); color: var(--text); background: rgba(255, 255, 255, 0.04); }
.page-assault-challenge .btn-link { font-family: 'Inter', system-ui, sans-serif; }
.page-assault-challenge .btn-link:hover { color: var(--text); }
.page-assault-challenge .notice,
.page-assault-challenge .alert { font-size: 0.85rem; }
.page-assault-challenge .notice:not(.notice--error) {
  color: var(--text);
  border-color: var(--line);
  background: rgba(255, 255, 255, 0.03);
}
/* success stays neutral like the rest of the page - just a thin green edge */
.page-assault-challenge .notice--success { border-left: 3px solid var(--green); }
.page-assault-challenge code { color: var(--text); }

/* ── DA ADMIN + LIST PAGES: SAME LOOK AS THE PUBLIC CHALLENGE PAGE ────────
   /server/assault (staff), /assault (list) and /assault/mine all carry
   body.page-assault-challenge now, so the rounded / neutral-accent block
   further up applies to them too. What's below is only the pieces those
   pages have that the challenge page doesn't: challenge cards, the
   pending/reported review cards, and dashboard-shell adjustments. */

/* Originally the shell (sidebar/topbar) kept the terminal look even here
   and only the page content underneath was themed calm - see the removed
   comment in the description of the rules above. Revisited: staff spend
   most of their time on this page reviewing/approving runs, so the frame
   around it should match instead of clashing with it. Deleting the old
   "force mono back" rule alone hands the sidebar/topbar mono text over to
   the earlier `.page-assault-challenge .mono { font-family: Inter }` rule
   automatically (it's an unscoped descendant selector, so it already
   reaches in here); everything below is the color/glow/shape pieces that
   rule doesn't touch. Revisited again: the brand mark was left cyan under
   the idea that a logo is identity, not a terminal accent - but sitting
   next to the now-neutral guild card and nav, that lone cyan tile just
   reads as a leftover terminal accent, not identity. Folded into the same
   neutral treatment as the guild icon's fallback tile (void fill, thin
   --line border) so the whole shell reads as one page. Scoped to
   .page-assault-challenge only, so the rest of the dashboard (still being
   reworked) is untouched. */
.dashboard-body.page-assault-challenge .sidebar-brand::after { background: var(--line); }
/* brand-mark's diagonal clip-path cut is the same hazard-terminal shape
   language as the old .panel bracket corners - dropped in favor of a
   plain rounded tile so the very first thing in the sidebar doesn't
   still read as the sharp-cornered dashboard next to a page that's
   rounded everywhere else. */
.dashboard-body.page-assault-challenge .brand-mark {
  background: var(--void);
  border: 1px solid var(--line);
  color: var(--text);
  clip-path: none;
  border-radius: 8px;
}
.dashboard-body.page-assault-challenge .guild-icon,
.dashboard-body.page-assault-challenge .guild-icon--fallback { border-radius: 6px; }
.dashboard-body.page-assault-challenge .hamburger { border-radius: 8px; }
/* nav-link's base 3px radius is a leftover from the sharp-cornered
   sidebar everywhere else - bumped to match the 6-14px scale the rest
   of this page's rounded elements (panel/btn/da-tab/agent-tile) use. */
.dashboard-body.page-assault-challenge .nav-link { border-radius: 8px; }
.dashboard-body.page-assault-challenge .guild-status .dot {
  background: var(--hover-line);
  box-shadow: none;
}
/* The topbar's own "uplink established" readout is gone on this page now
   (see topbar.ejs - it shows the settings/Ko-fi icons instead), but the
   sidebar's guild-status dot ("Online"/"Reconnecting") is still here, so
   it gets the same accent treatment: flat green with no glow by default,
   picks up the chosen accent (with its matching glow) once one's set,
   instead of staying a fixed color of its own regardless of what a
   reviewer picked. */
.dashboard-body.page-assault-challenge .guild-status .dot.online {
  background: var(--accent, var(--green));
  box-shadow: 0 0 6px var(--accent, transparent);
}
.dashboard-body.page-assault-challenge .nav-group-label { color: var(--label); letter-spacing: 0.03em; }
.dashboard-body.page-assault-challenge .nav-link.active {
  color: var(--sel-ink);
  background: var(--sel);
  border-left-color: var(--sel);
}
.dashboard-body.page-assault-challenge .nav-badge { background: var(--sel); color: var(--sel-ink); font-weight: 600; }
.dashboard-body.page-assault-challenge .user-logout:hover { color: var(--text); border-color: var(--hover-line); }
.dashboard-body.page-assault-challenge .ask-bot-form:focus-within { border-color: var(--hover-line); }
.dashboard-body.page-assault-challenge .ask-bot-submit:hover { color: var(--text); }
.dashboard-body.page-assault-challenge .content-inner .panel { border-radius: 14px; }

/* forms: rounded, and don't stack three borders/paddings on a phone */
.page-assault-challenge .settings-form { border-radius: 12px; }
.page-assault-challenge .agent-picker-trigger--locked { border-radius: 8px; }

/* challenge / submission cards - auto-fit grid instead of a fixed single
   column: naturally stays 1-up on a phone (container narrower than one
   280px card), goes 2-3 up as the panel gets wider on tablet/desktop.
   align-items: start (not grid's stretch default) so a short card next to
   a taller one keeps its own height instead of stretching to match. */
.da-card-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  align-items: start;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}
.da-card {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem 1.1rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
  color: inherit;
  text-decoration: none;
  min-width: 0;
}
a.da-card:hover { border-color: var(--hover-line, #4B5A70); }
/* Column, not row: with row + wrap, a short boss name (e.g. "Kusarikku")
   left enough space for the deadline to sit beside it instead of
   wrapping underneath like it does for longer names - the date's
   position shouldn't depend on how long the boss name happens to be.
   Stacking unconditionally keeps it under the title every time. */
.da-card-top { display: flex; flex-direction: column; gap: 0.15rem; }
.da-card-title { font-weight: 700; font-size: 1.1rem; overflow-wrap: anywhere; min-width: 0; }
.da-card-sub { color: var(--text-dim); font-size: 0.78rem; }
.da-card-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.da-card-actions form { display: inline; margin: 0; }

/* Small boss-art thumbnail on a challenge card (partials/daCard.ejs) -
   the same masked-fade idea as .boss-banner-img on the challenge page
   itself, just narrow and faded harder so it reads as a bit of color
   and texture behind a compact list row instead of a second hero image
   fighting the one you see after you click through. Falls back to a
   plain card automatically when a boss has no art set (no .da-card--art
   class, no .da-card-img element at all - see the ejs). */
.da-card--art { position: relative; overflow: hidden; }
.da-card--art > .da-card-top,
.da-card--art > .challenge-meta-chips,
.da-card--art > .da-card-sub { position: relative; z-index: 1; }
.da-card-img {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(46%, 140px);
  background-size: cover;
  background-position: center 15%;
  background-repeat: no-repeat;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 80%);
  mask-image: linear-gradient(to right, transparent 0%, #000 80%);
  pointer-events: none;
}
@media (max-width: 480px) {
  .da-card-img { width: min(52%, 115px); }
}

/* ── PAST CHALLENGES DISCLOSURE (assault.ejs) ───────────────────────────
   Closed by default (see the comment above the <details> markup in
   assault.ejs for why) - a plain .section-label reads as static text, so
   once it's also the toggle it needs to actually look clickable: a
   pointer cursor and a +/- marker alongside the divider line
   .section-label already draws via ::after.
   That divider is generated content, and generated content always
   renders after an element's real children in source order - so a
   trailing marker element would land *before* the line, not after it.
   Explicit `order` on both is what puts the line first and the marker
   last regardless of that source order. */
.past-challenges-toggle {
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.past-challenges-toggle::-webkit-details-marker { display: none; }
.past-challenges-toggle::marker { content: ''; }
.past-challenges-toggle:hover { color: var(--accent, var(--cyan)); }
.past-challenges-toggle::after { order: 1; }
.past-challenges-marker {
  order: 2;
  flex: 0 0 auto;
  margin-left: 0.6rem;
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.95rem;
}
.past-challenges-marker::before { content: '+'; }
.past-challenges[open] .past-challenges-marker::before { content: '\2212'; }
.past-challenges-toggle:hover .past-challenges-marker { color: var(--accent, var(--cyan)); }
/* Resting state, not just :hover - same fix as .league-bar-icon below:
   without this, a picked accent only ever flashed on this heading while
   actively hovered/tapped, so it looked like the theme wasn't sticking.
   Falls back to the toggle's normal colors (inherited --text, and the
   marker's --text-dim) when no accent is picked. */
.page-assault-challenge .past-challenges-toggle { color: var(--accent, var(--text)); }
.page-assault-challenge .past-challenges-marker { color: var(--accent, var(--text-dim)); }
.past-challenges-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-dim);
  background: rgba(255, 255, 255, 0.07);
  border-radius: 999px;
  padding: 0.05rem 0.55rem;
}
.past-challenges-search-wrap { margin: 0.9rem 0; }
.past-challenges-search { max-width: 320px; }
.past-challenges-empty { margin-top: 0; }

/* review card pieces (partials/assaultRunBody.ejs) */
.da-sub-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.da-sub-who { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.da-sub-user { font-weight: 700; font-size: 1.1rem; overflow-wrap: anywhere; }
.da-sub-boss { color: var(--text-dim); font-size: 0.8rem; overflow-wrap: anywhere; }
.da-sub-score { flex-shrink: 0; }
.da-sub-meta { display: flex; align-items: center; gap: 0.4rem 0.5rem; flex-wrap: wrap; }
.da-sub-pill {
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.3rem 0.6rem;
  color: var(--text-dim);
  font-size: 0.78rem;
  white-space: nowrap;
}
.da-sub-pill strong { color: var(--text); font-weight: 700; margin-left: 0.2rem; }
/* My submissions summary: NEPL look, not the dashboard's amber/pink/green.
   Neutral pill, number in the viewer's chosen accent (falls back to the
   neutral --sel), same rule chips and tiles on these pages follow. */
.sub-summary .da-sub-pill strong { color: var(--accent, var(--sel, var(--text))); }
.da-sub-proof { margin-left: auto; }
.da-card .run-agent-list { background: var(--void); }
.da-card .run-agent-name { min-width: 0; }
/* long W-Engine names wrap inside their own column instead of running
   underneath the disk icons */
.run-agent-engine-name { min-width: 0; overflow-wrap: anywhere; }
.run-agent-engine-icon { flex-shrink: 0; }
/* W-Engine and disk art are not square - show the whole thing */
.da-card .run-agent-disk-icon { object-fit: contain; background: var(--panel); }
.da-reports { display: flex; flex-direction: column; gap: 0.4rem; padding: 0.7rem 0.8rem; border: 1px solid var(--line); border-radius: 8px; background: var(--void); }
.da-reports-title { font-size: 0.75rem; color: var(--label, var(--text-dim)); font-weight: 500; }
.da-report { font-size: 0.82rem; line-height: 1.45; overflow-wrap: anywhere; }
.da-report-time { display: block; color: var(--text-dim); font-size: 0.72rem; margin-top: 0.1rem; }
.da-card .score-card-actions { margin-top: 0; }
.page-assault-challenge .reject-form input[type="text"] { min-width: 0; }

@media (max-width: 480px) {
  .dashboard-body.page-assault-challenge .content-inner .panel { padding: 1rem 0.8rem; }
  .page-assault-challenge .settings-form { padding: 0.9rem 0.75rem; }
  .da-card { padding: 0.9rem 0.85rem; }
  .da-sub-score { font-size: 1.05rem; padding: 0.4rem 0.65rem; }
  .da-sub-proof { margin-left: 0; }
  .da-card .run-agent-row { padding: 0.6rem 0.65rem; gap: 0.6rem; }
}


/* ── DA ADMIN: boss trigger art, toast placement, section tabs ───────── */

/* Boss art is full-bleed portrait art; the shared picker avatar uses
   "contain", which letterboxes it with bars down the sides. */
#bossTrigger .agent-picker-avatar,
[id^="rbBossTrigger"] .agent-picker-avatar { background-size: cover; }

/* Toasts used to sit at top-right, directly over the topbar's
   "uplink established" readout. Inside the dashboard shell they now
   float bottom-center on phones and drop below the topbar on desktop. */
.dashboard-body .toast-stack {
  top: 4.75rem;
  right: 1.25rem;
}
.dashboard-body .toast { margin: 0 0 0 auto; width: max-content; max-width: 100%; }
@media (max-width: 768px) {
  .dashboard-body .toast-stack {
    top: auto;
    right: auto;
    left: 50%;
    bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
    transform: translateX(-50%);
    width: min(92vw, 380px);
    align-items: center;
  }
  .dashboard-body .toast { margin: 0 auto; transform: translateY(8px); }
  .dashboard-body .toast.toast--in { transform: translateY(0); }
  .dashboard-body .toast.toast--out { transform: translateY(8px); }
}

/* section tabs (create / pending / reported / challenges) */
.da-tabs {
  display: flex;
  gap: 0.35rem;
  margin: 0 0 1.25rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
}
.da-tabs::-webkit-scrollbar { display: none; }
.da-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  flex-shrink: 0;
  padding: 0.5rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text-dim);
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}
.da-tab:hover { border-color: var(--hover-line, #4B5A70); color: var(--text); }
.da-tab--active { background: var(--sel, #E6EAF0); border-color: var(--sel, #E6EAF0); color: var(--sel-ink, #0B0F14); }
.da-tab--active:hover { color: var(--sel-ink, #0B0F14); }

/* Two-level tabs (Proxy League review page). Top row = areas, equal-width so
   all fit on a phone; second row = that area's tabs, lighter (underline, not
   a filled pill) so it's clear which row is the parent. */
.da-tabs--groups { overflow-x: visible; margin-bottom: 0.5rem; padding-bottom: 0; border-bottom: 0; }
.da-tabs--groups .da-tab { flex: 1 1 0; justify-content: center; }
.da-tabs--sub { flex-wrap: wrap; gap: 0.25rem 0.35rem; padding-bottom: 0.25rem; }
.da-tabs--sub .da-tab { padding: 0.4rem 0.6rem; border-color: transparent; border-radius: 0; font-size: 0.82rem; }
.da-tabs--sub .da-tab:hover { border-color: transparent; }
.da-tabs--sub .da-tab--active,
.da-tabs--sub .da-tab--active:hover {
  background: transparent; color: var(--text);
  box-shadow: inset 0 -2px 0 var(--accent, var(--sel, #E6EAF0));
}
.da-tabs--sub .da-tab--active .da-tab-count { background: var(--void); color: var(--text-dim); }
.da-tabs--sub .da-tab--active .da-tab-count--alert { background: var(--pink); color: #fff; }
.da-tab-count {
  min-width: 1.25rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: var(--void);
  color: var(--text-dim);
  font-size: 0.72rem;
  text-align: center;
}
.da-tab-count--alert { background: var(--pink); color: #fff; }
.da-tab--active .da-tab-count { background: rgba(11, 15, 20, 0.15); color: var(--sel-ink, #0B0F14); }
.da-tab--active .da-tab-count--alert { background: var(--pink); color: #fff; }

/* progressive-disclosure sections in the create form: [hidden] must win
   over the display rules on .field-row / .field-subsection, and revealed
   sections ease in instead of popping. */
.da-reveal[hidden] { display: none !important; }
.da-reveal:not([hidden]) { animation: da-reveal-in 0.22s ease both; }
@keyframes da-reveal-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .da-reveal:not([hidden]) { animation: none; }
}

/* ── PROXY LEAGUE HEADER (member pages) ────────────────────────────────────
   Replaces the terminal statusbar on /assault*: plain Inter, neutral colors,
   no glow or cursor. Same 640px width as .panel so the two line up. */
.league-bar {
  width: 100%;
  max-width: 640px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1.1rem;
  position: relative; /* z-index below was a no-op without this - see leagueBar.ejs FAQ panel */
  z-index: 20;
  font-family: 'Inter', system-ui, sans-serif;
}
.league-bar-name {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
}
.league-bar-name:hover { color: #FFFFFF; text-decoration: underline; text-underline-offset: 3px; }
/* Accent-aware: same "apply --accent once picked, --text otherwise" rule
   every other accent-aware element on these pages uses (e.g.
   .past-challenges-toggle below) - previously this only swapped in
   var(--accent) on :hover, so the title stayed plain white until you
   pressed/held it, then flashed the accent color and reverted. Now the
   accent (if any) is the name's actual color all the time; hover just
   adds the underline, it no longer changes what color you're looking at. */
.page-assault-challenge .league-bar-name { color: var(--accent, var(--text)); }
.page-assault-challenge .league-bar-name:hover { text-decoration: underline; text-underline-offset: 3px; }
.league-bar-tag {
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.1rem 0.42rem;
}
.league-bar-donate {
  margin-left: auto;
  font-size: 0.8rem;
  color: var(--text-dim);
  text-decoration: none;
}
.league-bar-donate:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.league-bar-user-row {
  width: 100%;
  max-width: 640px;
  margin: -0.7rem 0 1.1rem;
  display: flex;
  justify-content: flex-end;
}
.league-bar-icons {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.85rem;
  position: relative;
}
/* Signed-in user's name + role badge (role badge system) - its own row
   below the icons (see .league-bar-user-row and leagueBar.ejs) rather
   than inside .league-bar-icons, so name + Beta tag + every icon can
   share one line without the username's width competing for the same
   space. min-width: 0 lets the name below actually shrink/truncate
   instead of forcing the whole bar wider than its 640px cap on a long
   display name. */
.league-bar-user {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: var(--text-dim);
  min-width: 0;
}
.league-bar-user-name {
  max-width: 140px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.page-assault-challenge .league-bar-user { color: var(--accent, var(--text-dim)); }
.league-bar-icon {
  display: inline-flex;
  color: var(--text-dim);
  cursor: pointer;
  list-style: none;
}
/* <details> is a block box, so the <summary class="league-bar-icon"> inside
   it (the FAQ icon) lays out as an inline-flex box on its own anonymous
   line - that adds a font-metrics strut the plain <a class="league-bar-icon">
   icons next to it don't have, making the FAQ icon a few px taller and
   pushed up relative to Discord/Ko-fi. Turning the <details> itself into a
   tight-fitting flex box removes that strut so all three match. */
.league-bar-faq {
  display: inline-flex;
  align-items: center;
}
.league-bar-icon::-webkit-details-marker { display: none; }
.league-bar-icon svg {
  width: 19px;
  height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
}
/* Discord/Ko-fi marks are solid brand logos, not outline strokes - the
   base rule above is written for the FAQ icon's outline style, so these
   need their own fill/stroke back or the base rule blanks them out. */
.league-bar-icon svg[stroke="none"] { fill: currentColor; stroke: none; }
.league-bar-icon:hover { color: var(--text); }
.league-bar-faq[open] .league-bar-icon { color: var(--text); }

/* ── PROXY LEAGUE FOOTER (member pages) ─────────────────────────────────
   Privacy/Terms, moved out of the header bar. Same 640px width/centering
   as .panel and .league-bar so it lines up with everything above it.
   Sits in normal flow right after the page's content (not pinned to the
   viewport bottom via margin-top: auto) - on a short page like a fresh
   /assault with no past challenges yet, "auto" left a large empty gap
   above it and made it read as orphaned rather than as a footer. The
   top border gives it its own visual band instead. */
.site-footer {
  width: 100%;
  max-width: 640px;
  margin: 2rem 0 0;
  padding-top: 1.1rem;
  border-top: 1px solid var(--line);
  font-size: 0.72rem;
  color: var(--text-dim);
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}
.site-footer-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.site-footer-bottom {
  justify-content: space-between;
  gap: 0.75rem;
}
.site-footer a { color: inherit; text-decoration: none; }
.site-footer a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }
.page-assault-challenge .site-footer a { color: var(--accent, var(--cyan)); }
.page-assault-challenge .site-footer a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* Cookie Preferences was an unstyled native <button> - default grey/white
   browser chrome next to the copyright text is what made this look broken
   compared to the rest of the site. Give it the same bordered/mono
   treatment as every other secondary button (.btn-small). */
.site-footer-cookie-btn {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.45rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text-dim);
  background: var(--panel);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
  white-space: nowrap;
}
.site-footer-cookie-btn:hover {
  border-color: var(--cyan);
  color: var(--cyan);
  background: rgba(0, 217, 255, 0.06);
}
.site-footer-cookie-btn:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}
.page-assault-challenge .site-footer-cookie-btn:hover {
  border-color: var(--accent, var(--cyan));
  color: var(--accent, var(--cyan));
  background: transparent;
}

.site-footer-copyright { white-space: nowrap; }

@media (max-width: 480px) {
  .site-footer { font-size: 0.7rem; }
  /* Stack button above copyright instead of letting them wrap mid-line
     against each other - this is the exact layout from the screenshot. */
  .site-footer-bottom {
    flex-direction: column;
    align-items: flex-start;
  }
  .site-footer-copyright { white-space: normal; }
}

/* Custom tooltip for the Discord/Ko-fi icons, on data-tooltip rather than
   the native title attribute - title doesn't show on a mobile hold at all
   on iOS Safari, and is inconsistent elsewhere, so it can't be relied on
   for "hold to see what this does". :active covers touch-and-hold (shows
   while the finger stays down, hides on release/lift), :hover/:focus-visible
   cover desktop and keyboard nav. */
.league-bar-icon[data-tooltip] { position: relative; }
.league-bar-icon[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  width: max-content;
  max-width: 220px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.55rem 0.7rem;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.76rem;
  line-height: 1.4;
  color: var(--text-dim);
  text-align: left;
  opacity: 0;
  transform: translateY(-3px);
  pointer-events: none;
  transition: opacity 0.12s, transform 0.12s;
  z-index: 30;
}
.league-bar-icon[data-tooltip]:hover::after,
.league-bar-icon[data-tooltip]:focus-visible::after,
.league-bar-icon[data-tooltip]:active::after {
  opacity: 1;
  transform: translateY(0);
}
.league-bar-faq-panel {
  position: absolute;
  top: calc(100% + 0.6rem);
  right: 0;
  width: min(340px, 82vw);
  background: var(--panel-bg, #111);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0.9rem 1rem;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--text-dim);
  /* Above the icon tooltip's z-index: 30 (see [data-tooltip]::after below) -
     on mobile, :active tooltip state can stay "stuck" visible after a tap
     instead of clearing like a mouse hover would, and without this an open
     panel would render underneath that stuck tooltip instead of over it. */
  z-index: 40;
}
.league-bar-faq-panel p { margin: 0 0 0.8rem; }
.league-bar-faq-panel p:last-child { margin-bottom: 0; }
.league-bar-faq-panel strong { color: var(--text); font-weight: 600; }

/* ── LEAGUE BAR SETTINGS: ACCENT PICKER ──────────────────────────────────
   Off by default - the league pages keep their normal pink/neutral look
   until a visitor picks a color from the gear icon. Picking one sets
   --accent as an inline custom property on <html> (see the script at the
   bottom of leagueBar.ejs) and gets saved to localStorage per-visitor.
   Only the handful of rules at the bottom of this block read --accent,
   so nothing else here changes, and the admin "hazard terminal" dashboard
   never sees this variable at all. */
.league-bar-settings-panel { width: min(390px, 90vw); padding: 1.1rem 1.15rem; }
/* 390px, not the old 344px: the accent-grid below adds up to 336px on its
   own (5 circles + the double-wide custom square, plus 6 gaps) - at 344px
   with 1.15rem padding on each side, the panel only has ~307px of interior
   room, so the custom square overflowed the panel by ~29px on every
   viewport, desktop included. 390px leaves enough interior width
   (~353px) for the grid plus some breathing room. The narrow-screen
   media queries below already shrink --accent-swatch-size/--accent-gap
   (and re-shrink this panel's own padding at 360px) to fit smaller
   viewports, so they're untouched. */
/* All three of these are <p> tags inside the settings panel, which also
   carries .league-bar-faq-panel (shared FAQ-dropdown styling) - that
   panel's own ".league-bar-faq-panel p { margin: 0 0 0.8rem; }" rule
   above is more specific than a bare single class (two classes + an
   element beats one class), so it was quietly winning over the margins
   below and collapsing the "Reset to default" -> "Accessibility" gap to
   ~0. Scoping these to .league-bar-settings-panel brings each back up
   to (0,3,0) specificity so they actually apply. */
.league-bar-settings-panel .settings-panel-title {
  margin: 0 0 0.9rem;
  padding-bottom: 0.8rem;
  border-bottom: 1px solid var(--line);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
  font-size: 1.05rem;
}
.league-bar-settings-panel .settings-section-label {
  margin: 0 0 0.7rem;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--text-dim);
}

/* 5 circles across the top row and 5 more below; the custom-color swatch
   sits in an implicit 6th column and spans both rows (grid-row: 1 / 3 on
   .accent-swatch--custom below), so it renders as one tall square next to
   the two rows of circles rather than a small extra circle of its own.
   Sizing comes from --accent-swatch-size/--accent-gap so the whole grid
   (custom square included) can shrink together on narrow screens - and
   below 360px two of the ten circles drop out entirely (see that media
   query further down, after .accent-swatch--custom's base rule) rather
   than shrinking the circles past a comfortably tappable size. Below
   320px, circle-hiding and shrinking are both maxed out, so the custom
   swatch itself gives up its double-wide square treatment and becomes a
   plain circle instead (see the media query right after that one) -
   that's the piece that stops the grid from scaling down any further, so
   it's the one that changes shape rather than the panel just overflowing. */
.accent-grid {
  --accent-swatch-size: 36px;
  --accent-gap: 14px;
  display: grid;
  grid-template-columns: repeat(5, var(--accent-swatch-size)) calc(var(--accent-swatch-size) * 2 + var(--accent-gap));
  grid-template-rows: repeat(2, var(--accent-swatch-size));
  gap: var(--accent-gap);
  justify-content: space-between;
  margin-bottom: 0.95rem;
}
@media (max-width: 420px) {
  .accent-grid { --accent-swatch-size: 30px; --accent-gap: 10px; }
}
@media (max-width: 360px) {
  .accent-grid {
    --accent-swatch-size: 26px;
    --accent-gap: 8px;
    /* One fewer circle column - the two hidden swatches below no longer
       need a 5th column to sit in. */
    grid-template-columns: repeat(4, var(--accent-swatch-size)) calc(var(--accent-swatch-size) * 2 + var(--accent-gap));
  }
  .league-bar-settings-panel { padding: 0.9rem 0.85rem; }
}
.accent-swatch {
  width: var(--accent-swatch-size);
  height: var(--accent-swatch-size);
  border-radius: 50%;
  border: none;
  padding: 0;
  cursor: pointer;
  background: var(--swatch);
  box-shadow: 0 0 0 2px transparent;
  transition: box-shadow 0.12s, transform 0.12s;
}
.accent-swatch:hover { transform: scale(1.08); }
.accent-swatch--active { box-shadow: 0 0 0 2px var(--panel-bg, #111), 0 0 0 4px var(--ring, var(--swatch)); }
.accent-swatch--custom {
  grid-column: 6;
  grid-row: 1 / 3;
  width: calc(var(--accent-swatch-size) * 2 + var(--accent-gap));
  height: 100%;
  border-radius: 16px;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  background: var(--swatch, var(--line));
}
.accent-swatch--custom input[type="color"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.accent-swatch--custom svg { width: 17px; height: 17px; pointer-events: none; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6)); }
/* Placed after the base .accent-swatch--custom rule above on purpose -
   both set grid-column at the same specificity, so this only wins the
   cascade (under the media query below) because it comes later in the
   file. Amber and Ice white are the two that drop out; still reachable
   through the custom-color square for anyone who specifically wants
   them, same as any other color not in the ten presets. */
@media (max-width: 360px) {
  .accent-swatch[data-accent="#FF8A1E"],
  .accent-swatch[data-accent="#E8F6FF"] { display: none; }
  .accent-swatch--custom { grid-column: 5; }
}
/* Below 360px, hiding two circles and shrinking the rest bought some room,
   but the custom swatch itself never shrank - it's locked to two
   swatch-widths plus a gap no matter how small --accent-swatch-size gets,
   so it's the one piece of the grid that doesn't scale down with everything
   else. On the narrowest real phones that fixed width is what pushes the
   grid past the panel's interior edge. Rather than add a third round of
   circle-hiding (only two "extra" presets exist to drop; the other eight
   are the actual palette), the custom swatch instead drops its special
   double-wide treatment here and becomes a plain circle the same size as
   the rest - it keeps working exactly the same way (tap it, pick any
   color), it's just no longer visually the odd one out. That lets the grid
   go back to a single even column width instead of "5 circles + one wide
   one", which is what was overflowing. */
@media (max-width: 320px) {
  .accent-grid {
    --accent-swatch-size: 26px;
    --accent-gap: 8px;
    grid-template-columns: repeat(5, var(--accent-swatch-size));
    grid-template-rows: none;
  }
  .accent-swatch--custom {
    grid-column: auto;
    grid-row: auto;
    width: var(--accent-swatch-size);
    height: var(--accent-swatch-size);
    border-radius: 50%;
  }
  .accent-swatch--custom svg { width: 12px; height: 12px; }
  .league-bar-settings-panel { padding: 0.8rem 0.7rem; }
}
.accent-reset-btn {
  background: none;
  border: 1px solid var(--line);
  color: var(--text-dim);
  border-radius: 8px;
  padding: 0.45rem 0.75rem;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.78rem;
  font-weight: 500;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.accent-reset-btn:hover { color: var(--text); border-color: var(--hover-line, var(--line)); background: rgba(255, 255, 255, 0.04); }

/* ── LEAGUE BAR SETTINGS: ACCESSIBILITY ──────────────────────────────────
   Same off-by-default / localStorage pattern as the accent picker just
   above: picking a switch sets a data-* attribute on <html> (script at
   the bottom of leagueBar.ejs), and only the rules below read it - the
   admin "hazard terminal" dashboard doesn't include leagueBar.ejs at all,
   so it never sees these attributes or this CSS take effect. */
/* Its own sub-header style, not the plain --spaced label variant this used
   to reuse: noticeably smaller than .settings-panel-title (0.92rem vs
   1.05rem) but with the same "divider line underneath" treatment, so it
   reads as a real second section start rather than just another caption
   like "Accent Color" - and the larger top margin keeps real breathing
   room above it instead of sitting right under the reset button. */
.league-bar-settings-panel .settings-section-title {
  margin: 1.6rem 0 0.9rem;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--line);
  font-weight: 600;
  color: var(--text);
  font-size: 0.92rem;
}

.a11y-toggle-list { display: flex; flex-direction: column; gap: 0.7rem; }
.a11y-toggle { display: flex; align-items: center; justify-content: space-between; gap: 0.9rem; cursor: pointer; }
.a11y-toggle-title { display: block; font-size: 0.85rem; color: var(--text); }
.a11y-toggle-desc { display: block; font-size: 0.72rem; color: var(--text-dim); margin-top: 0.1rem; }
/* Visually hidden but still focusable/announced - the switch pill next to
   it is what's actually seen and clicked (via the wrapping <label>). */
.a11y-toggle-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.a11y-toggle-switch {
  flex-shrink: 0;
  width: 38px;
  height: 22px;
  border-radius: 999px;
  background: var(--line);
  border: 1px solid var(--line);
  position: relative;
  transition: background 0.15s, border-color 0.15s;
}
.a11y-toggle-switch::before {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text-dim);
  transition: transform 0.15s, background 0.15s;
}
.a11y-toggle-input:checked + .a11y-toggle-switch { background: var(--accent, var(--pink)); border-color: var(--accent, var(--pink)); }
.a11y-toggle-input:checked + .a11y-toggle-switch::before { transform: translateX(16px); background: var(--void); }
.a11y-toggle-input:focus-visible + .a11y-toggle-switch { outline: 2px solid var(--accent, var(--sel)); outline-offset: 2px; }

/* Changelog link tucked into the bottom of the Settings panel - same
   divider treatment as .settings-section-title above it, just without
   its own heading text, so it reads as a distinct "leaves this panel"
   action rather than another accessibility toggle. Scoped under
   .league-bar-settings-panel for the same reason .settings-panel-title
   above needs it: .league-bar-faq-panel p (elsewhere in this file) sets
   margin: 0 0 0.8rem at (0,1,1) specificity, which otherwise wins over
   a single .settings-changelog-link class and zeroes out this margin. */
.league-bar-settings-panel .settings-changelog-link {
  margin: 1.1rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
  font-size: 0.82rem;
}
.settings-changelog-link a { color: inherit; text-decoration: none; }
.settings-changelog-link a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }

/* Reduced motion: near-zero (not literal 0 - some libraries divide by the
   duration, and 0 can throw) animation/transition durations, plus instant
   scrolling. Two triggers below - the manual switch, AND the OS-level
   media query - so this already works for a first-time visitor whose OS
   asks for it, even before they've opened Settings once. The data-*
   attribute is only ever set while a league page's own script has run, so
   this can safely target <html> directly without extra scoping. */
@media (prefers-reduced-motion: reduce) {
  .page-assault-challenge *, .page-assault-challenge *::before, .page-assault-challenge *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
html[data-reduced-motion="on"] .page-assault-challenge *,
html[data-reduced-motion="on"] .page-assault-challenge *::before,
html[data-reduced-motion="on"] .page-assault-challenge *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}

/* Larger text: everything on these pages is sized in rem specifically so
   one root font-size bump like this rescales all of it together, instead
   of having to touch every rule individually. */
html[data-text-size="lg"] { font-size: 120%; }

/* Higher contrast: brighter dim-text/border tokens, read through the same
   custom-property indirection --accent already uses, plus a wider focus
   ring. --hover-line/--sel only exist inside .page-assault-challenge (see
   further up this file), so this is scoped the same way. */
html[data-contrast="high"] .page-assault-challenge {
  --text-dim: #C7CDD8;
  --line: #5B6B80;
  --hover-line: #8291A6;
}
html[data-contrast="high"] .page-assault-challenge a:focus-visible,
html[data-contrast="high"] .page-assault-challenge button:focus-visible,
html[data-contrast="high"] .page-assault-challenge input:focus-visible,
html[data-contrast="high"] .page-assault-challenge textarea:focus-visible {
  outline-width: 3px;
}

/* The actual accent effect, scoped to the league pages only. Each falls
   back to that element's normal color, so nothing changes until --accent
   is set. btn-primary's normal (unaccented) look is the same neutral
   border/text every other .btn uses - var(--line)/var(--text) - not
   pink; pink was only ever the picked-accent look, matching what the
   pink accent-swatch itself outputs. */
.page-assault-challenge .btn-primary { border-color: var(--accent, var(--line)); color: var(--accent, var(--text)); }
.page-assault-challenge .btn-primary:hover { background: var(--accent, var(--text)); color: var(--void); }
.page-assault-challenge a:focus-visible,
.page-assault-challenge button:focus-visible { outline-color: var(--accent, var(--sel)); }
/* Resting state, not just :hover - without this the icons only ever
   flashed the picked accent while actively hovered/tapped (and on touch,
   "hover" means mid-tap), so the theme looked like it wasn't sticking.
   Falls back to the icons' normal --text-dim when no accent is picked. */
.page-assault-challenge .league-bar-icon { color: var(--accent, var(--text-dim)); }
.page-assault-challenge .league-bar-icon:hover { color: var(--accent, var(--text)); }
/* .league-bar-faq[open] .league-bar-icon (further up this file) is more
   specific than the two rules above, so opening the FAQ or Settings panel
   was clobbering a picked accent back to plain --text the moment it
   opened - the icon only ever looked accented while still being
   hovered/held, never once you'd actually tapped it open. This needs one
   more class than that rule to win. */
.page-assault-challenge .league-bar-faq[open] .league-bar-icon { color: var(--accent, var(--text)); }
.page-assault-challenge .league-bar-tag { border-color: var(--accent, var(--line)); color: var(--accent, var(--text-dim)); }
/* These pink-by-default elements also appear on the league pages but were
   missed by the accent block above, so picking an accent left them stuck
   on the original pink. */
/* NOT var(--pink) here - that reintroduces the exact "selected chip
   flashes white-with-a-red-border on hover" bug the comment above
   .page-assault-challenge .chip:hover (further up this file) was written
   to fix. An unselected chip's hover is meant to stay the plain neutral
   --hover-line regardless of accent; only .chip-active (below) should
   ever read as the picked color. */
.page-assault-challenge .chip:hover { border-color: var(--hover-line); }
.page-assault-challenge .chip-active {
  color: var(--accent, var(--sel));
  border-color: var(--accent, var(--sel));
  background: color-mix(in srgb, var(--accent, var(--sel)) 10%, transparent);
  font-weight: 600;
}
.page-assault-challenge .type.high,
.page-assault-challenge .type.urgent { color: var(--accent, var(--sel)); border-color: var(--accent, var(--sel)); }
.page-assault-challenge .agent-tile.is-selected {
  border-color: var(--accent, var(--sel));
  background: color-mix(in srgb, var(--accent, var(--sel)) 8%, transparent);
}
/* var(--text), not var(--pink) - .page-assault-challenge .btn-link:hover
   further up this file already set this to plain --text; falling back to
   pink here just silently undid that neutral hover for anyone who hasn't
   picked an accent. */
.page-assault-challenge .btn-link:hover { color: var(--accent, var(--text)); }
.page-assault-challenge .agent-picker-required-tag { color: var(--accent, var(--text-dim)); }
.page-assault-challenge .da-tab-count--alert,
.page-assault-challenge .da-tab--active .da-tab-count--alert { background: var(--accent, var(--sel)); }

/* ── ROTATION PICKER (views/assault-rotations.ejs) ──────────────────────
   A vertical list of biweekly DA reset windows instead of a native
   <select> - same info (each row is just the rotation's date range) but
   inline and scrollable without popping a system sheet, and the selected
   row reads clearly against the "no run recorded" ones instead of a
   generic checkmark-in-a-menu. Deliberately reuses --sel/--sel-ink (the
   same neutral "currently selected" treatment as .da-tab--active above)
   rather than --accent - which window is selected has nothing to do with
   the opt-in accent color, so it should never light up in whatever the
   visitor picked. */
.rotation-picker {
  max-height: min(420px, 60vh);
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--void);
}
.rotation-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--line);
  color: var(--text);
  text-decoration: none;
}
.rotation-option:last-child { border-bottom: none; }
.rotation-option:hover { background: rgba(255, 255, 255, 0.03); }
.rotation-option-range {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.85rem;
}
.rotation-option-check {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  color: var(--sel, #E6EAF0);
}
.rotation-option--active {
  background: rgba(230, 234, 240, 0.06);
}
.rotation-option--active .rotation-option-range { color: var(--sel, #E6EAF0); font-weight: 600; }
.rotation-picker-current-tag {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.08rem 0.4rem;
}

/* Wider than the base 640px reading-width .panel - this page's content
   (the FAQ grid) is short items meant to sit side by side on a wide
   screen, not prose that gets harder to read past ~75 characters/line. */
.intro-panel { max-width: 900px; }
.legal-updated { margin: -0.6rem 0 1.4rem; font-family: 'Inter', system-ui, sans-serif; font-size: 0.8rem; color: var(--text-dim); }
.changelog-date { font-size: 0.75rem; color: var(--text-dim); margin: 0 0 0.4rem; }
.intro-faq {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  align-items: start;
  gap: 1.3rem;
  margin: 1.4rem 0 1.8rem;
}
.intro-faq-item a { color: var(--cyan); text-decoration: none; }
.intro-faq-item a:hover { text-decoration: underline; text-underline-offset: 2px; }
.intro-faq-item h2 {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 0.3rem;
}
/* On the intro/landing pages, let the user's chosen --accent (see the
   accent-swatch picker in leagueBar) carry through more of the page -
   headings, links, and the panel corner bracket - instead of being
   confined to just the CTA button, so picking an accent visibly
   re-themes the whole intro, not just one element. Each fallback below
   matches that element's actual pre-accent color (plain --text/--text-dim,
   inherited from body/.copy), NOT the brand pink/cyan - accent should
   only ever show up once someone picks one; with none picked this block
   must be a no-op and Reset must land back on the classic gray look, not
   on a hardcoded pink+cyan combo nobody chose. .intro-panel::before is the
   one exception: that bracket is pink on every panel site-wide (see
   .panel::before), not something this feature introduced, so it correctly
   keeps var(--pink) as its un-accented fallback. */
.page-assault-challenge .league-title { color: var(--accent, var(--text)); }
.page-assault-challenge .intro-faq-item h2 { color: var(--accent, var(--text)); }
.page-assault-challenge .intro-faq-item a { color: var(--accent, var(--cyan)); }
/* "Where this is headed" (home.ejs's .section-label) was the one heading
   on the intro page that didn't pick up the chosen accent - its only rule
   (further up this file) is a plain var(--text) with nothing reading
   --accent, same gap .league-bar-faq-panel strong had below. Fallback
   matches that original var(--text). */
.page-assault-challenge .section-label { color: var(--accent, var(--text)); }
.page-assault-challenge .intro-panel::before {
  border-top-color: var(--accent, var(--pink));
  border-left-color: var(--accent, var(--pink));
}
/* "Signed in as ... Staff dashboard · Log out" on home.ejs - lives inside
   a .copy paragraph, which is --text-dim; that's this link's real
   un-accented color (plain a { color: inherit } elsewhere in this file),
   not cyan. */
.page-assault-challenge .intro-panel .copy a { color: var(--accent, var(--text-dim)); text-decoration: none; }
.page-assault-challenge .intro-panel .copy a:hover { text-decoration: underline; text-underline-offset: 2px; }
/* A handful of key terms inside the Privacy/Terms body copy get the
   same accent treatment as the links around them, so important nouns
   ("Discord OAuth", "public leaderboard", etc.) pop out of the prose
   like the reference site does, without turning the whole paragraph
   into link-blue. Un-accented fallback is --text-dim (this strong sits
   in an .intro-faq-item p, not a .copy block, so it doesn't get the
   global .copy strong { color: var(--text) } treatment - it just
   inherits the paragraph's dim color). */
.page-assault-challenge .intro-faq-item p strong { color: var(--accent, var(--text-dim)); font-weight: 600; }
/* The "?" dropdown FAQ in leagueBar.ejs (shared header, every league page)
   is the same idea as .intro-faq-item above - question headers should pick
   up the chosen accent too - but it got missed in that pass: its base rule
   (.league-bar-faq-panel strong, further up this file) is a plain
   var(--text) with nothing reading --accent, so picking a color visibly
   re-themed the icons/name/tags in the bar above it while these headers
   stayed white regardless of choice. Fallback matches that original
   var(--text), same as the others in this block. */
.page-assault-challenge .league-bar-faq-panel strong { color: var(--accent, var(--text)); }
/* "Where this is headed" roadmap tags (Beta/Planned/Someday): these share
   the .league-bar-tag class with the badge up in leagueBar, which is why
   they were already picking up the site-wide accent rule above
   (.page-assault-challenge .league-bar-tag) uniformly across all three -
   Planned and Someday glowing right along with Beta, even though they're
   future stages nobody's "in" yet. Only the current stage should react to
   the chosen accent. These two rules (equal specificity, this one first)
   reset every roadmap tag back to the plain gray look, then re-apply the
   accent - plus a small glow - to just the one tag marked current. */
.page-assault-challenge .intro-faq-item .league-bar-tag {
  border-color: var(--line);
  color: var(--text-dim);
}
.page-assault-challenge .intro-faq-item .league-bar-tag-current {
  border: 1px solid transparent;
  /* The label itself is now transparent so the text-clipped gradient layer
     below (layer 1) shows through the glyph shapes instead of a flat
     solid color - that's what gives the letters the same shine as the
     border, rather than just sitting there as static colored text next
     to an animated ring. */
  color: transparent;
  /* Same moving sheen as before, now painted onto three layers instead of
     two so it reaches the text as well as the border:
     1) a repeating-linear-gradient clipped to `text` - shows only through
        the glyph shapes (color: transparent above is what makes this
        visible instead of being painted over by solid text color).
     2) the panel-fill layer (var(--panel)), clipped to padding-box -
        opaquely covers the pill's interior so layer 3 below only shows
        through the 1px ring the transparent border leaves exposed.
     3) the same repeating gradient as layer 1, clipped to border-box -
        the original border sheen.
     Layers 1 and 3 share one repeating gradient sized to an exact pixel
     tile, animated by exactly one tile width per loop, which is what
     keeps the sweep seamless - a plain gradient animated back to its
     start position visibly snaps/restarts each loop, which read as
     cheap; this never resets mid-cycle because tile N's end lines up
     exactly with tile N+1's start. With no accent picked, every stop
     below falls back to the same var(--line) - still animating, just
     invisibly, since a solid color repeating against itself is
     indistinguishable from the plain static look every other roadmap
     tag has. */
  background-image:
    repeating-linear-gradient(90deg,
      var(--accent, var(--line)) 0%,
      color-mix(in srgb, var(--accent, var(--line)) 20%, transparent) 15%,
      var(--accent, var(--line)) 30%
    ),
    linear-gradient(var(--panel), var(--panel)),
    repeating-linear-gradient(90deg,
      var(--accent, var(--line)) 0%,
      color-mix(in srgb, var(--accent, var(--line)) 20%, transparent) 15%,
      var(--accent, var(--line)) 30%
    );
  background-origin: border-box, padding-box, border-box;
  background-clip: text, padding-box, border-box;
  -webkit-background-clip: text, padding-box, border-box;
  background-size: 30px 100%, 100% 100%, 30px 100%;
  animation: roadmap-tag-border-sweep 1.8s linear infinite;
}
@keyframes roadmap-tag-border-sweep {
  to { background-position: 30px 0, 0 0, 30px 0; }
}
.intro-faq-item p {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--text-dim);
  margin: 0;
}
.intro-enter { display: inline-block; }
.league-title {
  margin: 0 0 1rem;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: 0;
}


/* ── DESKTOP LAYOUT (body.page-wide: assault, mine, rotations) ──────────
   Before this, the league bar and footer stayed at 640px while the panel
   was 900px, so the three edges never lined up and desktop just looked
   like a stretched phone page. Now all three share one width, the title
   and action buttons sit on one row, and the boss cards form an even grid
   where every card is the same size. Mobile is unchanged (everything is
   still capped by width: 100%). */
body.page-wide { --page-w: 1080px; }
body.page-wide .league-bar,
body.page-wide .league-bar-user-row,
body.page-wide .panel--wide,
body.page-wide .site-footer { max-width: var(--page-w); }

.da-head { display: flex; flex-direction: column; gap: 0.9rem; margin-bottom: 0.4rem; }
.da-head .league-title { margin: 0; }
.da-head-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }

/* Equal cards: fixed-width columns (auto-fill, not auto-fit, so a lone card
   doesn't stretch to full width), every row the same height, chips pinned
   to the bottom so they line up across cards whether or not a card has a
   cost cap line. */
body.page-wide .da-card-list {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
  grid-auto-rows: 1fr;
  align-items: stretch;
}
body.page-wide .da-card { min-height: 9.5rem; }
body.page-wide .da-card > .challenge-meta-chips { margin-top: auto; }

@media (min-width: 800px) {
  body.page-wide .panel--wide { padding: 2.25rem 2.5rem; }
  body.page-wide .da-card-list { gap: 1rem; }
  body.page-wide .da-card { padding: 1.2rem 1.3rem; }
  body.page-wide .da-card-title { font-size: 1.2rem; }
  body.page-wide .da-card-img { width: min(46%, 220px); }
  .da-head {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.6rem;
  }
  .da-head .league-title { font-size: 1.7rem; }
}


/* ── MOTION (subtle, public league pages) ───────────────────────────────
   Page fade-in, staggered challenge cards, card hover lift + art zoom,
   notice fade-in. Opacity + small transforms only, 120-220ms, ease-out.
   Scoped to body.page-assault-challenge (not the admin .dashboard-body), so
   the existing reduced-motion blocks above - the OS media query and the
   Settings switch (html[data-reduced-motion="on"]) - already switch every
   rule in here off. The accordion lives in motion.js and checks the same
   two signals. Entrance animations use `backwards` fill only, so nothing
   (opacity, transform, stacking context) lingers once they finish. */
@keyframes nepl-fade-up {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes nepl-fade-up-sm {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

/* 1. page fade-in */
body.page-assault-challenge:not(.dashboard-body) .panel {
  animation: nepl-fade-up 0.2s ease-out backwards;
}

/* 2. staggered challenge cards (open list only - the past-challenges list
   sits in a <details> and its search filter toggles cards' hidden state,
   which would replay the entrance on every keystroke) */
body.page-assault-challenge:not(.dashboard-body) .da-card-list:not(#past-challenges-list) > .da-card,
body.page-assault-challenge:not(.dashboard-body) .da-card-list:not(#past-challenges-list) > .da-lane-group {
  animation: nepl-fade-up 0.2s ease-out backwards;
}
body.page-assault-challenge .da-card-list > .da-card:nth-child(2) { animation-delay: 0.04s; }
body.page-assault-challenge .da-card-list > .da-card:nth-child(3) { animation-delay: 0.08s; }
body.page-assault-challenge .da-card-list > .da-card:nth-child(4) { animation-delay: 0.12s; }
body.page-assault-challenge .da-card-list > .da-card:nth-child(5) { animation-delay: 0.16s; }
body.page-assault-challenge .da-card-list > .da-card:nth-child(n+6) { animation-delay: 0.2s; }

/* 3. card hover lift + boss art zoom (hover only on devices that have it;
   touch just gets the press feedback) */
body.page-assault-challenge a.da-card {
  transition: transform 0.18s ease-out, border-color 0.18s ease-out;
}
body.page-assault-challenge .da-card-img {
  transition: transform 0.22s ease-out;
  transform-origin: right center;
}
@media (hover: hover) {
  body.page-assault-challenge a.da-card:hover { transform: translateY(-2px); }
  body.page-assault-challenge a.da-card:hover .da-card-img { transform: scale(1.04); }
}
body.page-assault-challenge a.da-card:active { transform: scale(0.99); }

/* 5. inline notices ease in. .notice--flash / .toast are excluded - those
   already have their own slide+fade (foot.ejs + .toast above), and a
   keyframe here would fight the toast's opacity. */
body.page-assault-challenge .notice:not(.notice--flash):not(.toast) {
  animation: nepl-fade-up-sm 0.2s ease-out backwards;
}

/* Toasts and buttons also appear on admin pages that don't carry
   .page-assault-challenge, so they get their own reduced-motion rule. */
@media (prefers-reduced-motion: reduce) {
  .toast, .toast-close, .btn { transition-duration: 0.001ms; }
  .btn:active:not(:disabled) { transform: none; }
}

/* /assault/thanks - "Special thanks" cards. Same panel/line/void tokens as
   the rest of the league pages (see .challenge-chip / .run-card); the name
   picks up the chosen accent like every other heading here, and falls back
   to plain --text when none is picked. */
.thanks-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.9rem;
  margin: 0 0 1.9rem;
}
.thanks-card {
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 1rem 1.05rem 1.1rem;
}
.thanks-card-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}
.thanks-avatar {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  border: 1px solid var(--line);
  object-fit: cover;
  background: var(--panel);
}
.thanks-card-who {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  min-width: 0;
}
.thanks-card h2 {
  margin: 0;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
  overflow-wrap: anywhere;
}
.page-assault-challenge .thanks-card h2 { color: var(--accent, var(--text)); }
.thanks-card p {
  margin: 0;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--text-dim);
}


/* Starter lane. .da-lane is the small Main/Starter pill; .da-lane-group wraps
   a boss's lanes so the Starter reads as part of that boss's card instead of
   a second boss. Groups only exist when a boss has more than one lane, so a
   boss with just a Main challenge looks exactly as it did before. */
.da-lane {
  display: inline-block;
  vertical-align: middle;
  margin-left: 0.35rem;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.da-lane--starter { color: var(--green); border-color: var(--green); }
.da-card-title > .da-lane:first-child { margin-left: 0; }
.da-lane-group { display: flex; flex-direction: column; min-width: 0; }
.da-lane-group[hidden] { display: none; }
.da-lane-group > .da-card:first-child { flex: 1 1 auto; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.da-lane-group > .da-card--attached {
  min-height: 0;
  margin-top: -1px;
  border-top: 1px dashed var(--line);
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.da-lane-group > a.da-card:hover { transform: none; }
.challenge-meta-value a { color: var(--cyan); }
/* A list that has a lane group in it can't keep "every row as tall as its
   tallest card" (grid-auto-rows: 1fr above) - the group is roughly two cards
   tall, so every single-lane card would stretch to match it. Let rows size to
   their content and top-align instead. Lists with no group are untouched. */
body.page-wide .da-card-list:has(.da-lane-group) { grid-auto-rows: auto; align-items: start; }

/* /assault/mine: submission status. The pill was 0.6rem, easy to miss on a
   phone; the note under it says in plain words what the status means. */
/* Cards here vary in height (rejection reasons), so rows size to their
   content instead of all matching the tallest card. */
body.page-wide .sub-list { grid-auto-rows: auto; }
.sub-summary { margin: 0 0 1rem; }
.sub-card .score-card-head .type { font-size: 0.72rem; padding: 0.15rem 0.5rem; margin-bottom: 0; }
.sub-status-note {
  font-size: 0.88rem;
  line-height: 1.5;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  background: var(--void);
  border: 1px solid var(--line);
  overflow-wrap: anywhere;
}
.sub-status-note--pending { color: var(--amber); }
.sub-status-note--approved { color: var(--green); }
.sub-status-note--rejected { color: var(--pink); }
.sub-status-note--rejected strong { font-weight: 600; }

/* Founding Proxies counter (home). Quiet on purpose: small, dim, one line. */
.founding-counter {
  margin: -0.4rem 0 1.3rem;
  font-size: 0.8rem;
  color: var(--text-dim);
}
.founding-counter strong { color: var(--accent, var(--text)); font-weight: 600; }

/* ── NEPL DESKTOP (>= 1024px) ────────────────────────────────────────────
   NEPL is the public product; the bot dashboard is dev-only. On phones the
   two happen to share a "column of panels" shape, but on desktop NEPL now
   reads as a website of its own instead of a stretched phone page or a
   terminal window:
     - full-bleed sticky top nav (blurred), content centered under it
     - soft accent glow behind the page instead of the flat terminal void
     - panels drop their card chrome on the listing/home pages so content
       sits directly on the page; text pages get a comfortable reading
       column and keep their card
     - home becomes a hero (big title, wider intro) with FAQ/roadmap as a
       card grid
     - the footer becomes a full-width band
   Everything is scoped to body.page-assault-challenge:not(.dashboard-body),
   so the dev dashboard (which also uses .page-assault-challenge for the
   reviewer view) and every viewport under 1024px are untouched. */
@media (min-width: 1024px) {
  body.page-assault-challenge:not(.dashboard-body) {
    --nepl-w: 1200px;
    --nepl-gutter: max(2rem, calc((100vw - var(--nepl-w)) / 2));
    padding: 0 0 0;
    background:
      radial-gradient(900px 420px at 50% -80px,
        color-mix(in srgb, var(--accent, #FF405E) 16%, transparent), transparent 70%),
      var(--void);
    background-repeat: no-repeat;
  }

  /* top nav: full width, sticky, blurred */
  body.page-assault-challenge:not(.dashboard-body) .league-bar {
    position: sticky;
    top: 0;
    max-width: none;
    margin: 0 0 2.5rem;
    padding: 0.85rem var(--nepl-gutter);
    gap: 0.9rem;
    background: rgba(11, 15, 20, 0.78);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);
    z-index: 50;
  }
  body.page-assault-challenge:not(.dashboard-body) .league-bar-name { font-size: 1.05rem; letter-spacing: 0.01em; }
  body.page-assault-challenge:not(.dashboard-body) .league-bar-icons { gap: 1.1rem; }
  body.page-assault-challenge:not(.dashboard-body) .league-bar-icon svg { width: 20px; height: 20px; }
  body.page-assault-challenge:not(.dashboard-body) .league-bar-user-row {
    max-width: var(--nepl-w);
    padding: 0 2rem;
    margin: -1.75rem 0 1.5rem;
  }

  /* one shared content width for every NEPL page */
  body.page-assault-challenge:not(.dashboard-body) .panel,
  body.page-assault-challenge:not(.dashboard-body) .site-footer {
    width: calc(100% - 4rem);
  }
  body.page-assault-challenge:not(.dashboard-body) .panel { max-width: 860px; padding: 2.5rem 3rem; }
  body.page-assault-challenge:not(.dashboard-body) .panel--wide,
  body.page-assault-challenge:not(.dashboard-body) .intro-panel,
  body.page-assault-challenge:not(.dashboard-body) .site-footer { max-width: var(--nepl-w); }

  /* listing + home: content sits on the page, no card chrome */
  body.page-assault-challenge:not(.dashboard-body) .panel--wide,
  body.page-assault-challenge:not(.dashboard-body) .intro-panel {
    background: transparent;
    border: 0;
    box-shadow: none;
    padding: 0.5rem 0 1rem;
  }
  body.page-assault-challenge:not(.dashboard-body) .league-title {
    font-size: 2rem;
    font-weight: 700;
    letter-spacing: -0.015em;
  }

  /* home hero */
  body.page-assault-challenge:not(.dashboard-body) .intro-panel { padding-top: 2.5rem; }
  body.page-assault-challenge:not(.dashboard-body) .intro-panel .league-title {
    font-size: 3.25rem;
    line-height: 1.05;
    margin-bottom: 1.1rem;
    max-width: 16ch;
  }
  body.page-assault-challenge:not(.dashboard-body) .intro-panel > .copy {
    font-size: 1.15rem;
    line-height: 1.6;
    color: var(--label);
    max-width: 58ch;
  }
  body.page-assault-challenge:not(.dashboard-body) .intro-enter {
    padding: 0.8rem 1.6rem;
    font-size: 0.95rem;
    margin: 0.25rem 0 2.5rem;
  }
  body.page-assault-challenge:not(.dashboard-body) .intro-panel > .intro-enter { display: inline-flex; }
  body.page-assault-challenge:not(.dashboard-body) .intro-faq {
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 1rem;
    margin: 1.25rem 0 2.25rem;
  }
  body.page-assault-challenge:not(.dashboard-body) .intro-faq-item {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 1.25rem 1.35rem;
    height: 100%;
  }
  body.page-assault-challenge:not(.dashboard-body) .intro-faq-item h2 { font-size: 1rem; margin-bottom: 0.5rem; }

  /* listing pages: roomier cards, bigger head */
  body.page-assault-challenge:not(.dashboard-body) .da-head { margin-bottom: 1.25rem; }
  body.page-assault-challenge:not(.dashboard-body) .da-card-list { gap: 1.25rem; }
  body.page-assault-challenge:not(.dashboard-body) .da-card { border-radius: 14px; }
  body.page-assault-challenge:not(.dashboard-body) a.da-card:hover {
    border-color: var(--hover-line);
    transform: translateY(-2px);
  }

  /* footer: full-width band */
  body.page-assault-challenge:not(.dashboard-body) .site-footer {
    margin-top: auto;
    padding: 1.5rem 0 2rem;
  }
  body.page-assault-challenge:not(.dashboard-body) > .panel:last-of-type { margin-bottom: 3rem; }
}

/* ─── Backup agents (Deadly Assault) ─────────────────────────────────────
   Small pill marking a run where a Standard/A-rank backup played a Limited
   agent's slot. Same leaderboard as every other run - this is only a label. */
.backup-pill,
.coming-soon-pill {
  display: inline-block;
  vertical-align: middle;
  margin-left: 0.35rem;
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--cyan, currentColor);
  border-radius: 999px;
  color: var(--cyan, inherit);
  font-size: 0.68rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}
/* "Coming Soon" = the same pill as above, on agents / W-Engines whose
   releaseAt (zzz/assaultData.js) hasn't passed. Inside a picker tile it sits
   under the name, so no left margin and a smaller size to fit the tile. */
.agent-tile .coming-soon-pill,
.vote-tile .coming-soon-pill {
  margin: 0.15rem 0 0;
  padding: 0.05rem 0.3rem;
  font-size: 0.56rem;
}
/* Locked tiles use the normal disabled look, just less faded so the pill
   stays readable. */
.agent-tile.is-coming-soon:disabled { opacity: 0.55; }
.vote-tile.is-coming-soon:has(input:disabled) { opacity: 0.6; }
.backup-toggle {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin: 1rem 0 0.25rem;
  padding: 0.5rem 0.65rem;
  border: 1px dashed var(--border, rgba(255,255,255,0.25));
  border-radius: 8px;
  font-size: 0.85rem;
  cursor: pointer;
}
.backup-toggle input { margin-top: 0.2rem; }
.agent-block[hidden] { display: none; }
