/* ====================================================
   JDI Automation Suite — Web console
   Graphite operations-room theme. Signal amber accent.
   Numerals and IDs are always IBM Plex Mono: numbers are
   the payload of this tool and deserve their own voice.
   ==================================================== */

:root {
  --bg: #242424;                /* desktop app background */
  --surface: #2b2b2b;
  --surface-2: #343434;
  --line: #3d3d3d;
  --text: #e6e6e6;
  --dim: #9aa0a6;
  --accent: #3a7ebf;        /* desktop blue — primary/interactive */
  --accent-ink: #ffffff;    /* text on blue buttons */
  --green: #2fa572;         /* desktop green — running/ok */
  --gold: #d1a63a;          /* JDI / spend / paused accent */
  --red: #e5605d;
  --blue: #58a6ff;          /* delivered figures */
  /* Dashboard metric colours — meaning-tied, match desktop */
  --m-running: #4680ff;
  --m-paused: #e67e22;
  --m-paused-zero: #e74c3c;
  --m-delivered: #2ecc71;
  --m-credits: #16a085;
  --m-transferred: #9b59b6;
  --m-received: #e84393;
  --m-spend: #f39c12;
  --m-drop-spend: #d68910;
  /* Spend page's number-pool cost (spent minus completed) — a
     soft coral-orange the existing dashboard palette has no
     equivalent for (m-paused is amber, red is a warning tone). */
  --m-pool: #e07a5f;
  --radius: 10px;
  --mono: "IBM Plex Mono", ui-monospace, Consolas, monospace;
  --sans: "Inter", system-ui, -apple-system, "Segoe UI",
          sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 14px;
}

button, input, select, textarea { font: inherit; }

/* ---------------- Login / setup ---------------- */

.gate {
  min-height: 100vh;
  display: grid;
  place-items: center;
}

.gate-card {
  width: min(340px, 92vw);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 28px;
}

.gate-card h1 {
  margin: 0 0 4px;
  font-size: 18px;
  letter-spacing: 0.04em;
}

.gate-card .sub {
  color: var(--dim);
  font-size: 12.5px;
  margin-bottom: 20px;
}

.wordmark {
  font-family: var(--mono);
  color: var(--accent);
  letter-spacing: 0.18em;
  font-size: 11px;
  margin-bottom: 10px;
}

/* ---------------- App shell ---------------- */

.shell { display: none; min-height: 100vh; }

/* 12px reserved gutter — just enough for the collapsed sidebar's
   rail sliver (see .sidebar below) so .main naturally starts right
   after it without extra margin math. Only relevant above the
   860px breakpoint; below it the sidebar becomes the click-toggled
   drawer instead (see the @media block further down) and this
   gutter isn't needed since the drawer is a pure overlay with no
   permanently-visible sliver. */
.shell.on { display: grid; grid-template-columns: 12px 1fr; }

/* Desktop-width sidebar: a hover flyout, same interaction as the
   desktop app's own rail + hover-expand sidebar (gui/app.py's
   _expand_sidebar/_schedule_collapse). Collapsed by default to a
   thin rail (just the accent-colored sliver from .sidebar::after
   below peeking out at the left edge); hovering it slides the full
   labeled sidebar out as an overlay via app.js's mouseenter/
   mouseleave handlers, which toggle the .expanded class below;
   moving the mouse away collapses it back after a short delay
   (avoids flicker). Below 860px this entire block is replaced by
   the click-toggled drawer treatment in the @media block further
   down, which uses a separate .open class and its own transform
   values. app.js's handlers are gated to only ever add/remove
   .expanded above 860px (see isDesktopWidth()), but that alone
   isn't airtight — e.g. resizing the window down while .expanded
   is still applied from a moment ago, before any resize listener
   fires, would otherwise leave a stale .expanded class around. So
   .sidebar.expanded's actual transform rule is ALSO wrapped in its
   own min-width media query further below (see "Desktop-only:
   .expanded"), belt-and-suspenders — even a stale class can never
   fight the mobile drawer's own transform once the CSS itself
   structurally can't apply below 860px. */
.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--line);
  padding: 18px 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: 236px;
  z-index: 190;
  transform: translateX(-224px);
  transition: transform 0.18s ease;
}

/* The always-visible affordance while collapsed — a thin accent
   line at the sidebar's own right edge, which is the only part
   still inside the viewport once the rest is translated off-screen
   (see .sidebar's -224px transform above). Hidden once expanded,
   since the sidebar's own border-right serves the same purpose
   then. */
.sidebar::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 3px;
  height: 100%;
  background: var(--accent);
  opacity: 0.55;
}

/* Hidden whenever the mobile click-open drawer (.open, see the
   @media block further down) is shown in full — it already has its
   own border/box-shadow, so this rail-only accent would just be a
   redundant thin line down its edge. (The desktop .expanded
   equivalent lives inside the min-width media query below, next to
   .sidebar.expanded itself, for the same stale-class safety reason
   noted above.) */
