/* Voice Bot web — the shared Call On Cloud app shell.
   Tokens and shell rules come from cpanel/docs/COC_APP_SHELL_GUIDE.md (§3 + §6)
   with the current chrome layout from §11: organisation badge at the right end
   of the topbar, account chip pinned to the foot of the rail.

   Do not fork the token values per service. The whole point is that portal,
   agent, transcript and Voice Bot wear one theme. */

:root {
  --bg: #f6f8fb;
  --surface: #ffffff;
  --text: #0a0a0a;
  --text-muted: #64748b;
  --border: #e6ebf1;
  --border-strong: #cbd5e1;
  --ink: #0f172a;              /* primary dark surface: buttons, dark chips */
  --ink-hover: #1e293b;
  --radius-lg: 14px;
  --radius: 10px;
  --radius-sm: 7px;
  --transition: .18s cubic-bezier(.4, 0, .2, 1);

  --shadow-xs: 0 1px 2px rgba(15,23,42,.05);
  --shadow-sm: 0 1px 2px rgba(15,23,42,.04), 0 2px 6px rgba(15,23,42,.05);
  --shadow-md: 0 2px 4px rgba(15,23,42,.04), 0 6px 16px rgba(15,23,42,.08);
  --shadow-lg: 0 10px 30px rgba(15,23,42,.12);
  --shadow-xl: 0 24px 60px rgba(15,23,42,.22);
  --ring: 0 0 0 3px rgba(15,23,42,.12);

  /* The chevron every select wears. One SVG, so a native control and a filter
     pill cannot drift apart. */
  --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2364748b' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 6.5 8 10l3.5-3.5'/%3E%3C/svg%3E");

  /* The same chevron, drawn for a dark control. */
  --caret-dim: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%239aa1a9' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 6.5 8 10l3.5-3.5'/%3E%3C/svg%3E");

  --green: #16a34a;  --green-bg: #dcfce7;
  --amber: #d97706;  --amber-bg: #fef3c7;
  --red:   #dc2626;  --red-bg:   #fee2e2;
  --blue:  #2563eb;  --blue-bg:  #dbeafe;
  --slate-bg: #f1f5f9;

  /* ── App shell (dark topbar + collapsible rail) ── */
  --chrome:         #0b0d0f;
  --chrome-ink:     #e9eaec;
  --chrome-ink-dim: #9aa1a9;               /* 4.9:1 on --chrome */
  --chrome-border:  rgba(255,255,255,.09);
  --chrome-hover:   rgba(255,255,255,.08);

  /* one accent: blue — nav icons, active pill, focus */
  --brand:         #2563eb;
  --brand-on-dark: #4f9bff;
  --nav-active-bg: #dde8fd;
  --nav-hover-bg:  rgba(37,99,235,.07);

  --topbar-h:      56px;
  --sidebar-w:     248px;
  --sidebar-w-min: 68px;                   /* collapsed icon rail */
  --content-pad:   clamp(1rem, 3vw, 2.5rem);

  /* semantic z-index scale (Bootstrap dropdowns sit at 1000, above the chrome) */
  --z-backdrop: 190;
  --z-sidebar:  195;
  --z-topbar:   210;
}

*, *::before, *::after { box-sizing: border-box; }
html { height: 100%; }

body {
  margin: 0; min-height: 100vh;
  background: var(--bg); color: var(--text);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 14px; line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

[hidden] { display: none !important; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Inline code — a placeholder name, a column name, an id. Bootstrap ships this
   as pink (#d63384), which belongs to no palette in this product; app.css loads
   after it, so this is where it is answered. */
code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .875em; color: #1e40af;
}
::selection { background: var(--nav-active-bg); color: var(--ink); }
input, textarea, select { caret-color: var(--brand); }

/* ── Shell frame ─────────────────────────────────────────────────────────── */
.app-shell { min-height: 100vh; }
.app-body { display: flex; align-items: flex-start; }

/* ── Top bar (dark) ── */
.app-topbar {
  position: sticky; top: 0; z-index: var(--z-topbar);
  display: flex; align-items: center; gap: .5rem;
  height: var(--topbar-h); padding: 0 clamp(.75rem, 2vw, 1.25rem);
  background: var(--chrome); color: var(--chrome-ink);
  border-bottom: 1px solid var(--chrome-border);
}
.chrome-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex-shrink: 0; border: 0;
  border-radius: var(--radius-sm); background: transparent;
  color: var(--chrome-ink); font-size: 1.15rem; cursor: pointer;
  text-decoration: none;                    /* .chrome-back is an <a> */
  transition: background-color var(--transition);
}
.chrome-toggle:hover { background: var(--chrome-hover); color: #fff; }
.chrome-toggle:focus-visible { outline: 2px solid var(--chrome-ink); outline-offset: 2px; }

/* The drawer toggle is the only way to reach the nav under 992px. On desktop the
   rail is always open, so the button would do nothing — hide it and give the
   slot to the back link. Re-show it (and set DESKTOP_COLLAPSE = true in
   chrome.js) to bring the 68px icon rail back. */
@media (min-width: 992px) {
  .chrome-drawer-toggle { display: none; }
}

.chrome-brand {
  display: inline-flex; align-items: center; gap: .55rem; flex-shrink: 0;
  text-decoration: none; color: var(--chrome-ink);
  padding: 0 .35rem; border-radius: var(--radius-sm);
  font-weight: 700; letter-spacing: -0.01em; font-size: .95rem;
}
.chrome-brand:hover { color: #fff; }
.chrome-brand:focus-visible { outline: 2px solid var(--chrome-ink); outline-offset: 2px; }
/* 34px is the box height the shell guide fixes for every COC service. Do not
   re-size it for this artwork's padding — an equal box paints an equal mark. */
.chrome-brand .brand-logo { height: 34px; width: auto; display: block; flex-shrink: 0; }
.chrome-brand .brand-word { white-space: nowrap; }
@media (max-width: 419.98px) { .chrome-brand .brand-word { display: none; } }

.chrome-divider { width: 1px; height: 24px; background: var(--chrome-border); margin: 0 .35rem; flex-shrink: 0; }
@media (max-width: 575.98px) { .chrome-divider { display: none; } }

.chrome-spacer { flex: 1 1 auto; }

/* Organisation badge — right end of the bar. Outlined, not filled: it states
   which organisation this session is scoped to, and it is never a control. One
   org = one session here, so there is nothing to switch. */
.chrome-org {
  display: inline-flex; align-items: center; gap: .4rem;
  flex-shrink: 1; min-width: 0; max-width: 46vw;
  padding: .26rem .65rem;
  border: 1px solid var(--chrome-border); border-radius: 999px;
  color: var(--chrome-ink); font-size: .8125rem; font-weight: 600;
}
.chrome-org .bi { flex-shrink: 0; font-size: .875rem; color: var(--chrome-ink-dim); }
.chrome-org .org-static { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Phones: the badge would ellipsize down to an icon and two letters, and the
   account menu at the foot of the rail spells the organisation out in full. */
@media (max-width: 575.98px) { .chrome-org { display: none; } }

/* Available credit — the last thing on the bar, and the only badge up here that
   is a CONTROL: it opens /credit, so it gets a hover state the organisation
   badge deliberately does not. Same pill, filled rather than outlined, because
   a figure that changes has to be findable without reading the bar. */
.chrome-credit {
  display: inline-flex; align-items: baseline; gap: .4rem; flex-shrink: 0;
  padding: .26rem .7rem;
  border: 1px solid transparent; border-radius: 999px;
  background: var(--chrome-hover); color: var(--chrome-ink);
  font-size: .8125rem; font-weight: 600; text-decoration: none;
  transition: background-color var(--transition), color var(--transition);
}
.chrome-credit:hover { background: rgba(255,255,255,.16); color: #fff; }
.chrome-credit:focus-visible { outline: 2px solid var(--chrome-ink); outline-offset: 2px; }
.chrome-credit .bi { align-self: center; font-size: .875rem; color: var(--chrome-ink-dim); }
.chrome-credit .credit-value { font-variant-numeric: tabular-nums; font-size: .875rem; }
.chrome-credit .credit-unit { font-weight: 500; color: var(--chrome-ink-dim); font-size: .75rem; }
/* Low is a WARNING, not an error: there is still credit, and the run that
   empties it is the one that gets refused. Amber, and the unit word comes with
   it so the colour is never the only carrier. */
.chrome-credit.is-low { background: rgba(245,158,11,.18); color: #fde68a; }
.chrome-credit.is-low:hover { background: rgba(245,158,11,.28); color: #fffbeb; }
.chrome-credit.is-low .bi, .chrome-credit.is-low .credit-unit { color: #fcd34d; }
/* Phones: the figure survives, the word does not — it is the number somebody
   came up here for, and /credit spells the rest out. */
@media (max-width: 575.98px) { .chrome-credit .credit-unit { display: none; } }

/* Standalone lockup for pages without the shell (the login page). */
.topbar-brand {
  display: inline-flex; align-items: center; gap: .5rem;
  color: var(--text); font-weight: 800; letter-spacing: -0.02em;
}
.topbar-brand .brand-logo { height: 30px; width: auto; display: block; flex-shrink: 0; }

/* ── Sidebar ── */
.app-sidebar {
  position: sticky; top: var(--topbar-h); z-index: var(--z-sidebar);
  flex-shrink: 0; width: var(--sidebar-w); height: calc(100vh - var(--topbar-h));
  display: flex; flex-direction: column; background: var(--surface);
  border-right: 1px solid var(--border);
  /* overflow: visible on purpose — the footer's dropup is absolutely positioned
     and ANY clipping context here cuts it off. Scrolling belongs to the nav. */
  overflow: visible;
  transition: width var(--transition);
}
.app-sidebar-head { flex: 0 0 auto; padding: .875rem .625rem .5rem; }
.app-sidebar-nav {
  flex: 1 1 auto;                    /* takes the free space, pinning the foot */
  padding: .25rem .625rem 1rem; display: flex; flex-direction: column; gap: .125rem;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}

/* The rail SCROLLS; it does not squash. `.app-sidebar-nav` is a column flex box
   with a definite height, so once the groups made the content taller than the
   rail every child shrank to fit before `overflow-y: auto` ever got a say — and
   the first thing to go was the section label, half a row of "MESSAGING" cut
   off by its own overflow:hidden. */
.app-sidebar-nav > *,
.nav-group > *,
.nav-sub > * { flex: 0 0 auto; }

.nav-section-label {
  padding: 0 .5rem; font-size: .6875rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: #64748b;
  white-space: nowrap; overflow: hidden;
}
.app-sidebar-nav .nav-section-label { padding: .6rem .5rem .35rem; }

.nav-item {
  position: relative; display: flex; align-items: center; gap: .625rem;
  padding: .375rem .625rem; border-radius: var(--radius-sm); color: #1f2937;
  font-size: .9rem; font-weight: 500; text-decoration: none; white-space: nowrap;
  transition: background-color var(--transition), color var(--transition);
}
.nav-item:hover { color: #0f172a; background: var(--nav-hover-bg); }
.nav-item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.nav-item.is-active { color: #111827; background: var(--nav-active-bg); font-weight: 600; }
.nav-item .nav-icon {
  flex-shrink: 0; width: 20px; font-size: 1rem; line-height: 1; text-align: center;
  color: var(--brand); -webkit-text-stroke: 0.5px currentColor;   /* thicken the outline */
}
.nav-item .nav-text { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; }
/* A nav item for a screen that does not exist yet. It reads as a plan, not as a
   broken link: dimmed, not clickable, and it says which phase brings it. */
.nav-item.is-soon { color: #94a3b8; cursor: default; pointer-events: none; }
.nav-item.is-soon .nav-icon { color: #cbd5e1; }
.nav-soon {
  margin-left: auto; flex: none; padding: 0 .4rem;
  border-radius: 999px; background: var(--slate-bg); color: #94a3b8;
  font-size: .625rem; font-weight: 700; letter-spacing: .04em; line-height: 1.25rem;
}

/* ── Nav groups ──────────────────────────────────────────────────────────
   The parent row is a LINK to the group's unfiltered screen, with the caret
   beside it as a second, separate hit target. The same click still means one
   thing wherever it lands: on the row it navigates, on the caret it expands.
   The children carry no icon. An indent and the guide line already say they
   belong to the row above, and a second column of icons in a rail this narrow
   reads as another list of top-level screens. ── */
.nav-group { display: flex; flex-direction: column; }
/* The caret is a sibling of the link, not a child of it — nesting a button in
   an anchor is invalid, and the browser would hand the click to the link. It
   is then laid OVER the link's right end rather than beside it, so the row
   keeps the same full-width pill as every ungrouped item; the button is on
   top, so the click still splits cleanly. */
.nav-parent-row { position: relative; display: flex; }
.nav-parent { flex: 1 1 auto; min-width: 0; padding-right: 2rem; }
/* The group HOLDING the current page, which is not the same as being on it —
   the child wears the active pill, the parent only says where you are. */
.nav-parent.is-current { color: #111827; font-weight: 600; }
.nav-caret {
  position: absolute; right: .25rem; top: 50%; transform: translateY(-50%);
  display: grid; place-items: center;
  width: 1.5rem; height: 1.5rem; padding: 0;
  border: 0; border-radius: var(--radius-sm); background: transparent;
  font-size: .7rem; color: #94a3b8; cursor: pointer;
  transition: color var(--transition);
}
.nav-caret:hover { color: #1f2937; }
.nav-caret:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.nav-caret .bi {
  transform: rotate(-90deg); transition: transform var(--transition);
}
.nav-group.is-open .nav-caret .bi { transform: rotate(0deg); }

/* display:none rather than a height animation: the links inside a collapsed
   group would still take Tab focus, and every fix for that fights the
   transition it was added for. */
.nav-sub { display: none; position: relative; padding-bottom: .125rem; }
.nav-group.is-open > .nav-sub { display: flex; flex-direction: column; gap: .0625rem; }
/* The guide runs under the centre of the parent's icon, so a child hangs off
   the row above instead of reading as a top-level item with a wide indent. */
.nav-sub::before {
  content: ""; position: absolute; left: 1.25rem; top: .0625rem; bottom: .1875rem;
  width: 1px; background: var(--border);
}
.nav-child {
  padding-left: 2.5rem;               /* lines the label up with the parent's */
  font-size: .8375rem; font-weight: 500; color: #475569;
}
.nav-child:hover { color: #0f172a; }
/* A child is marked on the GUIDE LINE, not with a pill. The line is already
   there saying "these hang off the row above"; the current one simply turns
   solid and brand-coloured. A full-width block at this indent reads as a
   second, louder level than the parent it belongs to. */
.nav-child.is-active {
  color: #111827; font-weight: 600;
  background: transparent;            /* undoes .nav-item.is-active's pill */
}
.nav-child.is-active::before {
  content: ""; position: absolute; left: calc(1.25rem - 1px);
  top: .125rem; bottom: .125rem; width: 2px; border-radius: 1px;
  background: var(--brand);
}

/* ── Account chip, pinned to the foot of the rail (menu opens upward) ── */
.app-sidebar-foot {
  flex: 0 0 auto; padding: .5rem .625rem .625rem;
  border-top: 1px solid var(--border); background: var(--surface);
}
.user-chip {
  position: relative;                 /* anchor for the collapsed-rail tooltip */
  display: flex; align-items: center; gap: .55rem; width: 100%;
  padding: .375rem .5rem; border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--text); font-size: .875rem;
  text-align: left; cursor: pointer; transition: background-color var(--transition);
}
.user-chip:hover, .user-chip[aria-expanded="true"] { background: var(--nav-hover-bg); }
.user-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.user-chip::after { display: none; }                    /* kill the Bootstrap caret */
.user-chip .bi-chevron-expand { flex-shrink: 0; font-size: .8rem; color: #64748b; }
.user-avatar {
  flex-shrink: 0; display: inline-grid; place-items: center;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--ink); color: #fff; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .02em;
}
.user-meta { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
.user-name { font-size: .8125rem; font-weight: 600; color: #1f2937; }
.user-mail { font-size: .72rem; color: #64748b; }
.user-name, .user-mail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Line the menu up with the chip instead of Bootstrap's own min-width. */
.portal-menu { min-width: 240px; }
.app-sidebar-foot .dropdown-menu.portal-menu { min-width: calc(var(--sidebar-w) - 1.25rem); }
.menu-identity { padding: .4rem .65rem .3rem; }
.menu-org {
  display: flex; align-items: center; gap: .45rem; min-width: 0;
  font-size: .78rem; font-weight: 600; color: var(--text);
}
.menu-org .bi { flex-shrink: 0; color: var(--text-muted); }
.menu-org span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-roles { padding: .1rem .65rem .35rem; font-size: .7rem; color: var(--text-muted); }
.dropdown-item { display: flex; align-items: center; gap: .55rem; border-radius: var(--radius-sm); padding: .45rem .65rem; }
.dropdown-item:hover, .dropdown-item:focus { background: rgba(15,23,42,.06); }
.dropdown-item.danger { color: var(--red); }
.dropdown-item.danger:hover, .dropdown-item.danger:focus { background: var(--red-bg); color: #b91c1c; }

/* ── Collapsed icon rail — desktop only ── */
@media (min-width: 992px) {
  .app-shell.is-collapsed .app-sidebar { width: var(--sidebar-w-min); }
  .app-shell.is-collapsed .app-sidebar-nav { overflow: visible; }   /* let tooltips escape */
  .app-shell.is-collapsed .app-sidebar .nav-text,
  .app-shell.is-collapsed .app-sidebar .nav-soon,
  .app-shell.is-collapsed .app-sidebar .nav-section-label,
  .app-shell.is-collapsed .app-sidebar .nav-sub,
  .app-shell.is-collapsed .app-sidebar .nav-caret { display: none; }
  .app-shell.is-collapsed .app-sidebar .nav-item { justify-content: center; padding-left: 0; padding-right: 0; }
  .app-shell.is-collapsed .app-sidebar .nav-icon { width: auto; }
  .app-shell.is-collapsed .app-sidebar-head { display: none; }

  .app-shell.is-collapsed .app-sidebar-foot { padding-left: .35rem; padding-right: .35rem; }
  .app-shell.is-collapsed .user-chip { justify-content: center; padding-left: 0; padding-right: 0; }
  .app-shell.is-collapsed .user-meta,
  .app-shell.is-collapsed .user-chip .bi-chevron-expand { display: none; }

  /* Custom tooltip for the collapsed rail (not the browser's title attribute).
     `display: block` is required: .user-chip::after is set to none above to kill
     the Bootstrap caret, and that rule would otherwise win. */
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::after {
    content: attr(data-tip); display: block;
    position: absolute; left: calc(100% + 10px); top: 50%;
    transform: translateY(-50%) scale(.96); transform-origin: left center;
    background: #111827; color: #fff; font-size: .8rem; font-weight: 500; line-height: 1;
    padding: .45rem .6rem; border-radius: 7px; white-space: nowrap;
    box-shadow: 0 8px 24px rgba(2,6,23,.3); opacity: 0; pointer-events: none;
    transition: opacity .13s ease, transform .13s cubic-bezier(.4,0,.2,1); z-index: var(--z-topbar);
  }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::before {
    content: ""; position: absolute; left: calc(100% + 4px); top: 50%;
    transform: translateY(-50%); border: 5px solid transparent; border-right-color: #111827;
    opacity: 0; pointer-events: none; transition: opacity .13s ease; z-index: var(--z-topbar);
  }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):hover::after,
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):focus-visible::after { opacity: 1; transform: translateY(-50%) scale(1); }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):hover::before,
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):focus-visible::before { opacity: 1; }
}

/* ── Content pane. No max-width: the content is what widens when the rail
   collapses, and capping it here is what stops that happening. ── */
main.app-content {
  flex: 1 1 auto; min-width: 0; width: 100%; max-width: none; margin: 0;
  padding: 0 var(--content-pad) 3rem;
}

/* The shared first-load gate reserves the real page layout underneath it, so
   revealing the data does not move the viewport or make controls pop in one by
   one. It covers only the content pane: navigation remains usable and stable. */
main.app-content.is-page-loading { position: relative; }
main.app-content.is-page-loading > :not(.page-loading-indicator) { visibility: hidden; }
.page-loading-indicator {
  position: absolute; z-index: 2; inset: 0 0 auto; height: calc(100dvh - var(--topbar-h));
  display: grid; place-items: center; background: var(--bg);
}
.page-spinner,
.loading::before {
  width: 1.75rem; height: 1.75rem; border: 2px solid var(--border-strong);
  border-top-color: var(--brand); border-radius: 50%;
  animation: page-spinner .7s linear infinite;
}
@keyframes page-spinner { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .page-spinner, .loading::before { animation-duration: 1.4s; }
}

/* A page that OWNS the viewport instead of growing past it. The list fills what
   is left after the head, the toolbar and the pager, and scrolls inside itself.

   A max-height on the table alone cannot do this: the thing above it is not a
   fixed height. A gate banner appears when this deployment cannot dial, the
   credit card appears when billing is on, and either one pushes a hand-tuned
   `calc(100vh - 19rem)` back into scrolling the whole page. Let the layout
   measure itself. */
main.app-content.page-fill {
  display: flex; flex-direction: column;
  min-height: calc(100vh - var(--topbar-h));
  padding-bottom: var(--content-pad);
}
main.app-content.page-fill > * { flex: 0 0 auto; }
/* The one child that takes the slack, and the chain of `min-height: 0` that
   lets it actually shrink — a flex item's default `min-height: auto` is what
   makes an overflowing table push its parent open instead of scrolling. */
/* No top margin of its own: what sits above this card is a banner that is
   usually not there at all, and a margin that survives its absence is a gap
   under the page head nobody put there. The spacing above this card belongs to
   whatever is above it. */
.page-fill > .card-x.fill { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; margin-top: 0; }
.page-fill > .card-x.fill > .card-x-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* The scroll belongs to the table's own box, not to the card body around it —
   that box is the one carrying the sticky head and the sticky last column.

   The floor is what stops "the page owns the viewport" from turning into a
   two-row window. On a short laptop with a gate banner above it, everything
   else in this column is a fixed height, so the table is the only thing left to
   take the shortfall — and it took it down to a strip that showed less than one
   row. Below the floor the card grows instead, and the page above `min-height`
   scrolls, which is the ordinary thing a browser does and the thing an operator
   already knows how to do. */
.page-fill .tbl-wrap { flex: 1 1 auto; min-height: 24rem; }

/* ── Mobile: off-canvas drawer ── */
.chrome-backdrop {
  position: fixed; inset: var(--topbar-h) 0 0; z-index: var(--z-backdrop);
  background: rgba(2,6,23,.45); opacity: 0; visibility: hidden;
  transition: opacity var(--transition), visibility var(--transition);
}
@media (max-width: 991.98px) {
  .app-sidebar {
    position: fixed; top: var(--topbar-h); left: 0; height: calc(100vh - var(--topbar-h));
    width: var(--sidebar-w); transform: translateX(-100%);
    box-shadow: var(--shadow-xl); transition: transform .22s cubic-bezier(.4,0,.2,1);
  }
  .app-shell.drawer-open .app-sidebar { transform: translateX(0); }
  .app-shell.drawer-open .chrome-backdrop { opacity: 1; visibility: visible; }
  :root { --content-pad: clamp(1rem, 4vw, 1.5rem); }
}

@media (prefers-reduced-motion: reduce) {
  .app-sidebar, .chrome-backdrop, .chrome-toggle, .nav-item, .user-chip { transition: none; }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::after,
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::before { transition: none; }
}

/* ── Page furniture ──────────────────────────────────────────────────────── */
.page-head { padding: 1.5rem 0 1.25rem; display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
/* The title column takes the slack and the actions keep their line. Without
   this a wide fact bar pushes Pause and Cancel onto a row of their own, which
   reads as a second toolbar rather than as this page's controls. */
.page-head > :first-child { flex: 1 1 320px; min-width: 0; }
.page-head > .page-actions { flex: 0 0 auto; }
.page-head h1 { font-size: 1.375rem; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 .2rem; }
.title-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .2rem; }
.title-row h1 { margin: 0; }
.page-head .page-sub { font-size: .875rem; color: #475569; margin: 0; max-width: 65ch; }
.page-head .page-sub:empty { display: none; }

/* What a record HOLDS, under the name of the thing that holds it.
   A list's fields decide its table's columns, what a campaign can say and what
   the add-contacts picker asks for — the shape of the thing, in other words —
   and for one release they lived behind a dropdown where nobody found them.
   A line under the title costs one row of the header and answers the first
   question somebody opens a list with.

   Baseline-aligned rather than centred: the chips are monospace and sit a
   little high in their pills, and `center` leaves the label looking dropped. */
.holds { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-top: .5rem; }
.holds-label { font-size: .8125rem; font-weight: 600; color: var(--text); }
.holds-note { font-size: .8125rem; color: var(--text-muted); max-width: 46ch; }
/* Quiet enough to read as part of the sentence rather than as a third button
   beside the two in the header, and it only appears for somebody who can act. */
.holds .btn-x.quiet.sm { padding: .18rem .5rem; }

/* The page's own explanation, folded into a mark beside the title. It used to
   be a paragraph under the `h1`, and the honest ones ran three or four lines —
   which pushed the first real thing on the page below the fold and read as
   preamble that everybody scrolls past after the first visit. Beside the title
   it is one hover away and costs the layout nothing.

   `.page-sub` stays for the RECORD's own subtitle on a detail page (a
   campaign's description, a list's counts): that is data about the thing on
   screen, not help about the screen, and hiding it behind a hover would hide
   the fact somebody came for.

   No JS and no Bootstrap tooltip: the text is in the DOM, `:hover` and
   `:focus-visible` reveal it, and a tap focuses the button — so touch works
   without a second code path. */
.page-help { position: relative; display: inline-flex; z-index: 5; }
.page-help-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 17px; height: 17px; padding: 0; border: 0; border-radius: 50%;
  background: var(--slate-bg); color: var(--text-muted);
  font-family: inherit; font-size: .6875rem; font-weight: 700; line-height: 1;
  cursor: help; transition: background var(--transition), color var(--transition);
}
.page-help-btn:hover, .page-help-btn:focus-visible {
  background: var(--border-strong); color: var(--ink);
}
.page-help-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.page-help-bubble {
  position: absolute; top: calc(100% + .55rem); left: -.6rem; z-index: 30;
  width: max-content; max-width: min(46ch, 78vw);
  padding: .55rem .7rem; border-radius: var(--radius-sm);
  background: var(--ink); color: #e2e8f0;
  font-size: .8125rem; font-weight: 400; line-height: 1.5;
  text-align: left; white-space: normal; box-shadow: var(--shadow-md);
  opacity: 0; visibility: hidden; transform: translateY(-3px);
  transition: opacity var(--transition), visibility var(--transition), transform var(--transition);
}
.page-help-bubble::before {
  content: ''; position: absolute; bottom: 100%; left: .8rem;
  border: 5px solid transparent; border-bottom-color: var(--ink);
}
.page-help-btn:hover + .page-help-bubble,
.page-help-btn:focus-visible + .page-help-bubble,
.page-help-bubble:hover { opacity: 1; visibility: visible; transform: none; }
@media (prefers-reduced-motion: reduce) { .page-help-bubble { transition: none; } }
/* On a phone the bubble hangs off a `?` that is already some way into the
   line, so `78vw` measured from THERE runs off the right-hand edge — a page
   that scrolls sideways for a tooltip nobody has opened. It is measured from
   the heading's own left edge instead, which is the one box that is never
   wider than the screen. The tail goes with it: an arrow pointing at the left
   margin points at nothing. */
@media (max-width: 767.98px) {
  main .title-row { position: relative; }
  main .page-help { position: static; }
  main .page-help-bubble {
    left: 0; max-width: min(46ch, calc(100vw - var(--content-pad) * 2));
  }
  main .page-help-bubble::before { display: none; }
}
.page-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

.card-x {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.card-x + .card-x { margin-top: 1rem; }
.card-x-head { padding: .9rem 1.15rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
/* Every card's own heading, whatever level the document outline needed it to
   be. `.card-x-title` was a class with no rule behind it for three screens:
   an `h2` wearing it looked right by accident and an `h3` fell through to the
   browser's default, which is how one card ended up in a different font from
   every other card on the page. */
.card-x-head h2, .card-x-head h3, .card-x-head .card-x-title {
  font-size: .95rem; font-weight: 600; margin: 0; letter-spacing: -0.01em;
}
.card-x-body { padding: 1.15rem; }

/* One question mark instead of a paragraph. The explanation still ships — it
   just stops competing with the controls it explains. */
.help {
  display: inline-grid; place-items: center; flex-shrink: 0;
  width: 22px; height: 22px; border: 0; border-radius: 50%;
  background: var(--slate-bg); color: #64748b; font-size: .7rem; cursor: help;
  transition: background-color var(--transition), color var(--transition);
}
.help:hover { background: #e2e8f0; color: var(--text); }
.help:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.btn-x {
  display: inline-flex; align-items: center; gap: .4rem; padding: .45rem .85rem;
  border-radius: var(--radius-sm); font-size: .8438rem; font-weight: 600;
  border: 1px solid var(--border-strong); background: #fff; color: var(--text);
  cursor: pointer; text-decoration: none; box-shadow: var(--shadow-xs);
  transition: all var(--transition);
}
.btn-x:hover { border-color: #94a3b8; background: #f8fafc; color: var(--text); box-shadow: var(--shadow-sm); }
.btn-x:active { transform: translateY(1px); box-shadow: none; }
.btn-x:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }

/* The mark that says a field cannot be left out. Red, because that is the
   one colour every form in the world has already taught, and never the only
   thing carrying the meaning — the control itself is `required` too. */
.required-mark { color: var(--red); font-weight: 700; margin-left: .15rem; }
.btn-x.pill-btn { border-radius: 999px; padding: .3rem .8rem; font-size: .8125rem; }
.btn-x.icon-only { justify-content: center; width: 36px; height: 36px; padding: 0; }
.btn-x.sm { padding: .22rem .6rem; font-size: .78rem; }
.btn-x.quiet { border-color: transparent; background: transparent; box-shadow: none; color: var(--text-muted); font-weight: 500; }
.btn-x.quiet:hover { background: var(--slate-bg); border-color: transparent; color: var(--text); box-shadow: none; }
.btn-x.primary { background: var(--ink); color: #fff; border-color: var(--ink); box-shadow: 0 1px 2px rgba(15,23,42,.25); }
.btn-x.primary:hover { background: var(--ink-hover); color: #fff; border-color: var(--ink-hover); box-shadow: var(--shadow-md); }

.banner { display: flex; gap: .6rem; align-items: flex-start; padding: .75rem .9rem; border-radius: var(--radius-sm); font-size: .8125rem; line-height: 1.45; }
.banner.warn { background: var(--amber-bg); color: #92400e; border: 1px solid #fde68a; }
.banner.err  { background: var(--red-bg);   color: #991b1b; border: 1px solid #fecaca; }
.banner.info { background: var(--blue-bg);  color: #1e40af; border: 1px solid #bfdbfe; align-items: center; }
/* Added in Phase 3: a campaign that started and a number that came back ready
   are both worth confirming, and saying so in the red error box would be worse
   than saying nothing. */
.banner.ok   { background: var(--green-bg); color: #15803d; border: 1px solid #bbf7d0; align-items: center; }
.banner .bi { flex-shrink: 0; margin-top: .1rem; }
/* A banner that carries a button — the rehearsal notice and its Go live. On a
   narrow screen the button drops below the sentence rather than being squeezed
   into two words down the right-hand edge. */
.banner.has-action { flex-wrap: wrap; }
.banner.has-action > .btn-x { flex-shrink: 0; margin-left: auto; align-self: center; }
@media (max-width: 575.98px) {
  .banner.has-action > .btn-x { margin-left: calc(1rem + 1.4em); }
}
.banner-list { margin: .35rem 0 0; padding-left: 1.05rem; }
.banner-list li { margin-top: .15rem; }

/* ── Toasts ──────────────────────────────────────────────────────────────── */
/* The acknowledgement of something that worked. It is not part of the page's
   layout — a banner between the header and the table pushes the very rows the
   operator just changed down the screen, and then stays there. This floats
   bottom-right, out of the reading path, and leaves on its own.
   Above the modal layer (1055) because an add closes its modal and confirms in
   the same breath, and the fading backdrop must not swallow the confirmation. */
.toast-stack {
  position: fixed; z-index: 1090; right: 1.25rem; bottom: 1.25rem;
  display: flex; flex-direction: column; align-items: flex-end; gap: .5rem;
  pointer-events: none;
}
.toast-chip {
  display: flex; align-items: center; gap: .5rem;
  max-width: min(380px, calc(100vw - 2.5rem));
  padding: .65rem .8rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: var(--text);
  font-size: .8125rem; font-weight: 500; line-height: 1.35;
  box-shadow: 0 10px 30px rgba(15, 23, 42, .16);
  cursor: pointer; pointer-events: auto;
  opacity: 1; transform: translateY(0);
  transition: opacity var(--transition), transform var(--transition);
}
.toast-chip.is-going { opacity: 0; transform: translateY(6px); }
.toast-chip .bi { flex-shrink: 0; font-size: .95rem; }
.toast-chip.is-ok .bi { color: var(--green); }
.toast-chip.is-err .bi { color: var(--red); }
@media (prefers-reduced-motion: reduce) { .toast-chip { transition: opacity .01ms; } }
@media (max-width: 575.98px) {
  .toast-stack { right: .75rem; left: .75rem; bottom: .75rem; align-items: stretch; }
}

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

/* ── Phase-0 overview: the roadmap list ──────────────────────────────────── */
.step-list { display: flex; flex-direction: column; }
.step {
  display: flex; align-items: flex-start; gap: .85rem;
  padding: .85rem 0; border-top: 1px solid var(--border);
}
.step:first-child { border-top: 0; padding-top: 0; }
.step-mark {
  flex-shrink: 0; display: inline-grid; place-items: center;
  width: 24px; height: 24px; margin-top: .05rem; border-radius: 50%;
  font-size: .8rem; background: var(--slate-bg); color: #94a3b8;
}
.step.is-done .step-mark { background: var(--green-bg); color: #15803d; }
.step-body { min-width: 0; }
.step-title { font-size: .875rem; font-weight: 600; margin: 0 0 .15rem; }
.step.is-todo .step-title { color: #475569; font-weight: 500; }
.step-note { font-size: .8125rem; color: var(--text-muted); margin: 0; }
.step-phase {
  flex-shrink: 0; margin-left: auto; align-self: center;
  padding: .1rem .5rem; border-radius: 999px;
  background: var(--slate-bg); color: #64748b;
  font-size: .6875rem; font-weight: 700; letter-spacing: .03em; white-space: nowrap;
}
.step.is-done .step-phase { background: var(--green-bg); color: #15803d; }

/* ── Tables, pills and empty states ──────────────────────────────────────── */
.card-x-body.flush { padding: 0; }
/* A table flush against the TOP of its card has to respect the card's corners.
   The heading row is a visible tint, and a square tint behind a 10px radius
   shows as two square shoulders on the card.

   Scoped to the case where the table really is the card's first row: Calls and
   Campaigns put a filter head above theirs, and rounding the heading there
   would notch two corners in the middle of the card. */
.card-x > .card-x-body.flush:first-child .tbl thead tr:first-child th:first-child {
  border-top-left-radius: var(--radius);
}
.card-x > .card-x-body.flush:first-child .tbl thead tr:first-child th:last-child {
  border-top-right-radius: var(--radius);
}
.tbl-wrap { overflow-x: auto; }
.tbl { width: 100%; border-collapse: collapse; font-size: .8438rem; }
.tbl th {
  padding: .6rem 1.15rem; text-align: left; white-space: nowrap;
  border-bottom: 1px solid var(--border); background: var(--slate-bg);
  font-size: .6875rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: #64748b;
}
.tbl td { padding: .7rem 1.15rem; border-bottom: 1px solid var(--border); vertical-align: middle; }

/* A header that sorts. It has to look like the header it replaced and act like
   a button, so it keeps every one of the `th`'s own type metrics and brings
   nothing of its own but the arrow and the hit area. Padding is moved off the
   `th` and onto the button, or the clickable part would be a word in the middle
   of a cell the operator is already aiming at. */
.tbl th.th-sortable { padding: 0; }
.th-sort {
  display: inline-flex; align-items: center; gap: .35rem; width: 100%;
  padding: .6rem 1.15rem; border: 0; background: none; cursor: pointer;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  transition: color var(--transition), background-color var(--transition);
}
.th-sort:hover { color: var(--text); background: #eef2f7; }
.th-sort:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* The column doing the work, and the ONLY one wearing an arrow at rest. Seven
   grey arrows across a header tell the reader nothing about which one is doing
   the work and read as clutter; the neutral one appears under the cursor, where
   it is answering "can I sort by this". `opacity` rather than `display`, so the
   heading does not shift sideways when it appears. */
.th-sort.is-active { color: var(--brand); }
.th-sort i { font-size: .8rem; opacity: 0; transition: opacity var(--transition); }
.th-sort:hover i, .th-sort:focus-visible i { opacity: .55; }
.th-sort.is-active i, .th-sort.is-active:hover i { opacity: 1; }

/* A date column. Quiet and unwrapped, like `.num`, but never right-aligned —
   the header and the cell have to start on the same pixel, and `.num` earned a
   right-aligned heading for digits that this column does not have. */
.tbl :is(th, td).date { white-space: nowrap; }
.tbl td.date { color: var(--text-muted); }

/* ── A table built for four thousand rows ────────────────────────────────── */
/* `.dt-scroll` is the box that scrolls, and it has to be the box rather than
   the page: a sticky head is sticky against its nearest scrolling ancestor, and
   a wrapper with only `overflow-x` has no vertical scroll to stick inside. The
   ceiling is a viewport fraction with a floor under it, so a short laptop still
   gets a table rather than a strip, and the pager below it stays on screen
   instead of being four thousand rows away.

   The inline axis is contained: a horizontal overscroll that chained out of a
   wide table would be a browser Back. */
.tbl-wrap.dt-scroll {
  max-height: clamp(22rem, calc(100vh - 21rem), 46rem);
  overflow: auto; overscroll-behavior-inline: contain;
}
/* The head holds the top edge. Collapsed borders do not travel with a sticky
   cell, so the line under it is an inset shadow. */
.dt-scroll .tbl thead th {
  position: sticky; top: 0; z-index: 3;
  border-bottom: 0; box-shadow: inset 0 -1px 0 var(--border);
}

/* The tick column, and the column that says WHO the row is, hold the left edge.
   A list carrying eight uploaded fields is wider than the screen, and scrolling
   sideways away from the phone number leaves rows nobody can identify.

   The offset is why the tick column has a fixed width rather than `1%`: the
   identity column has to start exactly where it ends, and a shrink-to-fit
   checkbox cannot be measured in CSS. */
.tbl :is(th, td).tick-col {
  width: 2.6rem; min-width: 2.6rem; padding-right: 0; white-space: nowrap;
}
.dt-scroll .tbl :is(th, td).tick-col,
.dt-scroll .tbl :is(th, td).sticky-col {
  position: sticky; left: 0; z-index: 2; background: var(--surface);
}
.dt-scroll .tbl.has-ticks :is(th, td).sticky-col { left: 2.6rem; }
/* The seam, so a frozen column reads as frozen rather than as an overlap. */
.dt-scroll .tbl :is(th, td).sticky-col { box-shadow: 1px 0 0 var(--border); }
/* The corner cells are sticky in two directions and must sit above either. */
.dt-scroll .tbl thead :is(th.tick-col, th.sticky-col) { z-index: 5; background: var(--slate-bg); }
.dt-scroll .tbl thead th.sticky-col {
  box-shadow: inset 0 -1px 0 var(--border), 1px 0 0 var(--border);
}
/* And the right edge, for the column that is not there yet. A list eight
   fields wide is wider than the screen, and `+ Add custom field` scrolled off
   the end of it is a button nobody finds twice. */
.dt-scroll .tbl :is(th, td).col-add {
  position: sticky; right: 0; z-index: 2; background: var(--surface);
  box-shadow: -1px 0 0 var(--border);
}
.dt-scroll .tbl thead th.col-add {
  z-index: 5; background: var(--slate-bg);
  box-shadow: inset 0 -1px 0 var(--border), -1px 0 0 var(--border);
}
/* A sticky cell paints its own background, so every row state has to be
   repeated on it or the frozen column stays white under a tinted row. */
.dt-scroll .tbl :is(.row-link, .row-pick):hover :is(.tick-col, .sticky-col) { background: #f8fafc; }
.dt-scroll .tbl .is-picked :is(.tick-col, .sticky-col) { background: #eaf0fd; }

/* Compact is the default, and it is roughly a third more rows in the same
   window. It is a setting because a five-row list wants the opposite, which is
   the whole reason this is a choice and not a redesign. */
.tbl.is-compact td { padding-top: .42rem; padding-bottom: .42rem; }
.tbl.is-compact th { padding-top: .45rem; padding-bottom: .45rem; }
.tbl.is-compact .th-sort { padding-top: .45rem; padding-bottom: .45rem; }
.tbl.is-compact .row-note { font-size: .72rem; }
.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl td.num { white-space: nowrap; color: var(--text-muted); }

/* Report owns the remaining viewport. The shared `.dt-scroll` cap is useful on
   document pages with content below a table, but here it stopped the card well
   above the bottom edge and left an unrelated slab of page background. Let the
   visible view and its table body take the measured remainder instead. */
main.app-content.report-page {
  height: calc(100dvh - var(--topbar-h)); min-height: 0; overflow: hidden;
}
.report-page > section:not([hidden]) {
  flex: 1 1 auto; min-height: 0; display: flex;
}
.report-page > section:not([hidden]) > .card-x {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
}
.report-page > section:not([hidden]) > .card-x > .card-x-body {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
}
.report-page > section:not([hidden]) .tbl-wrap {
  flex: 1 1 auto; min-height: 0; max-height: none;
}
/* Removing the obsolete second line made Campaign rows noticeably tighter.
   Comfortable mode keeps the former scanning rhythm without putting internal
   execution labels back into the UI; Compact remains available in View. */
.report-page #runsView .tbl:not(.is-compact) td {
  padding-top: .9rem; padding-bottom: .9rem;
}
/* Six useful columns still have to share one viewport. Give each a budget so a
   long campaign identifier cannot make the browser grow the whole table. The
   campaign keeps the largest share and truncates; numeric/status columns keep
   enough room to remain readable. */
.report-page #runsView .tbl { table-layout: fixed; }
.report-page #runsView .tbl :is(th, td) { padding-left: .85rem; padding-right: .85rem; }
.report-page #runsView .tbl .th-sort { padding-left: .85rem; padding-right: .85rem; }
.report-page #runsView .tbl .report-campaign { width: 31%; }
.report-page #runsView .tbl .report-delivered { width: 17%; }
.report-page #runsView .tbl .report-responded { width: 11%; }
.report-page #runsView .tbl .report-date { width: 17%; }
.report-page #runsView .tbl .report-status { width: 14%; }
.report-page #runsView .tbl .report-credits { width: 10%; }
.report-page #runsView .tbl :is(.report-date, .report-credits) {
  overflow: hidden; text-overflow: ellipsis;
}
/* The per-recipient values. On a phone these wrapped into a six-line stack and
   made every row as tall as a card; the table already scrolls sideways, so let
   them run and let it scroll. */
.tbl :is(th, td).vals { white-space: nowrap; }
.recipients-table :is(th, td).vals { min-width: 140px; }
.row-title { display: block; font-weight: 600; color: var(--text); }
.row-note { display: block; margin-top: .1rem; font-size: .78rem; color: var(--text-muted); }
.row-link { cursor: pointer; transition: background-color var(--transition); }
.row-link:hover { background: #f8fafc; }
.row-link:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* A row whose job is a checkbox. The whole row is the target — a 14px box is a
   hard thing to hit twenty times in a row, and every table like this one in
   every tool the operator already uses takes the row. The tint is what makes
   that discoverable: it says the row responds before anybody tries it. */
.row-pick { cursor: pointer; transition: background-color var(--transition); }
.row-pick:hover { background: #f8fafc; }
.row-pick.is-picked { background: var(--nav-hover-bg); }
.row-pick.is-picked:hover { background: var(--nav-active-bg); }
/* Nothing to pick: already a member, or a reader who may not write. */
.tbl tbody tr.is-fixed { cursor: default; }
.tbl tbody tr.is-fixed:hover { background: none; }

/* The action cell. Narrow and right-aligned, so the menu toggle sits on the
   table's right edge rather than floating in the middle of the last column.
   Its dropdown escapes `.tbl-wrap`'s scroll box through Popper's `fixed`
   strategy — see `vbChrome.wireRowMenus`, not CSS. */
.tbl :is(th, td).row-act {
  width: 1%; padding-left: .4rem; padding-right: .9rem; text-align: right;
  white-space: nowrap;
}
.tbl td.row-act { cursor: default; }
/* The menu is short and every item is a verb. `min-width` rather than a fixed
   one, because the explanation under the divider is a sentence. */
.row-menu { min-width: 200px; max-width: 260px; padding: .3rem; }
/* The row an open menu belongs to stays marked, so a menu floating over the
   rows below it is never mistaken for theirs. */
.tbl tr.menu-open { background: #f8fafc; }
.row-menu .dropdown-empty { padding: .35rem .65rem; font-size: .72rem; line-height: 1.4; }


/* Search on the left, filters on the right — the same head as Campaigns. The
   card carried a "Recent" title before, which named the list twice: the page
   heading above it already says Calls, and the filters decide what "recent"
   even means. */
.calls-card-head { gap: .5rem .75rem; }
@media (max-width: 767.98px) {
  .calls-card-head { flex-wrap: wrap; }
  .calls-card-head .calls-search,
  .calls-card-head > .page-actions { width: 100%; }
  .calls-card-head > .page-actions {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .calls-card-head .calls-search input,
  .calls-card-head .select-x { width: 100%; min-width: 0; }
}

/* The call history's own columns. The table itself is the shared data table
   (`.dt-scroll`), so what is left here is what is true about a phone number
   and an outcome: a number never wraps and its digits line up, and a badge in
   a cell of its own carries no trailing margin. */
.call-phone { white-space: nowrap; font-variant-numeric: tabular-nums; }
.call-outcome .pill { margin-right: 0; }
.call-source { color: var(--text); font-weight: 400; text-underline-offset: 2px; }
.call-source:hover { color: var(--brand); }
.call-open { text-align: right; }

/* A phone gets the same table everybody else does: it scrolls sideways with
   the When column frozen (`.dt-scroll`). The card-per-row layout that used to
   live here was a second design of the same screen, kept in step by hand, and
   it could not survive a column chooser — a row drawn from `data-label`
   attributes has to know its columns in CSS. */
@media (max-width: 575.98px) {
  .calls-page-head .page-actions { width: 100%; }
  .calls-card-head { align-items: stretch; }
}

/* Status, not decoration. Neutral by default so a colour always means something. */
.pill {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .08rem .45rem; margin-right: .3rem; border-radius: 999px;
  background: var(--slate-bg); color: #475569;
  font-size: .6875rem; font-weight: 700; letter-spacing: .02em; white-space: nowrap;
}
.pill.ok   { background: var(--green-bg); color: #15803d; }
.pill.warn { background: var(--amber-bg); color: #92400e; }
.pill.info { background: var(--blue-bg);  color: #1d4ed8; }
.pill.err  { background: var(--red-bg);   color: #b91c1c; }
/* Over, and nothing to do about it: darker than the default so "Cancelled"
   still reads as a decision somebody made rather than as absent data. */
.pill.off  { background: var(--slate-bg); color: #334155; }
/* The one outline in the set. A campaign nobody has started has not entered the
   run, and a filled chip would say it had. Inset shadow, not a border, so it
   does not shift the row against its filled neighbours. */
.pill.draft {
  background: transparent; color: #64748b;
  box-shadow: inset 0 0 0 1px var(--border-strong);
}
/* The only status that is an event rather than a state. The dot is what lets a
   list say "right now" without an operator rereading the word. */
.pill.live { background: var(--green-bg); color: #15803d; }
.pill.live::before {
  content: ''; width: .38rem; height: .38rem; border-radius: 50%;
  background: var(--green); animation: pill-live 1.6s ease-in-out infinite;
}
@keyframes pill-live { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .pill.live::before { animation: none; } }

.empty { padding: 3rem 1.15rem; text-align: center; }
.empty .bi { font-size: 1.75rem; color: #cbd5e1; }
.empty-title { margin: .6rem 0 .2rem; font-size: .9375rem; font-weight: 600; }
.empty-note { margin: 0; font-size: .8125rem; color: var(--text-muted); }
.empty .btn-x { margin-top: .9rem; }
.empty .btn-x.pill-btn { padding: .35rem .85rem; }
/* Two ways out of an empty state, side by side and wrapping on a phone rather
   than stacking into a column of equal-looking buttons. */
.empty-acts { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: center; }
.loading {
  min-height: 7rem; padding: 2rem 1.15rem; margin: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-muted); font-size: 0;
}
.loading::before { content: ''; display: block; flex: 0 0 auto; }
.pad { padding: 1rem 0; }
.hint { font-size: .8125rem; color: var(--text-muted); margin: 0 0 .9rem; max-width: 70ch; }
.hint code, .var-name { font-size: .78rem; background: var(--slate-bg); padding: .05rem .3rem; border-radius: 5px; }

.check { display: inline-flex; align-items: center; gap: .4rem; font-size: .8125rem; color: #475569; }
/* The way back, and it belongs to the TITLE, not to the chrome. It lives inside
   `.page-head` on every page that has one, so the 1.5rem above it is the page
   head's own padding — a crumb placed as a sibling above the head sat flush
   against the black bar with only a browser default margin holding it off.
   One shape too: an `<a class="crumb">`, never a `<p>` wrapping a bare `<a>`,
   which Bootstrap paints blue and underlines. */
.crumb {
  display: inline-flex; align-items: center; gap: .2rem; margin: 0 0 .3rem;
  font-size: .78rem; font-weight: 600; color: var(--text-muted); text-decoration: none;
  transition: color var(--transition);
}
.crumb:hover { color: var(--text); }
.crumb:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 4px; }
.crumb .bi { font-size: .72rem; }

/* ── Form fields ─────────────────────────────────────────────────────────── */
.field { display: block; margin-bottom: .85rem; }
.field:last-child { margin-bottom: 0; }
.field-label { display: block; font-size: .78rem; font-weight: 600; margin-bottom: .25rem; }
.field-value { float: right; font-weight: 600; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.field-input {
  display: block; width: 100%; padding: .4rem .6rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: #fff; color: var(--text); font: inherit; font-size: .8438rem;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.field-input:focus { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
.field-input:disabled { background: #f8fafc; color: #64748b; cursor: not-allowed; }
.field-input[type="range"] { padding: 0; border: 0; accent-color: var(--brand); }
.field-note { display: block; margin-top: .25rem; font-size: .72rem; color: var(--text-muted); line-height: 1.4; }
.field-note.lock { display: flex; align-items: center; gap: .35rem; margin-top: .85rem; padding-top: .75rem; border-top: 1px solid var(--border); }
/* The chosen voice, at the height of every other field on the row. It used to
   be a 52px tile with a hint line under the name, which made one field twice
   the weight of the control beside it for the sake of saying "click to
   listen" — something a chevron says for free. */
.voice-trigger {
  display: flex; align-items: center; gap: .5rem;
  padding: .4rem .6rem; text-align: left; cursor: pointer;
}
.voice-trigger:hover:not(:disabled) { border-color: #94a3b8; background: #f8fafc; }
#voicePickerName {
  flex: 0 1 auto; min-width: 0; overflow: hidden;
  font-weight: 650; text-overflow: ellipsis; white-space: nowrap;
}
.voice-trigger-tag {
  flex: none; padding: .05rem .4rem; border-radius: 999px;
  background: var(--slate-bg); color: var(--text-muted);
  font-size: .68rem; font-weight: 600;
}
.voice-trigger-tag:empty { display: none; }
.voice-trigger-arrow { flex: none; margin-left: auto; color: var(--text-muted); font-size: .7rem; }

/* ── The voice dialog ────────────────────────────────────────────────────── */
/* One list, one column, one click. The two gendered columns in a 1180px dialog
   were the shape of a comparison, and nobody compares twelve voices — they play
   two and pick one. Sex is a filter here rather than a wall, which is also the
   only shape that survives a catalogue that says nothing about sex.

   Nothing in here names a renderer: more than one sits behind this list, and
   which one speaks a voice is a property of the voice, never a question. */
@media (min-width: 576px) { .voice-modal-dialog { max-width: 520px; } }
.voice-modal-content { border: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-xl); overflow: hidden; }
.voice-modal-content .modal-header { padding: 1.1rem 1.15rem .9rem; background: #f8fafc; }
.voice-modal-content .modal-body { padding: .35rem .65rem .8rem; }

/* Outside the body, so the search and the chips stay put while the list
   scrolls under them. */
.voice-filters {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .7rem 1.15rem; border-bottom: 1px solid var(--border);
}
.voice-search { position: relative; flex: 1 1 180px; min-width: 0; }
.voice-search .bi {
  position: absolute; top: 50%; left: .6rem; transform: translateY(-50%);
  color: var(--text-muted); font-size: .8rem; pointer-events: none;
}
.voice-search input {
  width: 100%; padding: .34rem .6rem .34rem 1.85rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: #fff; color: var(--text); font: inherit; font-size: .8125rem;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.voice-search input:focus { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
.voice-search input::-webkit-search-cancel-button { cursor: pointer; }
.voice-chips { display: flex; gap: .25rem; }
.voice-chip {
  padding: .26rem .6rem; border: 1px solid var(--border); border-radius: 999px;
  background: #fff; color: var(--text-muted);
  font-size: .72rem; font-weight: 600; line-height: 1.3; cursor: pointer;
  transition: border-color var(--transition), background-color var(--transition), color var(--transition);
}
.voice-chip:hover { border-color: #94a3b8; color: var(--text); }
.voice-chip[aria-pressed="true"] { border-color: var(--ink); background: var(--ink); color: #fff; }
.voice-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.voice-list { display: flex; flex-direction: column; }
.voice-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
  gap: .1rem; border-radius: var(--radius-sm);
  transition: background-color var(--transition);
}
.voice-row:hover { background: #f8fafc; }
.voice-row.is-selected { background: var(--nav-hover-bg); }
/* The sample leads, because hearing one is what the dialog is for and it is
   the only control here that does not close it. */
.voice-sample {
  display: inline-grid; place-items: center; width: 30px; height: 30px; margin: .3rem;
  border: 1px solid var(--border-strong); border-radius: 50%;
  background: #fff; color: #475569; font: inherit; font-size: .75rem; cursor: pointer;
  transition: border-color var(--transition), background-color var(--transition), color var(--transition);
}
.voice-sample:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
.voice-sample.is-playing { border-color: var(--ink); background: var(--ink); color: #fff; }
/* A voice we hold no sample for is still a voice, and still choosable. The
   dead button says so more honestly than leaving it out of the list did. */
.voice-sample:disabled { opacity: .3; cursor: default; }
.voice-pick {
  display: grid; grid-template-columns: 30px minmax(0, 1fr) auto 18px;
  align-items: center; gap: .55rem;
  min-height: 46px; padding: .2rem .7rem .2rem .1rem;
  border: 0; background: none; color: var(--text); text-align: left; cursor: pointer;
}
.voice-pick:focus-visible, .voice-sample:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* A face, not a gender glyph. A ♀ beside a name is a symbol standing where a
   person should be, and it said the same thing fourteen times. */
.voice-face { display: block; border-radius: 50%; }
.voice-trigger-face { display: inline-grid; flex: none; }
.voice-trigger-face:empty { display: none; }
.voice-name {
  min-width: 0; overflow: hidden;
  font-size: .875rem; font-weight: 600; text-overflow: ellipsis; white-space: nowrap;
}
.voice-tag { color: var(--text-muted); font-size: .72rem; }
.voice-tick { color: var(--brand); font-size: .9rem; visibility: hidden; }
.voice-row.is-selected .voice-tick { visibility: visible; }
.voice-row.is-selected .voice-name { color: var(--brand); }
.voice-empty { margin: .9rem .5rem; color: var(--text-muted); font-size: .8125rem; text-align: center; }

/* ── A level, not a number ───────────────────────────────────────────────── */
/* Three answers to one ordered question, in one track — slower is left of
   faster because the order is real.

   Deliberately the same object as the dashboard's date period
   (`.period-segments` in overview.css): a pill track, a filled pill for the
   answer in force. Two segmented controls in one product that disagree about
   what a segmented control looks like is the thing the boss notices first.
   Change one and change the other. */
.level-toggle {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: .125rem;
  padding: .2rem; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); box-shadow: var(--shadow-sm);
}
.level-btn {
  padding: .45rem .75rem; border: 0; border-radius: 999px;
  background: none; color: var(--text-muted);
  font: inherit; font-size: .8125rem; font-weight: 600; line-height: 1.25;
  text-align: center; white-space: nowrap; cursor: pointer;
  transition: background-color var(--transition), color var(--transition), box-shadow var(--transition);
}
.level-btn:hover:not(:disabled):not([aria-checked="true"]) { color: var(--text); background: var(--slate-bg); }
.level-btn[aria-checked="true"] {
  color: #fff; background: var(--ink); box-shadow: 0 1px 2px rgba(15, 23, 42, .25);
}
.level-btn:disabled { cursor: default; }
.level-toggle:has(:disabled) { opacity: .65; }
.level-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

@media (max-width: 639.98px) {
  .voice-modal-content .modal-header { padding: 1rem .9rem .8rem; }
  .voice-filters { padding: .7rem .9rem; }
  .voice-modal-content .modal-body { padding: .35rem .4rem .8rem; }
  .voice-sample { width: 34px; height: 34px; }
}
.btn-x.danger { color: var(--red); border-color: #fecaca; }
.btn-x.danger:hover { background: var(--red-bg); border-color: #fca5a5; color: #991b1b; }
.btn-x:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.row-actions { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .25rem; }


/* ── The two steps ───────────────────────────────────────────────────────── */
/* Still capped, because a message read at 1400px wide is a message nobody
   proof-reads. The cap moved out by the width of one sidebar and no further:
   what the custom fields take is theirs, not the message's. The numbers earn
   their place because the order is real — words, then voice. */
.steps { display: flex; flex-direction: column; gap: 1rem; max-width: 1180px; }

/* The message and the blanks it leaves, side by side. The sidebar is a fixed
   column because it holds controls, not prose: letting it grow with the window
   would stretch four inputs across a screen and squeeze the only thing on this
   page anybody actually reads. */
.write-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
.write-main { min-width: 0; padding-right: 1.25rem; }
.write-side { min-width: 0; padding-left: 1.25rem; border-left: 1px solid var(--border); }
/* Under a laptop's width there is no room for two, and the message wins: the
   fields go back under it, which is where they were. */
@media (max-width: 1023.98px) {
  .write-grid { grid-template-columns: 1fr; }
  .write-main { padding-right: 0; }
  .write-side {
    margin-top: 1.15rem; padding: 1rem 0 0;
    border-left: 0; border-top: 1px solid var(--border);
  }
}
/* The add affordance at the foot of the list it adds to, dashed so it reads as
   the empty next row rather than as a fourth button competing with Publish. */
.add-var {
  width: 100%; margin-top: .6rem; justify-content: center;
  border-style: dashed; font-size: .8rem; padding: .34rem .7rem;
}

/* A dark bar, so a step reads as a lid on its own card rather than as one more
   pale row inside it. The mark, the title and the action sit on one centre
   line: the circle used to hang from the top of a single-line heading, which
   is the misalignment you see before you can name it. */
.step-head {
  display: flex; align-items: center; gap: .65rem;
  padding: .8rem 1.15rem;
  background: #000; color: #fff;
  border-bottom: 1px solid #000;
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}
/* On black the mark inverts: one white disc per step, unchanging. What is
   still missing is said in words under the header ("Before you can publish"),
   so the badge does not have to carry a second state as well. */
.step-no {
  flex-shrink: 0; display: inline-grid; place-items: center;
  width: 26px; height: 26px; border-radius: 50%;
  background: #fff; color: var(--ink);
  font-size: .78rem; font-weight: 700; font-variant-numeric: tabular-nums;
  transition: background-color var(--transition), color var(--transition);
}
.step-no .bi { font-size: .82rem; line-height: 1; }
.step-title { flex: 1 1 auto; min-width: 0; }
.step-title h2 { margin: 0; font-size: .95rem; font-weight: 600; letter-spacing: -0.01em; color: #fff; }
.step-title p { margin: .1rem 0 0; font-size: .78rem; color: rgba(255, 255, 255, .68); }
.step-meta { flex-shrink: 0; align-self: center; font-size: .78rem; }
/* A white button on a black bar reads as a hole punched in it. The action
   borrows the bar's own surface instead. */
.step-head .btn-x {
  background: rgba(255, 255, 255, .12); border-color: rgba(255, 255, 255, .28);
  color: #fff; box-shadow: none;
}
.step-head .btn-x:hover {
  background: rgba(255, 255, 255, .2); border-color: rgba(255, 255, 255, .45);
  color: #fff; box-shadow: none;
}
.step-head .btn-x:focus-visible { outline-color: #fff; }
/* Narrow: the title stops competing with its own metadata for the same line.
   A three-line heading beside a version pill is worse than two rows. */
@media (max-width: 575.98px) {
  .step-head { flex-wrap: wrap; }
  .step-title { flex-basis: calc(100% - 26px - .65rem); }
  .step-meta { margin-left: calc(26px + .65rem); align-self: flex-start; }
}

/* The head of a section inside a step. */
.sub-head { display: flex; align-items: center; gap: .4rem; margin-bottom: .65rem; }
.sub-head h3 { margin: 0; font-size: .85rem; font-weight: 600; }
.sub-head .btn-x, .sub-head .icon-btn { margin-left: auto; }
.sub-empty { margin: 0; font-size: .8125rem; }

.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1.15rem; }
@media (max-width: 639.98px) { .field-grid { grid-template-columns: 1fr; } }

/* A switch, for a block of settings that is either in play or not at all. It
   is not a checkbox: a checkbox is one of several answers to a question, this
   is the question. Bootstrap ships one, but its blue is not ours. */
.switch {
  display: inline-flex; align-items: center; gap: .55rem; margin: 0;
  cursor: pointer; font-size: .8125rem; font-weight: 550; color: var(--text);
}
.switch input { position: absolute; width: 0; height: 0; opacity: 0; }
.switch-track {
  position: relative; flex: none; width: 38px; height: 22px;
  border-radius: 999px; background: #cbd5e1;
  transition: background-color var(--transition);
}
.switch-track::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
  transition: transform var(--transition);
}
.switch input:checked + .switch-track { background: var(--brand); }
.switch input:checked + .switch-track::after { transform: translateX(16px); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--brand); outline-offset: 2px; }
.switch:has(input:disabled) { cursor: default; color: var(--text-muted); }
.switch input:disabled + .switch-track { opacity: .55; }

.dtmf-builder { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.dtmf-builder-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.dtmf-builder-head h3 { margin: 0 0 .2rem; font-size: .875rem; font-weight: 650; }
.dtmf-builder-head p, .dtmf-empty { margin: 0; color: var(--text-muted); font-size: .8125rem; }
.dtmf-empty { margin-top: .8rem; }
/* The keypad, and always all nine keys of it. A key an operator wrote into the
   message is found by reading down the column of numbers and typing what it
   means beside it; a blank key is a key nobody may press. So there is nothing
   to add and nothing to remove, and no dropdown of the digits still free. */
#responseKeys { margin-top: .8rem; }
.dtmf-pad { display: grid; gap: .4rem; max-width: 620px; }
.dtmf-key { display: grid; grid-template-columns: 22px minmax(0, 1fr) 4.5rem; align-items: center; gap: .55rem; }
.dtmf-key label { margin: 0; }
.dtmf-key-no {
  display: inline-grid; place-items: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: #e2e8f0; color: #475569;
  font-size: .7rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.dtmf-key.is-optout .dtmf-key-no { background: var(--amber-bg); color: var(--amber); }
.dtmf-key-note { grid-column: 2 / -1; font-size: .75rem; color: var(--amber); }
/* Which key takes somebody off the list is a property of a key, so it is a
   control on the key's own row and not a second dropdown of the same nine
   digits. Quiet until it is on: one of nine rows carries it, and the other
   eight must not look like eight buttons waiting to be pressed. */
.key-optout {
  justify-self: start; padding: .2rem .55rem; border-radius: 999px;
  border: 1px dashed var(--border); background: none; color: var(--text-muted);
  font-size: .7rem; font-weight: 600; line-height: 1.35; cursor: pointer;
  transition: color var(--transition), background-color var(--transition),
    border-color var(--transition);
}
.key-optout:hover:not(:disabled) { color: var(--amber); border-color: var(--amber); }
.key-optout[aria-pressed="true"] {
  border-style: solid; border-color: transparent;
  background: var(--amber-bg); color: var(--amber);
}
.key-optout:disabled { cursor: default; opacity: .55; }
@media (max-width: 575.98px) {
  .dtmf-builder-head { align-items: stretch; flex-direction: column; }
  .dtmf-builder-head .switch { align-self: flex-start; }
}

/* A grid of radio cards — "what should it do / say" in a create dialog. Same
   shape the `agent` service uses for its own create dialog, on purpose: two
   COC products asking the same question should not ask it in two layouts.

   The whole card is the target and the input is visually hidden but NOT
   `display: none`: it still has to take focus, answer the arrow keys and drive
   `:checked`. Removing it from the accessibility tree would leave a group of
   choices only a mouse can make.

   The note is clamped to two lines. A card whose height is set by the longest
   description makes every other card in the row taller for nothing, and the
   full text is on the screen the choice leads to. */
.pick { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .55rem; }
.pick-opt { display: block; margin: 0; cursor: pointer; }
/* Spans the row. Used for the option that is not one of a set — "Blank" beside
   four examples — which also keeps an odd count from leaving an orphan cell. */
.pick-opt.wide { grid-column: 1 / -1; }
.pick-opt input {
  position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;
}
.pick-body {
  display: flex; align-items: flex-start; gap: .65rem; height: 100%;
  padding: .8rem .85rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
  transition: border-color var(--transition), box-shadow var(--transition),
              background var(--transition);
}
.pick-opt:hover .pick-body { border-color: var(--border-strong); background: #fcfdfe; }
.pick-opt input:focus-visible + .pick-body { box-shadow: var(--ring); }
.pick-opt input:checked + .pick-body {
  border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink);
}
/* The icon tile. Tinted rather than filled: it labels the card, and five
   saturated squares in one dialog would compete with the primary button. */
.pick-icon {
  flex: none; display: grid; place-items: center;
  width: 2.1rem; height: 2.1rem; border-radius: var(--radius-sm);
  font-size: 1rem;
}
.pick-icon.tone-slate { background: var(--slate-bg);  color: #475569; }
.pick-icon.tone-green { background: var(--green-bg);  color: var(--green); }
.pick-icon.tone-blue  { background: var(--blue-bg);   color: var(--blue); }
.pick-icon.tone-amber { background: var(--amber-bg);  color: var(--amber); }
.pick-icon.tone-red   { background: var(--red-bg);    color: var(--red); }
.pick-text { min-width: 0; }
.pick-title { display: block; font-size: .8375rem; font-weight: 600; line-height: 1.3; }
.pick-note {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: .15rem; font-size: .75rem; color: var(--text-muted); line-height: 1.4;
}
@media (max-width: 575.98px) { .pick { grid-template-columns: 1fr; } }

/* A create dialog that asks a question with cards in it needs the room for
   two of them side by side. Bootstrap's `.modal-dialog` caps at 500px, which
   is right for a form and far too narrow for a choice.

   Above the 576px breakpoint only: below it Bootstrap already goes near
   full-width with its own margins, and a max-width here would fight that. */
@media (min-width: 576px) { .modal-create { max-width: 880px; } }

/* The line under a dialog's title, saying what the dialog is for. One sentence:
   a header that needs a paragraph is a dialog asking for too much. */
.modal-sub {
  margin: .2rem 0 0; font-size: .8125rem; color: var(--text-muted); line-height: 1.45;
}
/* Bootstrap centres the close button against the title. With a subtitle under
   it that puts the X halfway down the header instead of beside the heading. */
.modal-header:has(.modal-sub) { align-items: flex-start; }
/* Section-heading treatment for a field label, matching the create dialogs in
   the other COC services. Only where a dialog has real sections — on an
   ordinary form it makes every field shout. */
.field-label.caps {
  text-transform: uppercase; letter-spacing: .045em;
  font-size: .69rem; color: var(--text-muted); margin-bottom: .35rem;
}

/* Every step carries its action in its own head, in one shape. Deliberately
   quiet: there is one filled button on this page and it is Publish, so a step
   action that shouted would be competing with the only irreversible thing here. */
.step-action { flex-shrink: 0; align-self: center; padding: .32rem .7rem; font-size: .8rem; }
.sub-head .step-action { margin-left: auto; }

/* What is still standing between this draft and a publish. It appears only when
   there is something on it, and it sits under the title — within a glance of
   the button it disables, which is in the header. */
.blockers {
  max-width: 1180px; margin: 0 0 1rem; padding: .8rem .9rem;
  border: 1px solid #fde68a; border-radius: var(--radius-sm); background: var(--amber-bg);
}
.blockers-title {
  display: flex; align-items: center; gap: .4rem; margin: 0 0 .3rem;
  font-size: .8125rem; font-weight: 700; color: #92400e;
}
.blockers ul { margin: 0; padding-left: 1.35rem; font-size: .8125rem; color: #92400e; }
.blockers li { margin-top: .15rem; }

/* The insert menu, opened from the { } button on a part. */
.ins-menu { min-width: 240px; max-height: 264px; overflow-y: auto; padding: .3rem; }
.ins-menu .dropdown-item { gap: .5rem; font-size: .8125rem; border-radius: var(--radius-sm); }
.ins-menu .dropdown-item code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .78rem; color: #1e40af;
}
.ins-eg {
  margin-left: auto; max-width: 110px; font-size: .72rem; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dropdown-empty { margin: 0; padding: .45rem .65rem; font-size: .8125rem; color: var(--text-muted); }

.save-state { display: inline-flex; align-items: center; gap: .35rem; font-size: .78rem; font-weight: 500; color: var(--text-muted); }
.save-state .bi-circle-fill { font-size: .5rem; }
.save-state.dirty { color: var(--amber); font-weight: 600; }
.save-state.ok { color: var(--green); }
.spin { display: inline-block; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }

/* ── Segments ────────────────────────────────────────────────────────────── */
.seg-list { display: flex; flex-direction: column; gap: .5rem; }
.seg {
  display: grid; align-items: start;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "num text tools" "clip clip clip" ".   foot foot";
  gap: 0 .55rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: #fbfcfe; padding: .55rem .6rem;
  transition: border-color var(--transition), background-color var(--transition), box-shadow var(--transition);
}
.seg:hover { border-color: var(--border-strong); }
.seg:focus-within { border-color: var(--brand); background: #fff; box-shadow: var(--ring); }
/* The words get their own column, so a long line wraps inside it and never
   reaches the buttons. */
.seg-tools { grid-area: tools; display: flex; align-items: center; gap: .1rem; }
/* A narrow screen has no room for three columns: the buttons take the top row
   with the number, and the words run the full width underneath. */
@media (max-width: 639.98px) {
  .seg {
    grid-template-columns: auto 1fr;
    grid-template-areas: "num tools" "text text" "clip clip" "foot foot";
    gap: .35rem .55rem;
  }
  .seg-tools { justify-self: end; }
}
/* The part's number, and the size every other disc in a part is cut to — the
   play button below it is the same circle in the same column, so the two read
   as one edge down the left of the list rather than as two indents. */
.seg-num {
  grid-area: num; margin-top: 1px;
  display: inline-grid; place-items: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: #e2e8f0; color: #475569;
  font-size: .68rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
/* No frame, no scrollbar, no drag handle. The part is the frame; the words are
   the content of this page and get the largest type on it, at exactly the
   height they occupy — the only height at which an operator can check them.
   A textarea cannot colour part of its own text, so a backdrop mirrors it and
   the textarea's own glyphs go transparent. Nothing scrolls here — the box
   grows to fit — which is what makes that safe: no scroll offset to keep in
   step, only identical metrics. */
.seg-input { grid-area: text; position: relative; }
.seg-hl, .seg-text {
  margin: 0; padding: 0; border: 0;
  font: inherit; font-size: .9375rem; line-height: 1.6;
  white-space: pre-wrap; overflow-wrap: break-word;
}
.seg-hl {
  position: absolute; inset: 0; color: var(--text);
  pointer-events: none; user-select: none;
}
.seg-hl .ph { color: var(--brand); font-weight: 600; }
.seg-text {
  position: relative; display: block; width: 100%; min-height: 1.6em;
  resize: none; overflow: hidden; background: transparent;
  color: transparent; caret-color: var(--ink);
}
.seg-text:focus { outline: none; }
.seg-text::placeholder { color: #94a3b8; }
/* Translucent, because the glyphs it covers are painted by the layer below. */
.seg-text::selection { background: rgba(37, 99, 235, .22); }
/* The recording of this part, under the part. Only there once there is one:
   hidden, it takes no row, so an un-previewed message is exactly the page it
   was before. The dotted rule above it says the words below are not the
   operator's — they are what the voice will actually say. */
/* The row starts in the number's own column and carries the grid's column gap,
   so the play button lands under the number and the reading under the words it
   is a reading of. */
.seg-clip {
  grid-area: clip; display: flex; align-items: flex-start; gap: .55rem;
  margin-top: .45rem; padding-top: .45rem; border-top: 1px dashed var(--border);
}
.seg-reading {
  flex: 1 1 auto; min-width: 0;
  font-size: .8125rem; line-height: 1.5; color: var(--text-muted);
}
.seg-len { flex-shrink: 0; font-size: .72rem; padding-top: .15rem; font-variant-numeric: tabular-nums; }
/* The part playing right now. The whole reason the reading sits here is to let
   an operator follow the message by ear and by eye at the same time. */
.seg.is-playing { border-color: var(--brand); background: var(--nav-hover-bg); }
.seg.is-playing .clip-play { background: var(--brand); border-color: var(--brand); color: #fff; }
.seg-foot { grid-area: foot; display: flex; align-items: center; gap: .35rem; margin-top: .35rem; font-size: .72rem; }
.seg-foot:not(:has(> :not([hidden]))) { display: none; }
.seg-foot .seg-count { margin-left: auto; }
.seg-warn { display: inline-flex; align-items: center; gap: .3rem; color: #92400e; }
.seg-count { font-variant-numeric: tabular-nums; }
.seg-list .empty { padding: 2.25rem 1rem; }

.icon-btn {
  display: inline-grid; place-items: center; flex-shrink: 0;
  width: 26px; height: 26px; border: 0; border-radius: var(--radius-sm);
  background: transparent; color: #64748b; font-size: .8rem; cursor: pointer;
  transition: background-color var(--transition), color var(--transition);
}
.icon-btn:hover { background: rgba(15,23,42,.06); color: var(--text); }
.icon-btn.danger:hover { background: var(--red-bg); color: var(--red); }
.icon-btn:disabled { opacity: .35; cursor: not-allowed; }
.icon-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* ── Placeholders ────────────────────────────────────────────────────────── */
/* One card per custom field, standing in a column beside the message. It was a
   five-column table with a header row naming the controls — which is what a
   table of unlabelled inputs needs, and needs the width of five columns to do.
   Stacked, the name leads and the rest say what they are in their own
   placeholders; the card's frame is what separates one field from the next. */
.var {
  display: grid; gap: .35rem;
  padding: .55rem .6rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fbfcfe;
  transition: border-color var(--transition), background-color var(--transition), box-shadow var(--transition);
}
.var + .var { margin-top: .45rem; }
.var:hover { border-color: var(--border-strong); }
.var:focus-within { border-color: var(--brand); background: #fff; box-shadow: var(--ring); }
/* The name and its "Unused" chip and its delete share the top line: the name is
   what identifies the field, so it is the line you read to find one. */
.var-top { display: flex; align-items: center; gap: .3rem; }
.var-top .var-name-input { flex: 1 1 auto; min-width: 0; }
.var-top .pill { margin-right: 0; flex-shrink: 0; }
/* What it is read as, and an example of it — one question in two halves, so
   they stay on one line however narrow the column gets. */
.var-pair { display: grid; grid-template-columns: minmax(0, 118px) minmax(0, 1fr); gap: .35rem; }
/* The name is an identifier the renderer substitutes on, not prose — it is set
   in the face that shows an underscore for what it is. */
.var-name-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .8rem; font-weight: 600; color: #1e40af;
}
.var.is-stale .var-name-input { color: var(--text-muted); }
.var .field-input { font-size: .8rem; padding: .3rem .45rem; }

/* ── Preview player ──────────────────────────────────────────────────────── */
/* Playing one part. The whole message plays from the header — this is the
   button beside the reading of a single part, and the only survivor of the
   clip list that used to be the page's third step. */
.clip-play {
  flex-shrink: 0; display: inline-grid; place-items: center;
  width: 22px; height: 22px; border: 1px solid var(--border-strong); border-radius: 50%;
  background: #fff; color: #475569; font-size: .68rem; line-height: 1; cursor: pointer;
  transition: background-color var(--transition), border-color var(--transition), color var(--transition);
}
.clip-play:hover { border-color: var(--brand); color: var(--brand); }
.clip-play:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* ── Key/value detail (call detail, Phase 2) ─────────────────────────────── */
/* A finished call is a dozen small facts, most of which are absent most of the
   time — an unanswered call has no talk time, a call that never left the ground
   has no SIP cause. A definition list is the shape that survives that: rows
   that are not there simply are not rendered, and nothing has to hold a column
   open for them. */
.kv { display: grid; grid-template-columns: minmax(110px, auto) 1fr; gap: .35rem .9rem; margin: 0; }
.kv dt { font-size: .78rem; color: var(--text-muted); font-weight: 500; }
.kv dd { margin: 0; font-size: .8125rem; color: var(--text); overflow-wrap: anywhere; }
.kv dd code { font-size: .75rem; }
@media (max-width: 575.98px) { .kv { grid-template-columns: 1fr; gap: 0 0; }
  .kv dt { margin-top: .5rem; } }

/* Call detail is one record, not a dashboard. Outcome leads; stable facts and
   the optional recipient response follow in the same reading column. */
.call-record { max-width: 920px; }
.call-record > .card-x-body { padding: 1.4rem 1.5rem; }
.call-outcome-block { padding-bottom: 1.25rem; border-bottom: 1px solid var(--border); }
.call-outcome-block h2,
.call-response h2,
.call-problem h2 { margin: 0 0 .25rem; font-size: .9375rem; font-weight: 600; }
.call-outcome-block p,
.call-response p,
.call-problem p { max-width: 68ch; margin: 0; color: #475569; font-size: .875rem; }
.call-record-facts {
  display: grid; grid-template-columns: 88px minmax(0, 1fr) 88px minmax(0, 1fr);
  gap: .65rem 1rem; margin: 1.25rem 0 0;
}
.call-record-facts dt { color: var(--text-muted); font-size: .78rem; font-weight: 400; }
.call-record-facts dd {
  min-width: 0; margin: 0; color: var(--text); font-size: .8438rem;
  overflow-wrap: anywhere; font-variant-numeric: tabular-nums;
}
.call-record-facts a { color: var(--text); text-underline-offset: 2px; }
.call-record-facts a:hover { color: var(--brand); }
.call-response,
.call-problem { padding-top: 1.1rem; margin-top: 1.25rem; border-top: 1px solid var(--border); }
.call-problem h2 { color: #b91c1c; }

@media (max-width: 767.98px) {
  .call-record-facts { grid-template-columns: 82px minmax(0, 1fr); }
}

@media (max-width: 575.98px) {
  .call-record > .card-x-body { padding: 1.1rem; }
}

/* ── Campaign monitor (Phase 3) ──────────────────────────────────────────── */
/* A running campaign is one number an operator watches — how far through it is
   — plus the handful of counts that explain the number. The bar is deliberately
   plain: it is read at a glance from across a desk, and a striped animated one
   would say "working" when a paused campaign is not.

   The segments are stacked in the order they matter when something is wrong:
   delivered, then everything that will never be delivered. */
.meter { height: 8px; border-radius: 999px; background: var(--slate-bg); overflow: hidden; display: flex; }
.meter span { display: block; height: 100%; }
.meter .m-done { background: var(--green); }
.meter .m-failed { background: var(--red); }
.meter .m-skipped { background: var(--border-strong); }
/* The counts that used to sit in four boxes under the bar now read as a
   sentence beside it (`.runline`). Four cards for four integers was the page
   telling an operator that each number was a thing to study. */

/* ── Browser surfaces ────────────────────────────────────────────────────── */
/* The parts we did not draw. Selection and the caret were already themed at the
   top of this file; the scrollbar was not, and it shows on every table on every
   screen. Numerals in a column that is read down get the tabular figures. */
.tbl td.num { font-variant-numeric: tabular-nums; }

@supports selector(::-webkit-scrollbar) {
  .tbl-wrap, .ins-menu, .rows-wrap, .modal-body, .rail-body, .pane-body { scrollbar-width: thin; }
  :is(.tbl-wrap, .ins-menu, .rows-wrap, .modal-body, .rail-body, .pane-body)::-webkit-scrollbar {
    width: 10px; height: 10px;
  }
  :is(.tbl-wrap, .ins-menu, .rows-wrap, .modal-body, .rail-body, .pane-body)::-webkit-scrollbar-thumb {
    background: #d3dbe5; border: 3px solid var(--surface); border-radius: 999px;
  }
  :is(.tbl-wrap, .ins-menu, .rows-wrap, .modal-body, .rail-body, .pane-body)::-webkit-scrollbar-thumb:hover {
    background: #b6c2d1;
  }
  :is(.tbl-wrap, .ins-menu, .rows-wrap, .modal-body, .rail-body, .pane-body)::-webkit-scrollbar-track {
    background: transparent;
  }
}

/* ── Selects ─────────────────────────────────────────────────────────────── */
/* A filter that shipped as a bare browser select on three screens. Same metrics
   as `.btn-x`, because that is what it sits beside in every card head. */
.select-x {
  appearance: none; -webkit-appearance: none;
  display: inline-flex; padding: .45rem 2rem .45rem .7rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background-color: #fff; color: var(--text);
  font: inherit; font-size: .8125rem; font-weight: 500; line-height: 1.35;
  box-shadow: var(--shadow-xs); cursor: pointer;
  background-image: var(--caret);
  background-repeat: no-repeat; background-position: right .5rem center; background-size: 15px;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.select-x:hover { border-color: #94a3b8; }
.select-x:focus-visible { outline: none; border-color: var(--brand); box-shadow: var(--ring); }

/* ── The data table's toolbar ────────────────────────────────────────────── */
/* Search on the left and the controls that change the table on the right. It
   replaces a row of permanently-open dropdowns,
   which took the same width whether anything was filtered or not and read as
   "All lists · Active · Everyone" — three controls saying nothing about which
   was which. */
/* The bar is BLACK, and that is the one thing every table on this product now
   shares by name rather than by coincidence. It began on Campaigns
   (`.campaigns-card-head`), where the point was that a white toolbar above a
   grey heading row puts the lightest thing on top and makes the heading look
   like the toolbar; every list here had the same shape and the same problem,
   so the rule moved to the class they all wear.

   The colours are `--chrome*`, not new ones: this bar has to be the same black
   as the topbar and the rail, or the page wears two blacks. The heading row
   under it stays slate — the dark band says "controls", the slate one says
   "this is what the columns are", and the rows say themselves. */
.dt-bar {
  display: flex; align-items: center; justify-content: flex-start;
  gap: .6rem .75rem; flex-wrap: wrap; padding: .7rem 1.15rem;
  background: var(--chrome); color: var(--chrome-ink);
  border-bottom-color: transparent;
}
/* Only when the bar really is the card's first row. A card that opens with
   something else — a panel, a heading — would otherwise have two rounded
   shoulders cut into the middle of it. */
.card-x > .dt-bar:first-child { border-radius: var(--radius) var(--radius) 0 0; }
/* Keep search useful without letting a short query field stretch across the
   whole card. Every data-table index uses the same readable measure. */
.dt-bar .search {
  flex: 1 1 20rem; width: min(100%, 24rem); max-width: 24rem; min-width: 0;
}
.dt-bar .search input { width: 100%; }
/* The controls go dark WITH the bar rather than staying white holes punched in
   it. Translucent white, so one rule works whatever sits behind it. */
.dt-bar .search .bi { color: #cbd5e1; }
.dt-bar :is(.search input, .dt-btn) {
  background-color: rgba(255,255,255,.07);
  border-color: var(--chrome-border);
  color: var(--chrome-ink);
  box-shadow: none;
}
.dt-bar .search input { color: #fff; caret-color: #fff; }
.dt-bar .search input::placeholder { color: #cbd5e1; }
.dt-bar :is(.search input, .dt-btn):hover {
  background-color: rgba(255,255,255,.12);
  border-color: rgba(255,255,255,.28);
  color: #fff;
}
.dt-bar .dt-btn i { color: var(--chrome-ink-dim); }
.dt-bar .dt-btn:hover i { color: var(--chrome-ink); }
/* A control doing work says so in the one blue that survives on black. */
.dt-bar .dt-btn:is(.is-on, [aria-expanded="true"]),
.dt-bar .dt-btn.is-on i {
  color: var(--brand-on-dark); border-color: rgba(79,155,255,.5);
}
.dt-bar :is(.search input, .dt-btn):focus-visible,
.dt-bar .search input:focus {
  outline: none;
  border-color: var(--brand-on-dark);
  box-shadow: 0 0 0 3px rgba(79,155,255,.25);
}
/* "Including archived" — a marker on the bar, in the bar's own vocabulary. */
.dt-bar .archived-marker {
  background-color: rgba(255,255,255,.07); border-color: var(--chrome-border);
  color: var(--chrome-ink-dim);
}
.dt-bar .archived-marker:hover { color: #fff; border-color: rgba(255,255,255,.4); }
.dt-bar h2 { flex: 0 0 auto; }

/* A phone gets the search on its own row and the tools under it, rather than
   five controls squeezed into one line none of them fit. */
@media (max-width: 767.98px) {
  .dt-bar .search { flex: 1 1 100%; }
  .dt-tools { margin-left: 0; width: 100%; }
  .dt-tools > * { flex: 1 1 auto; }
  .dt-btn { width: 100%; justify-content: center; }
  .dt-btn-text { display: inline; }
}
.dt-tools { display: flex; align-items: center; gap: .35rem; margin-left: auto; }

/* One button for all three: Filter, Columns, and the row-height menu. Quieter
   than `.btn-x` — these change the VIEW, and the things that change the DATA
   (Add, Import, Export) are in the page header above and have to stay louder. */
.dt-btn {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .42rem .65rem; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); background: #fff; color: var(--text);
  font: inherit; font-size: .8125rem; font-weight: 500; line-height: 1.35;
  box-shadow: var(--shadow-xs); cursor: pointer; white-space: nowrap;
  transition: border-color var(--transition), background-color var(--transition);
}
.dt-btn:hover { border-color: #94a3b8; background: #fcfdff; }
.dt-btn:focus-visible { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
.dt-btn[aria-expanded="true"] { border-color: var(--brand); }
.dt-btn:disabled { opacity: 1; cursor: default; }
.dt-btn i { font-size: .85rem; color: var(--text-muted); }
.dt-btn.is-on, .dt-btn.is-on i { color: var(--brand); }
/* How many filters are on, or how many columns are showing. The only thing on
   screen that says a column is hidden rather than absent. */
.dt-badge {
  min-width: 1.05rem; padding: 0 .28rem; border-radius: 999px;
  background: var(--brand); color: #fff;
  font-size: .68rem; font-weight: 700; line-height: 1.5rem; height: 1.05rem;
  display: inline-flex; align-items: center; justify-content: center;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 575.98px) { .dt-btn-text { display: none; } }

/* The menus behind them. A heading per group, and a tick on what is in use —
   a menu of values where nothing is marked is a menu you have to remember. */
.dt-menu { min-width: 12rem; max-width: 20rem; padding: .3rem; }
.dt-menu-head {
  padding: .35rem .65rem .2rem; font-size: .68rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted);
}
.dt-menu .dropdown-item.is-on { color: var(--brand); font-weight: 600; }
.dt-menu .dropdown-item.is-on::after {
  content: "\F26E"; font-family: bootstrap-icons; margin-left: auto; font-size: .8rem;
}
.dt-check { width: 100%; cursor: pointer; gap: .55rem; }
.dt-check input { cursor: pointer; }

/* ── A filter, as a chip ─────────────────────────────────────────────────── */
/* It exists only while the filter does, so an unfiltered table has no filter
   row at all — and a filtered one says so in a sentence nobody can miss. That
   is also the answer to the commonest question about a long list: "where did my
   rows go". */
.dt-filters {
  display: flex; align-items: center; flex-wrap: wrap; gap: .4rem;
  padding: .55rem .9rem; border-bottom: 1px solid var(--border);
  background: var(--slate-bg);
}
/* NO `overflow: hidden` here, however tempting it is for the rounded ends:
   this box CONTAINS the value menu, and clipping it to a 26px pill is a menu
   that opens and cannot be seen. The two halves round themselves instead. */
.dt-chip {
  display: inline-flex; align-items: stretch; border: 1px solid var(--border-strong);
  border-radius: 999px; background: #fff; box-shadow: var(--shadow-xs);
}
.dt-chip > .dropdown:first-child .dt-chip-main {
  border-radius: 999px 0 0 999px;
}
.dt-chip:focus-within { border-color: var(--brand); box-shadow: var(--ring); }
.dt-chip-main {
  display: inline-flex; align-items: center; gap: .32rem;
  padding: .25rem .5rem .25rem .7rem; border: 0; background: none;
  font: inherit; font-size: .78rem; color: var(--text); cursor: pointer;
}
.dt-chip-main:hover { background: var(--nav-hover-bg); }
.dt-chip-main:focus-visible { outline: none; }
/* The field's name, quiet; the value it is set to, not. "Status: Active" reads
   as the sentence rather than as a control with a label bolted to it. */
.dt-chip-main b { font-weight: 600; color: var(--text-muted); }
.dt-chip-main b::after { content: ":"; }
.dt-chip-main > span { font-weight: 600; }
.dt-chip-main i { font-size: .7rem; color: var(--text-muted); }
.dt-chip-x {
  display: inline-flex; align-items: center; padding: 0 .45rem 0 .2rem;
  border: 0; border-left: 1px solid var(--border); background: none;
  border-radius: 0 999px 999px 0;
  color: var(--text-muted); cursor: pointer; font-size: .9rem;
}
.dt-chip-x:hover { background: var(--red-bg); color: #b91c1c; }
/* The chip is a flex row, so the menu Popper puts inside it must not become a
   third column of it. */
.dt-chip > .dropdown { display: inline-flex; }
.dt-clear {
  border: 0; background: none; padding: .25rem .4rem; margin-left: .15rem;
  font: inherit; font-size: .78rem; font-weight: 600; color: var(--text-muted);
  cursor: pointer; border-radius: var(--radius-sm);
}
.dt-clear:hover { color: var(--text); background: rgba(15,23,42,.06); }

/* ── The bar a selection raises ──────────────────────────────────────────── */
/* What can be done to the rows that are ticked, and it exists only while some
   are. It REPLACES the filter head rather than stacking under it: the filters
   are how you got this selection and are not what you are about to do to it,
   and two rows of controls over a table is how a screen stops being readable.
   The count is first because it is the thing to check before pressing any of
   the rest. */
.selbar {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .6rem 1.15rem; border-bottom: 1px solid var(--border);
  background: var(--nav-hover-bg);
}
.selbar-count { font-size: .8125rem; font-weight: 700; color: var(--text); }
.selbar-acts { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-left: auto; }
@media (max-width: 640px) {
  .selbar-acts { margin-left: 0; width: 100%; }
  .selbar-acts .btn-x, .selbar-acts .select-x { flex: 1 1 auto; justify-content: center; }
}
select.field-input {
  appearance: none; -webkit-appearance: none; padding-right: 2rem;
  background-image: var(--caret);
  background-repeat: no-repeat; background-position: right .55rem center; background-size: 15px;
}
select.field-input:disabled { background-image: none; }

/* A quiet inline button that is a link, not a control. Used where a sentence
   needs one word to be clickable and a `.btn-x` would shout. */
.lnk {
  padding: 0; border: 0; background: none; color: var(--brand);
  font: inherit; font-size: inherit; font-weight: 600; cursor: pointer;
  text-underline-offset: 2px;
}
.lnk:hover { text-decoration: underline; }
.lnk:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }

/* ── The two-column workspace ────────────────────────────────────────────── */
/* Settings on the left, the list on the right. The list is the page: it is what
   an operator is actually working on, and the settings are five answers they
   give once. The rail sticks, so the button that creates the campaign is
   reachable from anywhere in a list of three hundred people. */
.work { display: grid; grid-template-columns: 296px minmax(0, 1fr); gap: 1rem; align-items: start; }
@media (max-width: 991.98px) { .work { grid-template-columns: 1fr; } }
/* `.card-x + .card-x` gives a stacked card its 1rem of air. Inside a grid the
   two are side by side, and that margin was pushing the right column 16px lower
   than the left one — the misalignment reads as a broken layout. */
.work > .card-x + .card-x { margin-top: 0; }

.rail-body { padding: 1.05rem; }

/* A label and its control on one line. The prose that used to sit under every
   field is now one `?` beside the label — the explanation still ships, it just
   stops being 60% of the screen. */
.rail-row { display: flex; align-items: center; gap: .6rem; padding: .3rem 0; }
.rail-row + .rail-row { border-top: 1px solid #f1f4f8; }
.rail-l { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: .3rem;
  font-size: .8125rem; color: #475569; }
.rail-c { flex: 0 0 auto; width: 124px; }
.rail-row .help { width: 17px; height: 17px; font-size: .62rem; }
.rail-sep { margin: .85rem 0 .7rem; border: 0; border-top: 1px solid var(--border); }
.rail-h {
  margin: 0 0 .5rem; font-size: .6875rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: #94a3b8;
}
.rail .field { margin-bottom: .75rem; }
.rail .field-label { display: flex; align-items: center; gap: .3rem; }
.rail .field-label .help { width: 17px; height: 17px; font-size: .62rem; }

.pane { display: flex; flex-direction: column; min-width: 0; }
.pane-head {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .7rem 1.05rem; border-bottom: 1px solid var(--border);
}
.pane-head h2 { margin: 0; font-size: .95rem; font-weight: 600; letter-spacing: -0.01em; }
.pane-head .page-actions { margin-left: auto; }
.pane-body { padding: 1.05rem; min-width: 0; }

/* ── The drop zone ───────────────────────────────────────────────────────── */
/* The first thing on an empty pane, and the whole of it: one target, two
   buttons, one line about what the file needs. */
.dz {
  display: grid; place-items: center; gap: .1rem; padding: 2.6rem 1rem;
  border: 1.5px dashed var(--border-strong); border-radius: var(--radius);
  background: #fbfcfe; text-align: center; cursor: pointer;
  transition: border-color var(--transition), background-color var(--transition);
}
.dz:hover { border-color: #94a3b8; background: #f8fafc; }
.dz.is-over { border-color: var(--brand); background: var(--nav-hover-bg); }
.dz:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.dz .bi { font-size: 1.6rem; color: #94a3b8; }
.dz-title { margin: .45rem 0 0; font-size: 1rem; font-weight: 600; letter-spacing: -0.01em; }
.dz-acts { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; margin: .9rem 0 0; }
.dz-note { margin: .9rem 0 0; font-size: .78rem; color: var(--text-muted); }
.dz-note code {
  padding: .03rem .3rem; border-radius: 4px; background: var(--blue-bg); color: #1e40af;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .74rem;
}

/* ── The imported file ───────────────────────────────────────────────────── */
/* What the file was, what matched, and — only when something did not — the one
   question that fixes it. The mandatory "map every column" screen is gone: with
   two fixed fields and placeholder names the customer already has in their
   template, matching by name succeeds almost every time, and a grid of selects
   that always says the right thing is a form nobody should have to submit. */
.csvbar {
  margin-bottom: .85rem; padding: .6rem .75rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fbfcfe;
}
.csvbar-file { display: flex; align-items: center; gap: .5rem; font-size: .8125rem; }
.csvbar-file .bi-filetype-csv { color: var(--green); font-size: 1rem; }
.csvbar-file b { font-weight: 600; }
.csvbar-file .lnk { margin-left: auto; font-size: .78rem; }
.csvbar-note { margin: .25rem 0 0; font-size: .75rem; color: var(--text-muted); }
.csvbar-ask {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-top: .55rem; padding-top: .55rem; border-top: 1px solid var(--border);
  font-size: .8125rem; color: #92400e;
}
.csvbar-ask + .csvbar-ask { margin-top: .4rem; padding-top: .4rem; }
.csvbar-ask .bi { color: var(--amber); }
.csvbar-ask select { width: auto; min-width: 160px; margin-left: auto; }
.csvbar .rmap { margin-top: .8rem; padding-top: .7rem; border-top: 1px solid var(--border); }

/* Which CSV column is which. Selects under the names the file actually carries,
   so the operator maps by reading across rather than by guessing. */
.rmap { display: grid; grid-template-columns: repeat(auto-fill, minmax(186px, 1fr)); gap: .55rem .8rem; }
.rmap label { display: block; min-width: 0; margin: 0; }
.rmap .field-note { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── The recipient grid ──────────────────────────────────────────────────── */
/* An editable table, not a form per row: the operator is transcribing a list,
   and what makes that bearable is that the columns line up and Tab goes right.
   A paste from a spreadsheet fills the grid, which is why there is no separate
   place to paste one. */
.coll-tools {
  display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; margin-bottom: .75rem;
}
.coll-n { font-size: .8125rem; font-weight: 600; margin-right: auto; font-variant-numeric: tabular-nums; }

.rows-wrap {
  max-height: min(52vh, 440px); overflow: auto;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface);
}
.rgrid { min-width: 480px; }
.rhead, .rrow { display: grid; grid-template-columns: var(--rcols); align-items: stretch; }
.rhead {
  position: sticky; top: 0; z-index: 1; background: var(--slate-bg);
  border-bottom: 1px solid var(--border);
}
.rhead span {
  padding: .42rem .6rem; font-size: .6875rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: #64748b; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.rhead span.req::after { content: "*"; margin-left: .15rem; color: var(--red); }
.rrow + .rrow { border-top: 1px solid var(--border); }
.rrow.is-bad { background: #fffafa; }
.rcell { position: relative; min-width: 0; border-left: 1px solid var(--border); }
.rcell:first-child { border-left: 0; }
.rcell input {
  display: block; width: 100%; padding: .42rem .6rem; border: 0; border-radius: 3px;
  background: transparent; color: var(--text); font: inherit; font-size: .8125rem;
}
.rcell input:focus { outline: none; background: #fff; box-shadow: inset 0 0 0 2px var(--brand); }
.rcell input::placeholder { color: #a8b4c4; }
/* The CELL, not the row. Colouring five values red for one missing amount tells
   an operator to check all five. */
.rcell.is-bad input { color: #991b1b; }
.rrow .icon-btn { align-self: center; justify-self: center; width: 24px; height: 24px; }

/* ── The tally ───────────────────────────────────────────────────────────── */
/* Counts first, then the rows that need attention — never more than a handful of
   those: three hundred identical complaints is not a list anybody reads. */
.tally {
  display: flex; align-items: center; gap: .35rem; flex-wrap: wrap;
  margin: .8rem 0 0; font-size: .8125rem; color: var(--text-muted);
}
.tally-n { font-weight: 700; font-variant-numeric: tabular-nums; }
.tally-ok { color: #15803d; }
.tally-bad { color: #991b1b; }
.tally-sep { color: #cbd5e1; }
.problems {
  margin: .6rem 0 0; padding: .65rem .8rem;
  border: 1px solid #fecaca; border-radius: var(--radius-sm); background: var(--red-bg);
}
.problems ul { margin: 0; padding-left: 1.2rem; font-size: .8125rem; color: #991b1b; }
.problems li { margin-top: .12rem; }
.problems code {
  padding: .03rem .28rem; border-radius: 4px; font-size: .78rem;
  background: rgba(153,27,27,.09); color: #7f1d1d;
}

/* ── The API note ────────────────────────────────────────────────────────── */
.coll-api { margin: 1rem 0 0; font-size: .8125rem; }
.coll-api.open { padding-top: 1rem; border-top: 1px solid var(--border); }
.code-x {
  margin: 0; padding: .8rem .9rem; overflow-x: auto;
  border: 1px solid #1e293b; border-radius: var(--radius-sm); background: #0f172a;
  color: #e2e8f0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .78rem; line-height: 1.6; tab-size: 2;
}
.code-x .k { color: #7dd3fc; }
.code-x .s { color: #bef264; }
.code-x ::selection { background: rgba(125,211,252,.28); color: #f8fafc; }
.code-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
.code-head .btn-x { margin-left: auto; }

/* ── Placeholder chips ───────────────────────────────────────────────────── */
.chips { display: flex; align-items: center; gap: .28rem; flex-wrap: wrap; }
.chip {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .08rem .42rem; border: 1px solid #bfdbfe; border-radius: 999px;
  background: var(--blue-bg); color: #1e40af;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .7rem; font-weight: 600;
}
.chip.optional { border-color: var(--border); background: var(--slate-bg); color: #64748b; }

/* A segment chip is the same pill, but it is a LINK: the way from a person to
   the list that will call them, and back. Bootstrap underlines every anchor,
   which on a pill this small reads as a strikethrough. */
a.chip { text-decoration: none; }
a.chip:hover, a.chip:focus-visible { border-color: #60a5fa; }
/* An archived segment still shows on a contact — a membership that exists has
   to be visible, or a campaign's audience count looks wrong for no reason —
   but it is not a list anything will be called from, so it reads as grey. */
.chip.archived { border-color: var(--border); background: var(--slate-bg); color: #64748b; }

.settings-fields { min-width: 0; margin: 0; padding: 0; border: 0; }
.form-actions-end { justify-content: flex-end; }


/* "Including archived", and it is a MARKER rather than a control: it appears
   only when the archive is folded in — which happens by following the rail's
   Archived link, not by anything on this page — and clicking it is the way
   out. A page showing archived rows has to say so; a page that is not should
   not spend a control saying it could. */
.archived-marker {
  border-color: var(--border-strong); background: var(--slate-bg); color: #64748b;
  cursor: pointer; gap: .3rem;
}
.archived-marker:hover { color: var(--ink); border-color: var(--ink); }
.archived-marker .bi-x { font-size: .95rem; opacity: .75; }

/* Progress in a table cell: a bar the width of a thumbnail and the two numbers
   beside it. The bar alone cannot say 7 of 8 apart from 700 of 800, and the
   numbers alone need reading rather than glancing. */
.list-progress { display: grid; grid-template-columns: 84px auto; align-items: center; gap: .5rem; }
.list-progress .meter { height: 6px; }
.list-progress-n {
  font-size: .78rem; color: var(--text-muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* The campaign list's own columns.

   Everything structural about this table — the sideways scroll, the sticky
   head, the frozen first column, the sticky action cell — is the shared data
   table's now (`.dt-scroll`, `assets/js/table.js`), because it was written
   here first and then wanted on every other list. What is left is what is
   true about THESE columns and nothing else, and it is written against the
   column classes rather than a table class: the chooser can fold any of them
   away, so a rule that depended on where a column sits would be a rule that
   moves.

   One fact per column, and nothing stacked inside one. Two lines in a cell
   stops the eye reading DOWN a column — which is the only thing a table is
   better at than a list of cards — so the template, the number it calls from,
   how far it got and when it went out are four columns, not two cells with a
   second line each. */
.tbl :is(th, td):is(.cmp-template, .cmp-phone, .cmp-when) { white-space: nowrap; }

/* Six campaign facts and its actions fit the card on desktop. Fixed layout
   lets the bounded columns win over a long template name instead of making
   the whole table wider than the viewport. */
.campaigns-page .tbl { table-layout: fixed; }
.campaigns-page .tbl .cmp-name-col { width: 17%; }
.campaigns-page .tbl .cmp-template { width: 15%; }
.campaigns-page .tbl .cmp-phone { width: 14%; }
.campaigns-page .tbl .cmp-progress { width: 14%; }
.campaigns-page .tbl .cmp-status { width: 15%; }
.campaigns-page .tbl .cmp-when { width: 15%; }
/* Only the unpredictable template name truncates. Phone, Status and Started
   are operational facts and stay complete. */
.campaigns-page .tbl .cmp-template {
  overflow: hidden; text-overflow: ellipsis;
}
.campaigns-page .tbl :is(th, td).row-act { width: 5rem; padding-left: .25rem; padding-right: .65rem; }
.campaign-actions { display: inline-flex; align-items: center; justify-content: flex-end; gap: .2rem; }

/* The name is the one cell that can outgrow its column, and it truncates
   rather than wrapping: a two-line name sets the height of every row beside
   it, and the full name is one hover away.

   Archived is the OTHER axis (`campaigns.status`), so its chip rides beside
   the name rather than in the Status column, where it would be claiming to be
   a seventh state. The row is a flex line so the chip keeps its width and the
   name gives way — `.row-title` is a block everywhere else in the product, and
   a block would drop the chip onto a second line, which is the thing this
   table is not allowed to do. */
.cmp-id { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.cmp-id .row-title {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cmp-id .pill { flex: none; margin: 0; }
.tbl td.cmp-phone { font-variant-numeric: tabular-nums; color: var(--text-muted); }
/* The template is a link, and it underlines on hover rather than always: ten
   underlined names down a column is the loudest thing in a table whose rows
   are ALL clickable anyway. */
.cmp-template a { text-decoration: none; }
.cmp-template a:is(:hover, :focus-visible) {
  text-decoration: underline; text-underline-offset: 2px;
}
.cmp-progress .list-progress { grid-template-columns: 56px auto; gap: .45rem; }

/* The one row that is changing while it is being looked at. The pill already
   pulses; this is what makes it findable in a list of twenty without reading
   any of them. */
.tbl .row-link.is-live td:first-child {
  box-shadow: inset 3px 0 0 var(--green);
}
/* A phone scrolls the page — that is what a phone does. Boxing the list into a
   short window inside a document that scrolls anyway gives two scrollbars and
   no more rows. */
@media (max-width: 767.98px) {
  .campaigns-page .tbl { min-width: 720px; }
  main.app-content.page-fill { height: auto; padding-bottom: 3rem; }
  .page-fill > .card-x.fill,
  .page-fill > .card-x.fill > .card-x-body { display: block; }
  main.app-content.report-page { min-height: calc(100dvh - var(--topbar-h)); overflow: visible; }
  .report-page > section:not([hidden]),
  .report-page > section:not([hidden]) > .card-x,
  .report-page > section:not([hidden]) > .card-x > .card-x-body { display: block; }
  .report-page #runsView .tbl { table-layout: auto; min-width: 760px; }
}

/* Progress without a card around it. One bar, the counts beside it, and the
   money underneath — the shortest form of the only question a running campaign
   is asked. */
.runbar {
  display: grid; grid-template-columns: minmax(260px, 1fr) minmax(320px, auto);
  align-items: center; gap: 1.5rem; min-height: 92px;
  margin: 1.15rem 0 1.25rem; padding: 1rem 0;
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.run-meter-block { align-self: center; }
.run-meter-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  margin-bottom: .55rem; font-size: .78rem; color: var(--text-muted);
}
.run-meter-head strong { font-size: .8125rem; color: var(--text); }
.runbar .meter {
  height: 14px; border: 1px solid #dbe3ec;
  box-shadow: inset 0 1px 2px rgba(15,23,42,.08);
}
.run-details { align-self: center; }
.run-details .runline:first-child { margin-top: 0; }
.run-details #ledger { color: var(--text-muted); }
.runline {
  display: flex; align-items: baseline; gap: .35rem 1.15rem; flex-wrap: wrap;
  margin: .6rem 0 0; font-size: .8125rem; color: var(--text-muted);
}
.runline b { font-size: .9375rem; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.runline .rl { display: inline-flex; align-items: baseline; gap: .3rem; }
.runline .rl.dim b { color: var(--text-muted); }
.runline .rl-right { margin-left: auto; }
@media (max-width: 767.98px) {
  .runbar { grid-template-columns: 1fr; gap: .8rem; }
}

/* Which set of calls the campaign screen is showing. A campaign used once has
   nothing to choose between, so this is absent far more often than it is
   present — it sits above the progress bar and stays out of the way. */
.sendbar {
  display: flex; align-items: center; gap: .55rem; flex-wrap: wrap;
  margin: 1.15rem 0 -.35rem; font-size: .8125rem; color: var(--text-muted);
}
.sendbar .select-x { max-width: 24rem; }
.sendbar .field-note { margin: 0; }

.response-report { margin-bottom: 1rem; }
.response-report .card-x-head { align-items: center; }
.response-report .card-x-head p { margin: .18rem 0 0; color: var(--text-muted); font-size: .78rem; }
.response-track { display: flex; width: 100%; height: 10px; overflow: hidden; border-radius: 999px; background: var(--slate-bg); }
.response-segment { min-width: 2px; height: 100%; }
.response-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 0 1.25rem; margin-top: .8rem; }
.response-item { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto auto; align-items: center; gap: .45rem; padding: .55rem 0; border: 0; border-bottom: 1px solid var(--border); background: transparent; color: var(--text); text-align: left; }
.response-item:hover .response-name { text-decoration: underline; text-underline-offset: 3px; }
.response-item:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.response-swatch { width: 8px; height: 8px; border-radius: 50%; }
.response-item strong { font-variant-numeric: tabular-nums; }
.response-item > :last-child, .response-key { color: var(--text-muted); font-size: .75rem; }
.response-key { grid-column: 2; }
.tone-0 { background: #2563eb; } .tone-1 { background: #0f766e; }
.tone-2 { background: #7c3aed; } .tone-3 { background: #c2410c; }
.tone-4 { background: #be123c; } .tone-5 { background: #64748b; }
.response-cell { display: inline-flex; flex-direction: column; line-height: 1.25; }
.response-cell small { color: var(--text-muted); font-size: .72rem; }
@media (max-width: 575.98px) {
  .response-report .card-x-head { align-items: stretch; flex-wrap: wrap; }
  .response-report .card-x-head .btn-x { justify-content: center; width: 100%; }
  .response-legend { grid-template-columns: 1fr; }
}

/* Campaign confirmations are centred decisions with one clear consequence.
   Bootstrap owns focus trapping and escape handling; the app owns the voice. */
.confirm-content { border: 0; border-radius: var(--radius); box-shadow: 0 22px 60px rgba(15,23,42,.22); }
/* Bootstrap's h5 default is 20px/500, which reads as a page heading that has
   wandered into a dialog. A question this short is a label: smaller, heavier,
   ink rather than the body colour. */
.confirm-content .modal-header { padding: 1.15rem 1.25rem .45rem; border-bottom: 0; }
.confirm-content .modal-title { color: var(--ink); font-size: 1.0625rem; font-weight: 700; letter-spacing: -.011em; }
/* The close is the least important control in the box - it is the same answer
   as Back, which is already a button under it. It stops competing with the
   title for the top line. */
.confirm-content .btn-close { width: .7em; height: .7em; opacity: .4; }
.confirm-content .btn-close:hover { opacity: .75; }
.confirm-content .modal-body { padding: 0 1.25rem 1.25rem; }
/* No `max-width` here, deliberately. A 48ch cap inside a 500px dialog left the
   sentence ending two thirds of the way across while the table under it ran
   the full width - three different right edges down one narrow column. The
   dialog IS the measure: 500px of 14px Inter is about 66 characters. */
.confirm-message { margin: 0; color: var(--text-muted); font-size: .875rem; line-height: 1.55; }
/* The before/cost/after of a button that spends credit. The last row is the
   answer, so it is the one that is allowed to be loud. */
.confirm-ledger { margin: .95rem 0 0; padding: .55rem .85rem .65rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--slate-bg); }
.confirm-ledger .ledger-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1.25rem; padding: .3rem 0; font-size: .875rem; }
.confirm-ledger dt { margin: 0; color: var(--text-muted); font-weight: 400; }
.confirm-ledger dd { margin: 0; color: var(--text); font-variant-numeric: tabular-nums; }
.ledger-hint { margin-left: .4rem; color: var(--text-muted); font-size: .75rem; opacity: .85; }
.confirm-ledger .is-total { margin-top: .3rem; padding-top: .5rem; border-top: 1px solid var(--border-strong); }
.confirm-ledger .is-total dt { color: var(--ink); font-weight: 600; }
.confirm-ledger .is-total dd { color: var(--ink); font-size: 1.0625rem; font-weight: 700; }
.confirm-ledger .err dd { color: var(--red); }
.confirm-note { margin: .7rem 0 0; color: var(--text-muted); font-size: .8125rem; line-height: 1.5; }
.confirm-content .modal-footer { padding: .35rem 1.25rem 1.15rem; border-top: 0; }
.confirm-content [data-confirm].danger { background: var(--red); border-color: var(--red); color: #fff; }
.confirm-content [data-confirm].danger:hover { background: #b91c1c; border-color: #b91c1c; color: #fff; }

/* ── Pager ───────────────────────────────────────────────────────────────── */
.pager {
  display: flex; align-items: center; gap: .5rem;
  padding: .6rem 1.15rem; border-top: 1px solid var(--border);
  font-size: .78rem; color: var(--text-muted);
}
.pager .page-actions { margin-left: auto; }
.pager strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* Numbered paging. One row of discs: jump to an end, step by one, or land on a
   page you can see. The current page is the only filled thing in the row, so it
   needs no second signal — no border, no weight change, no colour code. */
.pages { display: flex; align-items: center; gap: .15rem; margin-left: auto; }
.pg {
  display: inline-grid; place-items: center;
  min-width: 30px; height: 30px; padding: 0 .35rem;
  border: 0; border-radius: 999px; background: transparent;
  color: var(--text-muted); font: inherit; font-size: .8125rem; font-weight: 600;
  font-variant-numeric: tabular-nums; line-height: 1; cursor: pointer;
  transition: background-color var(--transition), color var(--transition);
}
.pg:hover:not(:disabled):not(.is-current) { background: var(--slate-bg); color: var(--text); }
/* `:not(.is-current)` is load-bearing. The current page is a disabled button —
   there is nowhere to go — and without this it faded the one filled disc in the
   row to 30% grey, which read as "this page is unavailable". */
.pg:disabled:not(.is-current) { opacity: .3; cursor: default; }
.pg:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.pg.is-current { background: var(--ink); color: #fff; cursor: default; }
.pg.gap { min-width: 18px; color: var(--border-strong); cursor: default; }
/* Under 480px the numbers go and the four arrows stay: on a phone the useful
   move is one step or an end, not landing on page six of nine. */
@media (max-width: 479.98px) {
  .pager { flex-wrap: wrap; }
  .pg.num, .pg.gap { display: none; }
  .pg.is-current { display: inline-grid; }
}

/* ── Search ──────────────────────────────────────────────────────────────── */
.search { position: relative; display: inline-flex; }
.search .bi {
  position: absolute; left: .55rem; top: 50%; transform: translateY(-50%);
  font-size: .8rem; color: #94a3b8; pointer-events: none;
}
.search input {
  width: 190px; padding: .42rem .6rem .42rem 1.8rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: #fff; color: var(--text); font: inherit; font-size: .8125rem;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.search input:focus { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
@media (max-width: 575.98px) { .search, .search input { width: 100%; } }
/* The full-width form, for a search that is the only control on its row. */
.search.block { display: flex; width: 100%; }
.search.block input { width: 100%; }

/* ── Reveal ──────────────────────────────────────────────────────────────── */
/* The one authored moment on these screens: a panel that was not there arrives
   from where it will live rather than fading in from nowhere. */
.reveal { animation: reveal .22s cubic-bezier(.16,1,.3,1); }
@keyframes reveal { from { opacity: 0; transform: translateY(-5px); } }
@media (prefers-reduced-motion: reduce) { .reveal { animation: none; } }

/* ── The developer screen ─────────────────────────────────────────────────── */
/* A permission list is read top to bottom, not scanned across: each line is a
   thing a key would be allowed to do, and the machine name beside it is what
   the customer's developer will grep the docs for. `.check` alone is inline,
   which is right beside a field and wrong for a list of eight. */
.check-list { display: grid; gap: .4rem; }
.check-list .check { align-items: flex-start; }
.check-list .check input { margin-top: .2rem; }
.check-list code { font-size: .72rem; color: var(--text-muted); }

/* ── Custom fields ───────────────────────────────────────────────────────── */
/* A contact's custom fields are a set of named values, and the plan is explicit
   that ordinary users must never be shown raw JSON for them. So they are rows:
   a name, a value, and one button to take the row away. Two columns rather than
   a table because the header would say "Name" and "Value" and the placeholders
   already do. */
.cf-list { display: grid; gap: .4rem; }
.cf-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) auto; gap: .4rem; }
.cf-row .field-input { font-size: .8125rem; }
@media (max-width: 575.98px) {
  .cf-row { grid-template-columns: minmax(0, 1fr) auto; }
  .cf-row .field-input:nth-child(2) { grid-column: 1 / -1; }
}

/* ── Import counts ───────────────────────────────────────────────────────── */
/* What an import did, as five numbers in a row. It is the whole answer to
   "what just happened to my four thousand rows", so it is the first thing on
   the screen after the upload and it does not go away until the panel is
   closed — a toast that vanished would take the only report with it. */
.tallies { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 .9rem; }
.tally {
  flex: 1 1 5.5rem; padding: .5rem .65rem; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--slate-bg);
}
.tally b { display: block; font-size: 1.05rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.tally span { font-size: .72rem; color: var(--text-muted); }
.tally.is-warn { border-color: #fcd34d; background: #fffbeb; }
.tally.is-warn b { color: #92400e; }

/* A file that is on its way up. The drop zone stays visible — it is where the
   result appears — but stops inviting a second file while the first is being
   read. */
.dz.is-busy { pointer-events: none; opacity: .55; }

/* ── The review workspace ────────────────────────────────────────────────── */
/* The mirror of `.work`: the thing being built on the left, the answer about it
   on the right. Campaign settings are five questions with a list beside them,
   so the questions are the rail; a run is a SELECTION with a verdict beside it,
   so the verdict is the rail — and a verdict belongs after the thing it is
   about, which on a narrow screen is the only reading order there is. */
.work-rev { grid-template-columns: minmax(0, 1fr) 320px; }
@media (max-width: 991.98px) { .work-rev { grid-template-columns: 1fr; } }
/* One grid cell holding two stacked cards. The Run screen needs it because the
   rail beside them is sticky, and `position: sticky` only works against the
   grid itself — so the left column's cards cannot be direct children too. */
.work-col { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
/* Sticks, so the count and the button that acts on it stay on screen while an
   operator scrolls a list of two hundred segments.

   `top` clears the TOP BAR, which is itself sticky at `top: 0` and 56px tall:
   at `top: 1rem` the rail pinned underneath it and the bar covered the first
   40px of the card — which is the verdict number, the one thing on this screen
   that must never be half visible.

   And it is capped to what is left of the viewport, with the body scrolling
   inside it. A rail is only as tall as its exclusions, so a selection with six
   reasons for leaving people out grew past the fold and took the Start button
   with it — a sticky panel whose action cannot be reached is not sticky. The
   verdict stays put at the top of the card and the detail under it scrolls. */
@media (min-width: 992px) {
  .work-rev > .rail {
    position: sticky; top: calc(var(--topbar-h) + 1rem);
    max-height: calc(100vh - var(--topbar-h) - 2rem);
    display: flex; flex-direction: column;
  }
  /* `min-height: 0` or the flex item refuses to shrink and overflows anyway. */
  .work-rev > .rail > .rail-body { overflow-y: auto; min-height: 0; }
}

/* The verdict itself: one number the size of a decision, then its unit.
   `flex: 0 0 auto` because the rail is a flex column that may be capped to the
   viewport: without it the number is what gets squeezed. */
.verdict { flex: 0 0 auto; padding: 1.05rem; border-bottom: 1px solid var(--border); }
.verdict-n {
  display: block; font-size: 2.1rem; font-weight: 800; line-height: 1.05;
  letter-spacing: -0.03em; color: var(--ink); font-variant-numeric: tabular-nums;
}
.verdict-u { font-size: .8125rem; color: var(--text-muted); }
.verdict.is-empty .verdict-n { color: var(--text-muted); }

/* ── A choice of two or three, as one control ────────────────────────────── */
/* Deliberately NOT called `.seg`: in this product a segment is already a
   message part AND a saved audience rule, and a third meaning on the same word
   is exactly how those two got confused (`docs/LISTS_AND_SEGMENTS.md` §2).

   It replaces a `<select>` where the options are few, exclusive and worth
   reading before choosing. "Start: Now ▾" hid the fact that a run can be
   scheduled at all behind a click — the operator had to open the menu to learn
   the choice existed, and a run they meant to send at 9am tomorrow is not a
   discovery to leave to curiosity. Two labels side by side answer it without
   being opened, and the one that is on is a decision the screen keeps showing.

   A radiogroup rather than a tablist: nothing is revealed or hidden by the
   choice itself, it IS the value. So it is one tab stop and the arrows move
   inside it, which is what a keyboard expects of a radio group. */
.choice {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: .15rem;
  padding: .2rem; border: 1px solid var(--border);
  border-radius: 999px; background: var(--slate-bg);
}
.choice-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .35rem;
  min-width: 0; padding: .4rem .5rem; border: 0; border-radius: 999px;
  background: transparent; color: var(--text-muted);
  font-size: .8125rem; font-weight: 600; line-height: 1.2; cursor: pointer;
  transition: background-color var(--transition), color var(--transition),
    box-shadow var(--transition);
}
.choice-btn:hover { color: var(--text); }
/* Filled with the ink, not lifted in white. A white pill on a near-white
   trough is a difference an operator has to look for, and this control answers
   "when do the phones ring" — it has to be readable at a glance from across
   the rail. It stays a PILL against the primary button's rectangle, so the two
   black shapes never read as the same kind of thing. */
.choice-btn.is-on {
  background: var(--ink); color: #fff; box-shadow: 0 1px 2px rgba(15, 23, 42, .25);
}
.choice-btn.is-on:hover { color: #fff; }
.choice-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.choice-btn .bi { font-size: .875rem; flex-shrink: 0; }

/* ── Exclusions ──────────────────────────────────────────────────────────── */
/* Who is in the selection and will not be called, and why. Never a toast: it
   is the reason the two numbers on the screen differ, and an operator comes
   back to it while they fix the data. */
.excl { list-style: none; margin: 0; padding: 0; }
.excl li {
  display: grid; grid-template-columns: 1rem auto minmax(0, 1fr); gap: .45rem;
  align-items: baseline; padding: .32rem 0; font-size: .78rem; color: var(--text-muted);
}
.excl li + li { border-top: 1px solid #f1f4f8; }
.excl li b { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.excl li i { color: #94a3b8; font-size: .8rem; }

/* ── Picked sources ──────────────────────────────────────────────────────── */
/* What is currently selected, above the picker rather than inside it: a
   selection that survives a new search has to stay visible when none of its
   members are on screen any more. */
.picked {
  display: flex; flex-wrap: wrap; gap: .35rem; align-items: center;
  padding: .6rem 1.05rem; border-bottom: 1px solid var(--border); background: var(--slate-bg);
}
.picked-tag {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .12rem .3rem .12rem .5rem; border: 1px solid var(--border);
  border-radius: 999px; background: #fff; font-size: .75rem; font-weight: 600;
}
.picked-tag .icon-btn { width: 18px; height: 18px; font-size: .62rem; }
.picked-tag em { font-style: normal; color: var(--text-muted); font-weight: 500; }

/* ── Run history ─────────────────────────────────────────────────────────── */
/* A campaign's rows are its executions. `run_no` leads, because that is the
   only name a customer has for one, and the campaign's own name is in the
   heading above. */
.run-no { font-variant-numeric: tabular-nums; font-weight: 700; }

/* ── The configuration summary ───────────────────────────────────────────── */
/* What this campaign would say, from which number, how carefully — read-only,
   because the gear opens the form. Definition list rather than a table: there
   is one campaign, and a table of one row with seven columns scrolls. */
.kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: .9rem 1.4rem; margin: 0; }
.kv > div { min-width: 0; }
.kv dt { font-size: .6875rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #94a3b8; }
.kv dd { margin: .18rem 0 0; font-size: .8125rem; color: var(--text); word-break: break-word; }

/* An archived row stays in the list when the filter shows both — a campaign
   that ran last month is how somebody finds the run they are looking for — but
   it is not a thing to work on, so it reads back a step. */
.tbl .row-link.is-muted td { color: var(--text-muted); }
.tbl .row-link.is-muted .row-title { font-weight: 500; }

/* ── A form that is the whole page ───────────────────────────────────────── */
/* No list beside it, so no two-column workspace: a measure wide enough for two
   fields and narrow enough to read is the whole layout. */
.form-page { max-width: 760px; }
.form-page .field-note { margin-top: .3rem; }

/* ── The three ways in ───────────────────────────────────────────────────── */
/* Named on the screen that no longer takes a list, because an operator who
   used to upload a CSV here needs to be told where the CSV went. */
.paths { list-style: none; margin: 0; padding: 0; }
.paths li {
  display: grid; grid-template-columns: 1rem minmax(0, 1fr); gap: .5rem;
  align-items: baseline; padding: .34rem 0; font-size: .8125rem; color: var(--text-muted);
}
.paths li + li { border-top: 1px solid #f1f4f8; }
.paths li i { color: #94a3b8; font-size: .8rem; }
.paths a { font-weight: 600; }

/* ── The report's two views ──────────────────────────────────────────────── */
/* Runs first, calls second, because that is the size of the question people
   actually ask: "how did the September batch go" comes up daily, "what happened
   on this one number" comes up when somebody complains. Links rather than
   buttons — the view is in the URL, so Back works and a ticket can link to it. */
.tabs {
  display: flex; gap: 1.4rem; margin: 0 0 1.2rem;
  border-bottom: 1px solid var(--border);
}
.tab {
  padding: .55rem 0 .6rem; border-bottom: 2px solid transparent;
  color: var(--text-muted); font-size: .875rem; font-weight: 600; text-decoration: none;
}
.tab:hover { color: var(--text); }
.tab.is-on { color: var(--ink); border-bottom-color: var(--ink); }

/* The run picker's tabs are a FILTER over one pane, not a destination: three
   lists of different lengths stacked on one screen made picking a contact a
   scroll past every list and every segment. A button rather than a link
   because there is no URL to point at — this page's URL names the campaign,
   and which of its three lists you were looking at is not worth a page state.
   The count beside each name is what a tab normally costs you: with the other
   two lists off screen, the operator has to be told a search matched there. */
button.tab {
  background: none; border: 0; border-bottom: 2px solid transparent;
  font: inherit; font-size: .875rem; font-weight: 600; cursor: pointer;
}
/* A band across the pane, like the header above it and the selection under it —
   the tab bar is part of the card's furniture, not something floating in the
   body's padding. */
.pane-tabs { margin: 0; padding: 0 1.05rem; }
.tab-n {
  margin-left: .3rem; font-size: .75rem; font-weight: 700;
  color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.tab.is-on .tab-n { color: var(--ink); }

/* ── The run table ───────────────────────────────────────────────────────── */
/* Same table as every other list on this product now (`assets/js/table.js`),
   one row per campaign sent. Two drafts were worse: a card per run (a card is a
   thing to study, a report is a thing to scan down) and a heading band per
   campaign — which, with most campaigns holding exactly one run, was a grey
   stripe between every pair of rows.

   The name in a frozen first column is the one thing in these tables that can
   be long. It gets an ellipsis rather than setting the height of the row. */
.tbl .sticky-col :is(.row-title, .row-note) {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 34ch;
}

/* ── The Segment rule builder ────────────────────────────────────────────────
   One row per condition, four controls across: field, then an optional custom
   field name, then the comparison, then the value. Grid rather than flex so
   the comparison column lines up down the rule — "is more than" under "is any
   of" is what makes a five-condition rule readable at all. The value column
   takes the slack, because it is the only one whose content is unbounded.

   Below 720px it stacks: the alternative was a horizontal scrollbar inside a
   modal, which hides the × that removes the row. */
.rule-builder { display: grid; gap: .6rem; }
.rule-match { display: flex; align-items: center; gap: .4rem; font-size: .82rem; color: #475569; }
.rule-match .select-x { width: auto; }
.rule-rows { display: grid; gap: .4rem; }
.rule-row {
  display: grid; gap: .35rem; align-items: center;
  grid-template-columns: minmax(0, 9rem) minmax(0, 8rem) minmax(0, 1fr) 2rem;
}
/* A row whose field is not a custom field has no key input, so the comparison
   takes that column too rather than leaving a hole in the grid. */
.rule-row:not(:has(.rule-key)) { grid-template-columns: minmax(0, 9rem) minmax(0, 9rem) minmax(0, 1fr) 2rem; }
.rule-row:not(:has(.rule-key)) .rule-op { grid-column: 2; }
.rule-row .field-input, .rule-row .select-x { min-width: 0; width: 100%; }
.rule-value { min-width: 0; }
.rule-novalue { font-size: .82rem; }
.rule-lists { height: auto; }

@media (max-width: 720px) {
  .rule-row { grid-template-columns: minmax(0, 1fr) 2rem; }
  .rule-row .rule-op, .rule-row .rule-value, .rule-row .rule-key { grid-column: 1; }
}

/* The rule as a sentence. Monospace because it is a machine-readable thing
   being read back, and clamped on an index row because a ten-condition rule
   must not set the height of the table. */
.rule-summary {
  font-size: .78rem; color: #475569; background: var(--slate-bg);
  border: 1px solid var(--border); border-radius: .4rem; padding: .16rem .4rem;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; word-break: break-word;
}
.rule-summary.big { font-size: .86rem; -webkit-line-clamp: 6; padding: .5rem .6rem; }