.sidebar.open::after { opacity: 0; }

/* Desktop-only: .expanded (the hover-flyout's "open" state).
   Deliberately scoped to a min-width media query, NOT left as a
   bare .sidebar.expanded rule — see the long comment on the base
   .sidebar rule above for why: app.js already gates its own
   mouseenter/mouseleave handlers to 861px+, but if .expanded were
   ever left applied while the viewport is narrower (a resize mid-
   hover, devtools device emulation, etc.), an unscoped rule here
   would out-specificity the mobile drawer's own .sidebar transform
   and wedge the drawer open. Scoping the RULE itself to 861px+
   means that can't happen regardless of the class's state. */
@media (min-width: 861px) {
  .sidebar.expanded {
    transform: translateX(0);
    box-shadow: 4px 0 20px rgba(0, 0, 0, 0.35);
  }

  .sidebar.expanded::after { opacity: 0; }
}

.sidebar .wordmark { padding: 0 10px 14px; }

.nav-btn {
  text-align: left;
  background: none;
  border: 0;
  color: var(--dim);
  padding: 9px 10px;
  border-radius: 7px;
  cursor: pointer;
  letter-spacing: 0.02em;
}

.nav-btn:hover { color: var(--text); background: var(--surface-2); }

.nav-btn.on {
  color: var(--accent);
  background: var(--surface-2);
  box-shadow: inset 3px 0 0 var(--accent);
}

.sidebar .foot {
  margin-top: auto;
  padding: 10px;
  color: var(--dim);
  font-size: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.main {
  /* Top padding bumped from 26px to reserve room for the
     .live-calls-topbar badge now floating above the content on
     every breakpoint (2026-08-14) — same idea as the mobile-only
     hamburger reservation just below, just no longer mobile-only. */
  padding: 56px 30px 60px;
  min-width: 0;
  /* .sidebar is position:fixed (see above), which takes it out of
     normal grid flow entirely — without this, grid auto-placement
     puts .main into the first (12px) track instead of the second
     (1fr) one, squeezing all page content into a sliver. Reset to
     column 1 in the mobile media query below, where the grid is a
     single 1fr column and .sidebar is a pure overlay drawer. */
  grid-column: 2;
}

.view { display: none; }
.view.on { display: block; }

.view-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}

.view-head h2 {
  margin: 0;
  font-size: 17px;
  letter-spacing: 0.03em;
}

.view-head .spacer { flex: 1; }

/* "N Live" call counter — mirrors gui/app.py's LiveCallsIndicator
   (the SAME live_call_icon.png asset — a pixel-exact green phone
   handset with two call/signal waves — plus a live count, summed
   across every configured Ringba account). Muted/grayscale (.off)
   when no token/accounts are configured or the poll fails, same
   as the desktop. */
.live-calls {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.live-calls-icon {
  display: block;
  width: 16px;
  height: 16px;
}

.live-calls b {
  font-family: var(--mono);
  font-size: 14px;
  color: #2ecc71;
}

.live-calls.off .live-calls-icon {
  filter: grayscale(1) opacity(0.55);
}

.live-calls.off b { color: var(--dim); }

/* Lives outside the sidebar entirely (moved 2026-08-14, per
   request: "the live call must live outside the sidebar and must
   be visible across all tabs" — applies to both mobile and
   desktop; then moved from top-left to top-right per follow-up
   request: "not conflicting any buttons" — the hamburger button
   occupies top-left below 860px, and top-left is also the first
   place every view-head's own content starts, so top-right is
   clear of both on every tab/breakpoint). Fixed on every
   breakpoint so it's never hidden by the sidebar being collapsed
   (desktop hover-rail) or closed (mobile drawer), and never
   disappears switching tabs. z-index above the sidebar (190) and
   hamburger (200) so it stays on top even while the sidebar is
   expanded/open. Nothing else is fixed at top-right at any
   breakpoint, so no mobile-specific offset override is needed here
   (contrast the old top-left placement, which needed one to clear
   the hamburger). See .main's padding-top above for the reserved
   header strip this needs on every breakpoint, not just mobile —
   every view-head's own right-aligned buttons (Push to Sheet,
   Refresh, etc.) start below that reserved strip, same as the
   page title does on the left, so this can't overlap them. */
.live-calls-topbar {
  position: fixed;
  top: 14px;
  right: 24px;
  z-index: 210;
  padding: 5px 10px;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--line);
}

/* Mobile-only sidebar drawer controls — both stay display:none
   above the 860px breakpoint, where the sidebar is already
   persistently visible (same as the desktop app), so there's
   nothing to toggle. See the @media (max-width: 860px) block
   below for the drawer itself (.sidebar.open). */
.nav-hamburger {
  display: none;
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 200;
  width: 38px;
  height: 38px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}

.nav-hamburger span {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 1px;
  background: var(--text);
}

/* Toggled on while the drawer is open (see app.js's openSidebar) —
   the drawer's own wordmark header sits at the same corner the
   hamburger occupies, so both visible at once would overlap. */
.nav-hamburger-hidden { visibility: hidden; }

.nav-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 170;
  background: rgba(0, 0, 0, 0.5);
}

.nav-backdrop.on { display: block; }

/* Batch Create's server-side folder browser (Browse button). */
.fb-row {
  padding: 6px 10px;
  cursor: pointer;
  font-size: 13px;
  border-bottom: 1px solid var(--line);
}
.fb-row:last-child { border-bottom: none; }
.fb-row:hover { background: var(--surface-2); }
.fb-row.fb-up { color: var(--accent); font-weight: 600; }

/* ---------------- Controls ---------------- */

.btn {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 7px 14px;
  cursor: pointer;
}

.btn:hover { border-color: var(--dim); }

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

.btn.danger { border-color: var(--red); color: var(--red); }
/* Pause / Resume — same amber/green convention as the desktop's
   Pause and Resume campaign buttons. */
.btn.warn { border-color: var(--gold); color: var(--gold); }
.btn.success { border-color: var(--green); color: var(--green); }
.btn.ghost { background: none; }
.btn:disabled { opacity: 0.45; cursor: default; }

.btn.sm { padding: 4px 9px; font-size: 12.5px; }

input[type=text], input[type=password], input[type=number],
input[type=date], select, textarea {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 7px;
  color: var(--text);
  padding: 7px 10px;
  width: 100%;
}

input:focus, select:focus, textarea:focus, .btn:focus-visible,
.nav-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

label.f {
  display: block;
  font-size: 12px;
  color: var(--dim);
  margin: 10px 0 4px;
}

.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
.row > * { flex: 0 0 auto; }
.row .grow { flex: 1 1 140px; }

.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* Campaign Monitor: Audio File cell is double-clickable
   (opens the same audio picker as the "Audio" button). */
td.audio-cell { cursor: pointer; }
td.audio-cell:hover { text-decoration: underline; }

/* ---------------- Dashboard cards ----------------
   Signature element: each account is a "line card" with a
   status strip on its left edge — the switchboard read:
   green = lines running, amber = paused only, red = fault,
   grey = idle. */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(255px, 1fr));
  gap: 14px;
}

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px 14px 20px;
  overflow: hidden;
}

.card::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 5px;
  background: var(--line);
}

.card.running::before { background: var(--green); }
.card.paused::before  { background: var(--gold); }
.card.fault::before   { background: var(--red); }

.card.running::before {
  animation: pulse 2.4s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .card.running::before { animation: none; }
}

.card h3 {
  margin: 0 0 2px;
  font-size: 13.5px;
  font-weight: 600;
}

.card .role {
  font-size: 10.5px;
  letter-spacing: 0.14em;
  color: var(--dim);
  font-family: var(--mono);
  margin-bottom: 12px;
}

.stat-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 14px;
}

.stat .v {
  font-family: var(--mono);
  font-size: 19px;
  font-variant-numeric: tabular-nums;
}

.stat .k { font-size: 11px; color: var(--dim); }

.v.green { color: var(--green); }
.v.amber { color: var(--gold); }
.v.gold  { color: var(--gold); }
.v.red   { color: var(--red); }
.v.blue  { color: var(--blue); }

.card .err {
  margin-top: 10px;
  color: var(--red);
  font-size: 12px;
  word-break: break-word;
}

.chips { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }

.chip {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 12.5px;
  color: var(--dim);
}

.chip b { color: var(--text); font-family: var(--mono); margin-left: 6px; }

/* ---------------- Tables ---------------- */

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

th, td {
  text-align: left;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}

th {
  color: var(--dim);
  font-weight: 500;
  font-size: 11.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: var(--surface-2);
}

tr:last-child td { border-bottom: 0; }

td.num, th.num { font-family: var(--mono); }

.tag {
  font-family: var(--mono);
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 5px;
  border: 1px solid var(--line);
  color: var(--dim);
}

.tag.running { color: var(--green); border-color: var(--green); }
.tag.paused  { color: var(--gold); border-color: var(--gold); }
.tag.stopped { color: var(--red); border-color: var(--red); }

/* ---------------- SmartPool: order-job progress bar ---------------- */

.sp-progress {
  width: 100%;
  height: 8px;
  border-radius: 4px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  overflow: hidden;
  margin: 8px 0;
}

.sp-progress .bar {
  height: 100%;
  background: var(--accent, var(--green));
  transition: width 0.3s ease;
}

.sp-ivr-row {
  display: flex;
  gap: 6px;
  align-items: center;
  margin-bottom: 6px;
}

/* ---------------- Panels / logs ---------------- */

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  margin-bottom: 16px;
}

.panel h3 {
  margin: 0 0 4px;
  font-size: 14px;
}

.panel .hint { color: var(--dim); font-size: 12.5px; margin: 0 0 12px; }

.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.log {
  font-family: var(--mono);
  font-size: 12px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 10px 12px;
  height: 220px;
  overflow: auto;
  white-space: pre-wrap;
  color: var(--dim);
}

.state-dot {
  display: inline-block;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--dim);
  margin-right: 7px;
}

.state-dot.on { background: var(--green); }
.state-dot.warn { background: var(--gold); }
.state-dot.err { background: var(--red); }

/* ---------------- Misc ---------------- */

.toast {
  position: fixed;
  right: 22px; bottom: 22px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-left: 4px solid var(--accent);
  border-radius: 8px;
  padding: 12px 16px;
  max-width: 380px;
  z-index: 50;
  font-size: 13px;
}

.toast.err { border-left-color: var(--red); }
.toast.ok { border-left-color: var(--green); }

.muted { color: var(--dim); }
.danger-text { color: var(--red); }

.file-drop {
  border: 1.5px dashed var(--line);
  border-radius: var(--radius);
  padding: 26px;
  text-align: center;
  color: var(--dim);
  cursor: pointer;
}

.file-drop.hot { border-color: var(--accent); color: var(--accent); }

.pill-select {
  display: flex; flex-wrap: wrap; gap: 8px;
}

.pill {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 13px;
  cursor: pointer;
  color: var(--dim);
  background: none;
  font-size: 12.5px;
}

.pill.on {
  border-color: var(--accent);
  color: var(--accent);
}

.modal-back {
  position: fixed; inset: 0;
  background: rgba(6, 9, 12, 0.72);
  display: grid; place-items: center;
  z-index: 40;
}

.modal {
  width: min(520px, 92vw);
  max-height: 88vh;
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px;
}

.modal h3 { margin: 0 0 14px; }

.modal .actions {
  display: flex; gap: 10px; justify-content: flex-end;
  margin-top: 20px;
}


/* ==== Dashboard: desktop-style chips + grouped table ==== */

.dchips {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}

.dchip {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 14px 7px;
  min-width: 92px;
}

.dchip .dc-v {
  font-size: 19px;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.dchip .dc-k {
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  margin-top: 3px;
}

/* Metric colour classes (chips and table cells share them) */
.m-run    { color: var(--m-running); }
.m-pause  { color: var(--m-paused); }
.m-pause0 { color: var(--m-paused-zero); }
.m-del    { color: var(--m-delivered); }
.m-cred   { color: var(--m-credits); }
.m-trans  { color: var(--m-transferred); }
.m-recv   { color: var(--m-received); }
.m-spend  { color: var(--m-spend); }
.m-drop   { color: var(--m-drop-spend); }
.m-drop-rate { color: var(--red); }
/* Desktop's COLOR_CAMPAIGNS_RAN (#5dade2, light blue) is close
   enough to the existing --blue to reuse rather than add a
   near-duplicate variable. */
.m-campaigns-ran { color: var(--blue); }

/* Dashboard "Debug" panel — the same per-account Drop Rate
   diagnostic line that's also printed to the server console
   (tagged "[spend-diag]"), surfaced here for anyone running the
   server without a visible terminal. */
.debug-out {
  margin-top: 12px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--dim);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 260px;
  overflow: auto;
}

.dash-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  background: var(--surface);
}

.dash-table th {
  text-align: right;
  padding: 8px 12px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--dim);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}

.dash-table th.dname { text-align: left; }

/* Brand(s) Running -- text, not a number, so its header (and the
   liveCellText td beneath it) stay left-aligned like the account
   name column instead of the right-aligned numeric default above. */
.dash-table th.left { text-align: left; }

.dash-table td {
  padding: 6px 12px;
  border-bottom: 1px solid transparent;
  font-size: 13px;
}

.dash-table td.num {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* Single line per row, always — without this, a long account name
   wraps to two lines and every row on the page quietly grows taller,
   which adds up fast across dozens of rows and is a big part of why
   the table needed so much more scrolling than it should have. A
   clipped "…" on the rare very long name is a better trade than
   doubling every row's height. */
.dash-table td.dname {
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Brand(s) Running: same single-line-with-ellipsis treatment as
   the account name column above, and for the same reason -- a
   fixed-width cell (see col.col-brands) with several comma-joined
   brand names would otherwise wrap and grow every row's height. */
.dash-table td.brands-cell {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Group divider before each new main group */
.dash-table tr.grp-start td { border-top: 1px solid var(--line); }

/* MAIN / SUB name tags */
.dash-table .tag.main {
  color: var(--m-transferred);
  border-color: var(--m-transferred);
}
.dash-table .tag.sub {
  color: var(--dim);
  border-color: var(--line);
}

.dash-table .nm-main { font-weight: 600; }
.dash-table .nm-sub  { font-weight: 400; }

/* Indent subs under their main */
.dash-table tr.sub td.dname { padding-left: 26px; }

.dash-table tr.drow:hover td { background: var(--surface-2); }
.dash-table tr.drow { cursor: pointer; }

/* OTHER group subhead */
.dash-table tr.grp-other td {
  padding: 10px 12px 4px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  border-top: 1px solid var(--line);
}

/* ---- Dashboard: fill the viewport, no page scroll ----
   The view-head and chip row are pinned; the account table is
   the only thing that scrolls, and only if it doesn't fit —
   with its own header row staying visible while it does (same
   frozen-header idea as the Spend tab, minus that tab's footer-
   stacking complexity, since this table has no footer to pin).
   The actual height comes from fitFillHeight() in app.js as an
   inline style, not a calc() here — how much space the view-head
   and chip row take above this varies far too much by screen size
   and orientation for one hardcoded number (measured anywhere from
   ~235px to ~430px of "stuff above the table" across real phone
   sizes, portrait vs landscape, before this became dynamic) to
   ever be right for all of them. This 70vh is just the fallback
   for the instant before that JS runs. */
#view-dashboard.on {
  display: flex;
  flex-direction: column;
  height: 70vh;
  min-height: 0;
}

#view-dashboard .view-head,
#view-dashboard .dchips {
  flex: 0 0 auto;
}

/* A little tighter than other pages' default spacing (scoped
   here, not changed globally) — extra density buys more room
   for the account table below. */
#view-dashboard .view-head { margin-bottom: 12px; }
#view-dashboard .dchips { margin-bottom: 10px; }

#dash-cards {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

/* Genuinely frozen header around a scrolling account table —
   two stacked <table>s (head / scrolling body) sharing one
   <colgroup>, same idea as the Spend tab's frozen header/footer.
   NOT position:sticky on the header <th>s: sticky positioning on
   table cells is unreliable with border-collapse: collapse (the
   header silently scrolls away instead of staying pinned — this
   is what shipped first, and scrolling to the bottom of a long
   account list showed the header had scrolled off with it).
   Splitting into two tables sidesteps that browser limitation
   entirely. alignDashFreeze() in app.js compensates for the
   scroll body's own scrollbar width so columns still line up. */
.dash-freeze {
  display: flex;
  flex-direction: column;
  height: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.dash-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

.dash-table col.col-dname { width: 200px; }

/* Brand(s) Running holds comma-joined brand names (occasionally
   several per account), so it needs meaningfully more room than a
   plain number column -- mirrors the desktop table's minsize=170
   for this same column. */
.dash-table col.col-brands { width: 160px; }

a.btn { text-decoration: none; display: inline-block; }

/* ==== Spend page: hourly/daily grid + 7-day trend ====
   Metric colour classes not already covered by the dashboard's
   m-* set (m-spend/gold already matches COLOR_SPEND, green
   already matches COLOR_REMAIN, blue is a close-enough stand-in
   for COLOR_COMPLETED — see the --m-pool comment above for why
   pool cost needed a genuinely new variable). */
.m-completed { color: var(--blue); }
.m-pool      { color: var(--m-pool); }
.m-remaining { color: var(--green); }

/* Genuinely frozen header + footer around a scrolling hour body,
   mirroring the desktop's separate frozen panes: three stacked
   <table>s (head / scrolling body / foot) sharing one <colgroup>,
   rather than one <table> with sticky thead/tfoot. The sticky
   approach needed the footer's per-row pixel offsets to exactly
   match the real rendered row height — any drift (font metrics,
   padding, etc.) silently overlapped the footer onto the last
   hour row(s) and hid them. Splitting into separate tables removes
   that fragility; alignSpendFreeze() in app.js compensates for the
   scroll body's own scrollbar width so columns still line up. */
/* Height is set by fitFillHeight() in app.js as an inline style —
   same reasoning as the Dashboard's #view-dashboard.on above: a
   fixed 620px is either too tall (forcing page scroll on a short
   landscape phone to reach the whole thing) or leaves the page
   under-filled (on a tall viewport, showing fewer rows than could
   fit) depending on orientation and screen size. 400px here is
   just the fallback for the instant before that JS runs. */
.spend-freeze {
  height: 400px;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.spend-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

.spend-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  background: var(--surface);
  font-size: 12.5px;
}

.spend-table col.col-dname { width: 104px; }
.spend-table col.col-total { width: 92px; }

.spend-table th, .spend-table td {
  padding: 6px 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.spend-table td.dname, .spend-table th.dname { text-align: left; }

.spend-head thead th {
  background: var(--surface-2);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  text-align: right;
  border-bottom: 1px solid var(--line);
}

.spend-body tbody td {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

.spend-foot tfoot td {
  background: var(--surface-2);
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  border-top: 1px solid var(--line);
}

.spend-foot tfoot tr:first-child td { border-top: 2px solid var(--line); }

.spend-table .sp-main       { color: var(--m-transferred); }
.spend-table .sp-now        { color: var(--m-spend); font-weight: 700; }
.spend-table .sp-total-head { color: var(--m-spend); }

/* ---- Shared mobile condensed-card component ----
   Used identically by BOTH the Spend tab (.spend-mobile-summary)
   and the Dashboard tab (.dash-mobile-list) — same totals strip +
   tap-to-expand card list shape, just fed different data, so the
   two tabs' mobile views actually match rather than just looking
   similar. Hidden by default (desktop keeps each tab's own frozen
   grid); swapped in below the 860px breakpoint — see the media
   query further down, where each tab's own outer wrapper is
   toggled opposite its desktop grid. Built in app.js every time
   its tab renders, right alongside that tab's desktop grid, so a
   resize/rotation across the breakpoint never needs a re-render,
   just a CSS switch. */
.spend-mobile-summary, .dash-mobile-list { display: none; }

.mob-totals {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
  flex-shrink: 0;
}

.mob-tot {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 7px 10px 6px;
  flex: 1 1 88px;
  min-width: 88px;
}

.mob-tot-v {
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.mob-tot-k {
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  margin-top: 2px;
}

/* The scrollable part — totals above stay pinned/visible, this is
   the only piece that scrolls (vertically only, never sideways),
   sized by fitFillHeight() in app.js so it fills exactly down to
   the bottom of the viewport instead of adding page-level scroll. */
.mob-list {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  min-height: 0;
}

.mob-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  margin-bottom: 6px;
  overflow: hidden;
}

/* Mobile equivalent of .dash-table tr.grp-other — the "OTHER" group
   subhead, styled to match. Spend has no leftover-account grouping,
   so this only ever appears in the Dashboard's mobile card list. */
.mob-grp-other {
  padding: 10px 2px 4px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}

.mob-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 10px;
  cursor: pointer;
}

/* Re-assert .tag's own small pill padding and add the MAIN/SUB
   coloring here explicitly — the bare "main"/"sub" modifier class
   this reuses (same markup pattern as renderDashboard()'s desktop
   table tags) collides with the page's unrelated, unscoped .main
   rule (the "26px 30px 60px" .main content-area padding), which
   otherwise blows this pill up into a large block. Confirmed via a
   standalone render test before this was caught. */
.mob-card-head .tag {
  padding: 2px 8px;
  flex-shrink: 0;
}
.mob-card-head .tag.main { color: var(--m-transferred); border-color: var(--m-transferred); }
.mob-card-head .tag.sub  { color: var(--dim); border-color: var(--line); }

.mob-name {
  flex: 1 1 auto;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The one headline number shown collapsed, next to the name —
   Spend's daily total, Dashboard's delivered count. Colour comes
   from whichever m-* class the caller adds alongside this. */
.mob-hero {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.mob-caret {
  color: var(--dim);
  transition: transform 0.15s ease;
  flex-shrink: 0;
}

/* Rotate the caret and reveal the detail rows on tap — see each
   tab's own .onclick handler (renderSpendGrid / renderDashboard)
   that toggles this class. */
.mob-card.open .mob-caret { transform: rotate(90deg); }

.mob-card-sub {
  display: flex;
  gap: 14px;
  padding: 0 10px 8px;
  font-size: 11px;
  color: var(--dim);
}

.mob-card-sub b { color: var(--text); font-weight: 600; }

.mob-detail {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.2s ease;
  border-top: 1px solid transparent;
}

.mob-card.open .mob-detail {
  /* Tall enough for every detail row at its compact row height
     below (24 hours for Spend, ~7 stats for Dashboard) — a fixed
     cap rather than a measured height keeps the transition simple;
     any leftover space just scrolls with the rest of .mob-list. */
  max-height: 640px;
  border-top-color: var(--line);
}

.mob-row {
  display: flex;
  justify-content: space-between;
  padding: 5px 10px;
  font-size: 12px;
  border-bottom: 1px solid var(--line);
}

.mob-row:last-child { border-bottom: none; }

.mob-row-lbl { color: var(--dim); }

.mob-row-val {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* Spend's "current hour" row highlight — Dashboard's detail rows
   don't use .sp-now, so this simply never matches there. */
.mob-row.sp-now .mob-row-lbl,
.mob-row.sp-now .mob-row-val {
  color: var(--m-spend);
  font-weight: 700;
}

/* ---- Stats panel: cards (reuse .dchips/.dchip) + trend chart ---- */

.spend-chart svg {
  width: 100%;
  height: 140px;
  display: block;
}

@media (max-width: 860px) {
  /* Without align-content:start, the grid's implicit rows
     (sidebar row + main row) stretch to fill .shell's
     min-height:100vh, ballooning the sidebar's row track far
     past its actual content height and leaving a large blank
     gap above the nav row. Packing rows at the start instead
     lets any leftover height fall after the last row, where it
     belongs. */
  .shell.on {
    grid-template-columns: 1fr;
    align-content: start;
  }

  /* The sidebar becomes an off-canvas drawer — same vertical
     layout as the desktop-width sidebar above (wordmark, nav list,
     sign-out foot all unchanged, just the same column it already
     is — the live-calls chip lives outside the sidebar now, see
     .live-calls-topbar) — instead of the previous horizontal
     scrolling top-tab-bar treatment. Hidden fully off-screen by
     default; the hamburger button (.nav-hamburger, always visible
     at this breakpoint) slides it in, and picking
     a nav item — or tapping the backdrop, or the hamburger again —
     slides it back out. See app.js's openSidebar/closeSidebar and
     the nav-btn click handler. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 180;
    width: 240px;
    max-width: 82vw;
    height: 100vh;
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    box-shadow: 2px 0 16px rgba(0, 0, 0, 0.35);
  }

  .sidebar.open { transform: translateX(0); }

  .nav-hamburger { display: flex; }

  /* Room for the fixed hamburger button, which now sits above the
     content instead of a sticky nav row pushing it down. */
  .main { padding: 60px 14px 48px; grid-column: 1; }

  .grid-2 { grid-template-columns: 1fr; }

  /* Wide data tables (campaigns, contacts, etc.) get their own
     horizontal scrollbar instead of blowing out the page width.
     .spend-table and .dash-table are excluded — they already have
     their own scroll wrapper with frozen headers/footer that
     depend on real table layout (colgroup-based column widths
     shared across separate head/body/foot tables), which
     `display:block` breaks: the table stops laying out columns
     against the shared colgroup at all, so the head and body
     tables silently drift to different natural widths and their
     columns no longer line up. */
  table:not(.spend-table):not(.dash-table) {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    white-space: nowrap;
  }

  /* Dashboard account table: same reasoning as the Spend table
     below — Account + 8 metric columns can't all fit at a legible
     width on a phone screen, so give each column a sane minimum
     and let the table scroll horizontally, with the header row
     tracking along (syncHScrollPanes in app.js) rather than being
     independently draggable — only the account rows underneath are
     meant to be swiped directly. Width itself is NOT set here
     (no `width: max-content`) — fitMobileTableWidth() in app.js sets
     an explicit pixel width on all frozen panes instead. table-layout:
     fixed only reliably apportions the declared column widths below
     once the table's OWN width is a concrete resolved number; asking
     the browser to resolve `max-content` on a fixed-layout table is
     unreliable and was confirmed (via direct measurement) to produce
     a different natural width per table — header, body, and footer
     each drifted to a different scrollWidth from the exact same
     colgroup, so they could never actually stay column-aligned no
     matter how well scrollLeft was kept in sync between them. */
  .dash-table col.col-dname { width: 132px; }
  .dash-table col:not(.col-dname) { width: 80px; }
  /* Placed after the rule above (same specificity) so it wins the
     tie -- Brand(s) Running still needs more than the flat 80px
     every other mobile data column gets, same reasoning as the
     desktop-width rule above. */
  .dash-table col.col-brands { width: 130px; }

  .dash-table th, .dash-table td { padding: 6px 8px; }

  /* Dashboard stat chips: on a phone there are 8 of them, and at
     their desktop size only ~3 fit per row — wrapping to a tall
     stack of rows above the table, all of which counts against the
     "stuff above the table" that fitFillHeight() has to subtract
     from the viewport height before the table gets whatever's left.
     Shrinking them meaningfully increases how much of the screen
     the actual table gets, and how many rows are visible before it
     needs its own internal scroll. */
  #view-dashboard .dchips { gap: 6px; margin-bottom: 8px; }

  #view-dashboard .dchip {
    min-width: 0;
    flex: 1 1 calc(25% - 5px);
    padding: 5px 8px 4px;
  }

  #view-dashboard .dchip .dc-v { font-size: 15px; }
  #view-dashboard .dchip .dc-k { font-size: 8px; margin-top: 1px; }

  /* overflow-x: hidden (not auto) — the header row itself isn't
     meant to be dragged; it just visually tracks whatever column
     is currently in view via scrollLeft, set programmatically by
     syncHScrollPanes() in app.js whenever the account rows below
     (the one thing meant to be swiped directly) scroll. Setting
     scrollLeft on a hidden-overflow element still shifts its
     content — hidden only blocks the user's own drag/scrollbar,
     not script control — so the header still stays aligned with
     whatever's scrolled into view without being interactive itself. */
  .dash-head-wrap {
    overflow-x: hidden;
    overflow-y: visible;
    flex-shrink: 0;
  }

  .dash-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Spend hourly table: on a narrow phone there just isn't room
     for Hour + every account + Total without crushing each column
     to a few px (headers truncate to one letter, values truncate
     to "$..."). Give every column a legible minimum width instead
     and let the table scroll horizontally — only the hour rows in
     the middle are meant to be dragged directly; the header and
     footer totals track along via syncHScrollPanes() in app.js
     rather than being swipeable in their own right (see the
     .spend-head-wrap / .spend-foot-wrap rule below).
     Width itself is NOT set here (no `width: max-content`) — see
     the long comment on .dash-table col.col-dname above for why:
     a fixed-layout table's `max-content` resolution isn't reliable
     enough to keep 3 separate tables (head/body/foot) at the same
     natural width, which silently breaks their column alignment.
     fitMobileTableWidth() in app.js sets an identical explicit
     pixel width on all three instead. */
  /* dname holds both the hour label ("6 PM") and the footer row
     labels ("STARTING" / "REMAINING" / "COMPLETED" / "POOL COST" /
     "DAILY TOTAL") — 104px alone was already enough to stop those
     labels truncating (desktop's own width), so 150px here is
     purely extra breathing room on request. Keep this in sync with
     MOBILE_COL_W.spend.dname in app.js — it has to match exactly,
     see the comment there for why. */
  .spend-table col.col-dname { width: 150px; }
  .spend-table col.col-total { width: 92px; }
  .spend-table col:not(.col-dname):not(.col-total) { width: 92px; }

  .spend-table th, .spend-table td { padding: 6px 8px; }

  /* Full-bleed the table out to the screen edges instead of sitting
     inside .main's normal side padding (14px each side at this
     breakpoint) — free width the table can actually use instead of
     idle margin, on top of the wider columns above. Desktop is
     unaffected; this whole block only applies at this breakpoint. */
  .spend-freeze {
    margin-left: -14px;
    margin-right: -14px;
    border-radius: 0;
    border-left: none;
    border-right: none;
  }

  /* overflow-y stays visible (not the shorthand `overflow`) — a
     flex item's automatic minimum size collapses to 0 once any
     overflow axis is non-visible, so `overflow: hidden` here let
     the column flex layout in .spend-freeze shrink this wrapper
     below its own table's height, silently clipping off the
     bottom footer rows (Pool cost / Daily total). flex-shrink:0
     additionally guarantees the row layout can't compress it.
     overflow-x is hidden, not auto — only the hour rows in the
     middle (.spend-scroll) are meant to be swiped directly; the
     header and footer just track along, kept at the same
     scrollLeft by syncHScrollPanes() in app.js whenever the hour
     rows scroll. Setting scrollLeft on a hidden-overflow element
     still shifts its content — hidden only blocks the user's own
     drag/scrollbar, not script control — so both stay visually
     aligned with whatever's scrolled into view without being
     independently draggable themselves. */
  .spend-head-wrap, .spend-foot-wrap {
    overflow-x: hidden;
    overflow-y: visible;
    flex-shrink: 0;
  }

  .spend-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Swap the frozen Hour×account grid out for the condensed
     summary + tap-to-expand cards built alongside it in
     renderSpendGrid() — see the long comment on
     .spend-mobile-summary above for why both are always built. */
  .spend-freeze { display: none; }

  .spend-mobile-summary {
    display: flex;
    flex-direction: column;
    /* Full-bleed to the screen edges, same reasoning as
       .spend-freeze's own mobile margin just above. */
    margin-left: -14px;
    margin-right: -14px;
    padding-left: 14px;
    padding-right: 14px;
  }

  /* Same swap for the Dashboard tab's account table, built
     alongside it in renderDashboard(). Dashboard's chips
     (.dchips, above the table) are left exactly as they are —
     already a condensed, wrapping summary strip on their own. */
  .dash-freeze { display: none; }

  .dash-mobile-list {
    display: flex;
    flex-direction: column;
    margin-left: -14px;
    margin-right: -14px;
    padding-left: 14px;
    padding-right: 14px;
  }
}

@media (max-width: 480px) {
  /* Keep the 860px breakpoint's top padding (room for the fixed
     hamburger button) — only side/bottom padding shrinks further
     at this narrower width. */
  .main { padding: 60px 10px 40px; }
  .view-head h2 { font-size: 15.5px; }
  .modal { padding: 16px; }
  .gate-card { padding: 20px; }

  /* .main's side padding shrinks to 10px at this narrower breakpoint
     — the .spend-freeze bleed margin above was sized for the 14px
     padding used up to 860px, so it needs to match here too or the
     table pokes 4px past the screen edge and adds an unwanted
     page-level horizontal scrollbar. */
  .spend-freeze {
    margin-left: -10px;
    margin-right: -10px;
  }

  .spend-mobile-summary {
    margin-left: -10px;
    margin-right: -10px;
    padding-left: 10px;
    padding-right: 10px;
  }

  .dash-mobile-list {
    margin-left: -10px;
    margin-right: -10px;
    padding-left: 10px;
    padding-right: 10px;
  }
}
