/* ============================================
   COMPONENT STYLES. The Scaling Team Dashboard
   All components support light/dark via CSS vars
   Accent: Indigo (navigation/brand)
   Success: Emerald green (wins/bookings)
   ============================================ */

/* ── Cards ─────────────────────────────────
   Phase 1.4 migration (2026-05-24):
   Border replaces ring-shadow as the structural marker. Radius drops
   16 → 8 so data containers stop reading as marketing surfaces. Hover
   deepens the border instead of stacking another shadow layer. Shadows
   reserved for floating things (popovers, modals, slide-in panel). */
.card {
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);   /* Premium pass: 8→12px — softer, more premium surface */
  padding: var(--space-6);           /* Premium pass: 20→24px — more breathing room in every card */
  /* Premium pass: subtle material depth (inset top-highlight + faint ambient)
     so cards read as real surfaces, not flat cutouts. */
  box-shadow: var(--shadow-card);
  transition: border-color var(--duration-normal) ease,
              background-color var(--duration-normal) ease,
              box-shadow var(--duration-normal) var(--ease-out),
              transform var(--duration-normal) var(--ease-out);
}

.card-hover:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-2px);
}

.card-clickable {
  cursor: pointer;
}

.card-clickable:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-2px);
}

.card-clickable:active {
  transform: scale(0.97);   /* Phase 1.4: was 0.96 — less squish on small cards */
  transition: transform var(--duration-fast) ease;
}

@media (max-width: 767px) {
  .card { padding: var(--space-4); }
}

/* Premium pass (2026-07-11): extend the card material (lit top-edge +
   separation) to the bespoke card family so overview/admin surfaces stop
   reading as flat cutouts. .card--lit / .kpi-card--lit keep their stronger
   flagship treatment via later, more-specific rules. */
.chart-card,
.kpi-card,
.today-stathero,
.today-stream,
.admin-detail-section-card {
  box-shadow: var(--shadow-card);
}

/* ── Liquid Glass. light mode translucency ─ */
[data-theme="light"] .card,
[data-theme="light"] .kpi-card,
[data-theme="light"] .chart-card {
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(20px) saturate(1.2);
  -webkit-backdrop-filter: blur(20px) saturate(1.2);
}

/* ── KPI Cards ───────────────────────────── */
.kpi-grid {
  display: grid;
  gap: var(--space-4);
}
.kpi-grid-3 { grid-template-columns: repeat(3, 1fr); }
.kpi-grid-4 { grid-template-columns: repeat(4, 1fr); }
.kpi-grid-admin {
  grid-template-columns: repeat(2, 1fr);
}
.kpi-grid-admin > .kpi-card.kpi-hero:not(.analytics-kpi-hero) {
  grid-column: 1 / -1;
}
/* admin-analytics KPI strip — 5 dense tiles, composes .kpi-grid base
   (S7 de-island). Tighter gap than the 3/4-up overview grids is the
   page's legitimate density preference; pairs with .kpi-card--sm. */
.kpi-grid-analytics {
  grid-template-columns: repeat(5, 1fr);
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}
@media (max-width: 1100px) { .kpi-grid-analytics { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 700px)  {
  .kpi-grid-analytics { grid-template-columns: repeat(2, 1fr); }
  /* Hide ACTIVE AGENTS so the remaining 4 tiles fit 2×2 (no lonely 5th). */
  .kpi-grid-analytics [data-kpi="active-agents"] { display: none; }
}

/* KPI card — Phase 1.4 extension (admin-analytics worked example,
   2026-05-24). Bringing .kpi-card in line with .card's new treatment:
   border replaces ring-shadow, radius 16 → 8, hover deepens border
   instead of stacking shadow. Hero variant keeps its accent radial
   tint but drops the halo blur + accent rim border + top highlight
   per COMPONENT-SPECS §1. */
.kpi-card {
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  position: relative;
  transition: border-color var(--duration-normal) ease,
              background-color var(--duration-normal) ease,
              transform var(--duration-normal) ease;
  display: flex;
  flex-direction: column;
  min-height: 148px;
  overflow: hidden;
}

.kpi-card:hover {
  border-color: var(--border-strong);
  transform: translateY(-1px);
}

/* Hero KPI — soft radial corner tint, neutral border, no halo, no top
   highlight (COMPONENT-SPECS §1). The radial gradient alone marks
   the hero as "anchor of the page"; the old halo blur + accent rim
   were the chrome competing with content. */
.kpi-card.kpi-hero {
  background:
    radial-gradient(120% 80% at 100% 0%, rgba(var(--accent-rgb), 0.10) 0%, transparent 55%),
    var(--bg-card);
  border: 1px solid var(--border-default);
  padding: var(--space-7) var(--space-6);
}

/* Halo blur + top highlight removed in the migration. Keeping the
   selectors as no-ops in case other rules reference them. */
.kpi-card.kpi-hero::before {
  content: none;
}

.kpi-card.kpi-hero::after {
  content: none;
}

.kpi-card.kpi-hero:hover {
  border-color: var(--border-strong);
}

.kpi-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  font-size: var(--text-overline);
  color: var(--text-muted);
  margin-bottom: var(--space-2);
  font-weight: 500;
  letter-spacing: 0.04em;
  white-space: nowrap;
  position: relative;
  z-index: 1;
}

.kpi-label-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.kpi-label-line {
  display: inline;
}

.kpi-label-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 16px;
  width: 16px;
  height: 16px;
  color: var(--text-secondary);
}

.kpi-label-icon svg {
  width: 15px;
  height: 15px;
  stroke-width: 2;
}

.kpi-label-icon--calls svg {
  transform: scale(0.98);
}

.kpi-label-icon--rate svg,
.kpi-label-icon--satisfaction svg,
.kpi-label-icon--clients svg,
.kpi-label-icon--duration svg {
  transform: scale(0.95);
}

.kpi-label-icon--booked > svg:first-child {
  width: 16px;
  height: 16px;
  transform: translateX(-1px);
}

.kpi-label-icon--booked > svg:last-child {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 9px;
  height: 9px;
  padding: 1px;
  color: var(--success);
  background: var(--bg-card-solid);
  border-radius: var(--radius-full);
  stroke-width: 2.4;
}

.kpi-card.kpi-hero .kpi-label {
  color: var(--text-secondary);
  opacity: 0.96;
}

.kpi-card.kpi-hero .kpi-label-icon {
  color: var(--accent-strong);
}

.kpi-value {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 2.35rem;
  line-height: 0.98;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  position: relative;
  z-index: 1;
  margin-top: auto;
}

/* Empty-state glyph (e.g., "–" when a KPI has no data available) */
.kpi-value-empty {
  color: var(--text-muted);
  font-weight: 600;
  letter-spacing: 0;
}

/* ─── Today Board (Tier 1 v2) ───────────────────────────────────────────
   Illuminated overview: stat hero + optional next-up card + narrative
   activity stream. Shares the calendar's card-lit vocabulary so the
   strip-logo test passes. All counts in tabular Inter. Accent + warning
   are the only semantic tints; calls numbers stay primary white.
   --------------------------------------------------------------------- */
/* ─── Today Board polish pass (v69+) ──────────────────────────────────
   Unified overline treatment for all metadata (eyebrow, scope, stat
   labels): 11px / 600 / upper / 0.1em tracking / text-muted.
   Hero anchor: 4.5rem display numbers, -0.04em tracking.
   Transitions: --ease-out across the board, respects reduced-motion.
   Hover: 1px translateY + border-color shift. Focus: accent outline-offset.
   Spacing: 8/16/24 rhythm. Mobile: 11px min text, 44px min tap targets. */
/* Board uses the full content-area width. Max-width removed. content
   fills the viewport on desktop per Linear/Vercel/Cal.com patterns.
   On mobile the parent's padding naturally constrains width. */
.today-board {
  margin: 0 0 var(--space-6);
}
/* Radius ladder (2026-08-03). Was a flat 8px to match the calendar's
   month wrap — but 8px on a ~400px-tall hero reads tight, and the call
   items nested inside it carried a LARGER radius (12), which is the
   inverted-concentric tell. Now: surface 16, controls/rows inside it 10.
   Outer = inner + the 6px the row is inset by, so the curves are
   concentric rather than fighting. */
.today-board .card { border-radius: var(--radius-xl); }
/* [hidden] override. author CSS (display:grid/flex) otherwise ties with
   browser default; (0,2,0) specificity resolves the tie. */
.today-board [hidden], .today-board[hidden] { display: none !important; }
/* Class-based visibility replacement for the `hidden` attribute. Simpler
   to debug in DevTools; harder for other rules to override; works
   uniformly across browsers. Used on .today-lower, .today-nextup,
   .today-stream. */
.today-board .is-collapsed { display: none !important; }

/* No entrance of its own (todayBoardIn, a 4px rise, went 2026-09-04): the
   board rides #content's contentEnter, which is the page's one entrance. */

/* Keyboard focus. matches calendar (.calendar-popup-close:focus-visible)
   so both surfaces share one focus vocabulary. Outline + offset is the
   app-wide pattern; pills handle the radius via box-shadow fallback. */
.today-board :is(button, a, [role="button"]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Pill-shape elements need box-shadow instead of outline for rounded
   focus rings (outline ignores border-radius). */
.today-board .today-pill:focus-visible,
.today-board .notif-bell:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.38);
}

/* ── Stat hero ─────────────────────────────────────────────────────── */
/* Desktop layout: grid 1fr 1fr at ≥1280px. hero splits into main
   (stats) and aside (action panel). Below 1280px (including laptop-
   small) the hero stacks single-column. the 3-stat cluster + pills
   need ≥~520px to not cramp. Mobile behavior included via the same
   single-column fallback. */
.today-stathero {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  padding: 0;
  /* Gap to recent calls. 24px (space-6) per Roman's refinement #5.
     Previously space-5 (20px); 24 is the sweet spot: separates without
     disconnecting. */
  margin-bottom: var(--space-6);
  overflow: hidden;
}
/* Padding 16/24/16 main, 16/24/16 aside. Main content is flex-column
   so `justify-content: center` on the desktop grid cell (below) can
   vertically center the stats + pills when the aside's stacked
   content extends further. balances the hero visually instead of
   leaving the left column floating at the top. */
.today-stathero-main {
  padding: var(--space-4) var(--space-6) var(--space-4);
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.today-stathero-aside {
  padding: var(--space-4) var(--space-6) var(--space-4);
  min-width: 0;
  /* Top-border divider on mobile/laptop. separates aside from main.
     --border-subtle, not --border: an INTERIOR division inside one card
     cannot be drawn at the same weight as the card's own outer edge, or
     the card reads as two cards touching. */
  border-top: 1px solid var(--border-subtle);
  display: flex;
  flex-direction: column;
  /* No flex gap; sections below the first get a top border + padding
     which both separates them visually and provides breathing room. */
  gap: 0;
}
/* Hairline dividers between aside sub-sections. makes UP NEXT,
   FOLLOW-UPS, THIS WEEK feel like three distinct sub-sections within
   one card rather than a continuous list of two similar indigo links. */
/* 12 + 12 = the card's block→block step (24), the same gap the left
   column puts above .today-spark and .today-stathero-month, with the
   hairline sitting on its midpoint. It was 16+16 (32) — the aside's
   blocks were separated by a third more air than the main column's, which
   is most of why the two halves read as set by two different hands.
   Mobile already used 12+12; desktop now agrees. */
.today-stathero-aside > .today-ap-section + .today-ap-section {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-subtle);
}
/* Accent gradient corner. only on the aside on mobile so it visually
   distinguishes from the main content below it. */
.today-stathero::before {
  content: '';
  position: absolute;
  inset: 0;
  /* Halved (was .14/.035). At full strength it read as a coloured corner
     rather than light falling on a material. */
  background: radial-gradient(circle at 94% 4%, rgba(var(--accent-rgb), 0.09) 0%, rgba(var(--accent-rgb), 0.02) 30%, transparent 55%);
  pointer-events: none;
  border-radius: inherit;
  z-index: 0;
}
.today-stathero-main,
.today-stathero-aside { position: relative; z-index: 1; }

/* Base styles for the desktop-only THIS MONTH section. Hidden by default
   at all viewports; the ≥1280 media query below flips it to display:block.
   Source order matters: this rule must precede the @media override so the
   later, more specific (viewport-matching) rule wins. */
.today-stathero-month {
  display: none;
}

@media (min-width: 1280px) {
  .today-stathero {
    /* 1.1:1 (2026-08-08). 5:7 gave the stat column 533px and the action
       column 746px — and the measured reality was the opposite of the
       assumption behind that split: the aside carried ~490×110px of blank
       to the right of a caller's name while the four key numbers, the
       14-day strip and THIS MONTH were crammed into the narrow half. The
       reclaimed width goes to the follow-up rows, which now carry a name
       and a reason instead of a bare number. */
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    /* Let both grid cells stretch to the tallest row; main's internal
       flex `justify-content: center` centers its content vertically. */
    align-items: stretch;
  }
  .today-stathero-main {
    /* Vertically center stats + pills within the stretched cell. the
       aside (with stacked UP NEXT + FOLLOW-UPS + THIS MONTH) is taller,
       so centering prevents the left side from floating at the top. */
    justify-content: center;
  }
  .today-stathero-aside {
    border-top: none;
    border-left: 1px solid var(--border-subtle);
    padding: var(--space-4) var(--space-6) var(--space-4);
    height: 100%;
  }
  /* Aside holding only the desktop-hidden week fallback: collapse the hero
     to a single calm column — mobile already behaves this way (audit C1). */
  .today-stathero:has(> .today-stathero-aside.aside-week-only) {
    grid-template-columns: 1fr;
  }
  .today-stathero-aside.aside-week-only { display: none; }
  /* Desktop ≥1280: THIS MONTH moves from the aside to the main column
     (see .today-stathero-month below). Hide the aside copy to avoid
     rendering it twice. Mobile/tablet keeps the aside version intact. */
  .today-stathero-aside > .today-ap-section--week {
    display: none;
  }
  /* The desktop-only THIS MONTH section in the main column. Overrides
     the display:none base rule at this breakpoint. */
  .today-stathero-month {
    display: block;
    /* Single owner of the gap above this section. It used to be 32px, set
       back when THIS MONTH sat directly under the stats — then the 14-day
       strip was inserted between them and its own 24px bottom margin
       stacked on top, giving a 56px dead band against 16px everywhere
       else. The strip's margin is now 0 and this is the only separator, so
       they cannot sum again; it also still reads correctly on the rare day
       the strip is hidden (no calls in the window). */
    margin-top: var(--space-6);
  }
  /* The twin only shows while loading; the real one is [hidden] until data. */
  .today-stathero-month.today-sk { display: none; }
  #dashboard-content[data-loading="true"] .today-stathero-month.today-sk { display: block; }

  /* Quiet-day used to need its own tighter margin here; the base is now
     --space-6 too, so the override would just be a second place for the
     value to drift. Removed. */
}

/* Quiet-day: hide the 0/0 stats block across ALL viewports. The quiet-day
   message carries the "nothing today" signal; showing zero counters
   alongside it is redundant. Mobile's aside THIS MONTH section still
   renders at the bottom of the card so monthly context isn't lost. */
.today-stathero-main.is-quiet .today-stats {
  display: none;
}

.today-stathero-month-head {
  margin-bottom: var(--space-3);   /* eyebrow→content step */
}
.today-stathero-month-line {
  font-size: var(--text-body);
  color: var(--text-secondary);
  line-height: 1.5;
  margin-bottom: var(--space-3);
  font-variant-numeric: tabular-nums;
}
.today-stathero-month-line strong {
  font-weight: 600;
  color: var(--text-primary);
}
.today-stathero-month-line .today-ap-dot {
  margin: 0 6px;
  opacity: 0.45;
  color: var(--text-muted);
}
/* ── ONE card link (2026-08-08) ────────────────────────────────────────
   "Open calendar", "See analytics" and "See all calls" are the same
   affordance — leave this card, go to that page — and they shipped as
   three: accent-strong at 110x20, tertiary at 102x32, tertiary at 104x40.
   Three colours, three heights, two of them under the 40px floor. One
   recipe: accent-strong, inline-flex, 40px minimum, one hover. Hierarchy
   between them comes from where they sit, not from dimming two of the
   three below the touch minimum. */
.today-stathero-month-link,
.today-ap-action,
.today-stream-more {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 40px;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--accent-strong);
  text-decoration: none;
  transition: color var(--motion-hover) var(--ease-out),
              transform var(--motion-hover) var(--ease-out);
}
.today-stathero-month-link:hover,
.today-ap-action:hover,
.today-stream-more:hover {
  color: var(--accent);
  transform: translateX(1px);
}
@media (prefers-reduced-motion: reduce) {
  .today-stathero-month-link,
  .today-ap-action,
  .today-stream-more { transition: color var(--motion-hover) linear; }
  .today-stathero-month-link:hover,
  .today-ap-action:hover,
  .today-stream-more:hover { transform: none; }
}
.today-stathero-month-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
/* margin-bottom is the card's EYEBROW→CONTENT step (12). It was 16 here,
   8 under THIS MONTH and 12 in the aside — three values for one
   relationship. Mobile already used 12; desktop now agrees. */
.today-stathero-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap; /* CL-6: heartbeat drops to its own right-aligned line on narrow viewports instead of overflowing the card */
  gap: var(--space-1) var(--space-2);
  margin-bottom: var(--space-3);
}
/* Shared overline treatment. eyebrow + scope use this exact vocabulary so
   the page has a single "metadata voice."
   Tracking 0.06em matches .calendar-dow; 0.1em was tighter than the
   calendar's DOW and read as a slightly different voice.
   .today-stat-label LEFT this group on 2026-08-08 — see its own rule. */
.today-stathero-eyebrow,
.today-scope {
  font-family: var(--font-display);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
.today-stathero-dot { margin: 0 6px; opacity: 0.45; }

/* Receptionist heartbeat moved to the sidebar (2026-08-03, css/sidebar.css
   → .sidebar-org-status). It was desktop-only here and scoped to a single
   page; "is my receptionist working?" is account-level truth, so it now
   sits under the org name and holds everywhere. The "· last call Xh ago"
   half was dropped entirely — Roman's call. */
.today-stathero-live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-display);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--accent-strong);
  text-transform: uppercase;
}
.today-live-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16);
  /* ease-in-out for smooth loop (calendar uses similar curves for
     infinite pulses. avoids the loop-point snap that ease-out has). */
  animation: todayLiveBreath 1.8s ease-in-out infinite;
}
@keyframes todayLiveBreath {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.75; transform: scale(0.82); }
}
@media (prefers-reduced-motion: reduce) {
  .today-live-dot { animation: none; }
}

/* Stats grid. tighter cluster. Was 640px max with space-7 (32) gap;
   felt like three islands. Now 460px max with space-5 (20) gap so the
   three numbers read as one visual group. Left-aligned so the eyebrow +
   stats + pills share the same optical column. */
/* TWO NUMBERS, AND THEY MUST NEVER READ AS ONE (owner ruling 2026-08-07).
   The column-gap is a FLOOR, not a preference: at 1fr/1fr inside a 420px
   measure a 64px gap puts the second stat's origin 242px from the first's,
   so with 2-digit tabular figures (~72px) about 170px of air separates
   "24" from "9". A 2+1 or 2+2 pair cannot close that. Anything that
   narrows this — a smaller gap, a wider measure, max-content columns —
   walks back the ruling; the count of visible pixels between the two
   figures is the acceptance test, not the token name. */
.today-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));   /* was repeat(3). Follow-ups stat removed */
  column-gap: var(--space-10);                         /* 64px floor */
  row-gap: var(--space-6);
  align-items: end;
  /* Block→block step (24) — the same gap .today-context leaves, so the
     strip below sits at one height whichever of the two precedes it.
     Mobile keeps its approved 16. */
  margin-bottom: var(--space-6);
  max-width: 420px;
}
.today-stat {
  /* Left rag. The label used to be CENTRED under a left-aligned number,
     which broke the one straight edge the hero column has — eyebrow,
     figure, label and strip now all start on the same optical line. */
  display: grid;
  grid-template-columns: max-content;
  justify-items: start;
  justify-self: start;
  width: max-content;
  max-width: 100%;
  min-width: 0;
}
.today-stat-num {
  --today-stat-lh: 0.92;   /* read by .today-sk-num so both states share it */
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 4.25rem;
  line-height: var(--today-stat-lh);
  letter-spacing: -0.04em;
  color: var(--text-primary);
  /* TABULAR (2026-08-07). This was the one proportional figure in an app
     where every other number is tabular — exactly backwards. It is also
     the only LIVE number on the page: at 9→10 the whole figure changed
     width mid-roll and shoved its own label sideways. Equal-width digits
     are what a counter needs. */
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--space-2);
}
/* A zero is a real answer, not a missing one — but it should not carry the
   same weight as a count. */
.today-stat-num.is-zero { color: var(--text-muted); opacity: 0.55; }
/* Desktop (≥1024px): counter-style presentation. Two large numbers
   anchor the left column. 3.75rem = 60px. bigger than the prior 48px
   now that we're down to 2 stats and they need to carry the column on
   their own without pills/subheaders to share visual weight. Mobile
   override below at ≤767px keeps compact sizing. Tablet (768-1023)
   inherits base 4.25rem. */
@media (min-width: 1024px) {
  .today-stat-num {
    --today-stat-lh: 0.95;
    font-size: var(--text-stat);
    line-height: var(--today-stat-lh);
    letter-spacing: var(--tracking-stat);
  }
}
.today-stat-num--muted { color: var(--text-muted); opacity: 0.5; }
.today-stat-num--warning { color: var(--pill-warning-fg); }
/* SENTENCE CASE, no tracking (2026-08-08). Seven uppercase micro-labels
   were live on one screen — the eyebrow reflex at scale, where the
   treatment that was meant to mark a section marks everything and so
   marks nothing. Uppercase is now reserved for the things that really are
   section headers (TODAY, THIS MONTH, UP NEXT, FOLLOW-UP NEEDED); a
   caption under a number is a caption. */
.today-stat-label {
  font-family: var(--font-body);
  /* Was a bare 12px — one below the scale's --text-sm step for no reason
     the rule could name. The 17px line box below is unchanged, so this is
     a glyph-size correction only. Mobile keeps its own 10.5px override. */
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  /* Explicit nowrap so a longer caption never wraps to 2 lines at narrow
     column widths. */
  white-space: nowrap;
  /* Fixed box so the loading twin can occupy the identical height
     without hand-matching a placeholder to a font metric. */
  line-height: 17px;
}

/* ── Overview loading state ────────────────────────────────────────────
   One DOM, two states. The shell renders ONCE, before auth, and
   [data-loading] paints placeholders inside the final boxes. Previously
   the page did two innerHTML passes — a hand-built skeleton, then an
   empty shell that sat through two more round trips, then data — so the
   user saw three layouts and the page grew ~550px on arrival.

   Parity here is structural, not hand-matched: the placeholders reuse the
   real classes (.today-stat, .today-stat-label, .call-row), so their
   boxes are the same boxes. Edit a row's padding and both states move
   together. */
.today-sk { display: none; }
/* Explicit per-slot display — the twins reuse layout classes (.today-stats
   is a grid), so a blanket `display: block` would flatten them. */
#dashboard-content[data-loading="true"] .today-stats.today-sk { display: grid; }
#dashboard-content[data-loading="true"] .today-spark.today-sk,
#dashboard-content[data-loading="true"] .today-stathero-aside > .today-sk,
#dashboard-content[data-loading="true"] .today-stream-list > .today-sk { display: block; }
/* Data landing is a settle, not a swap. It used to be an opacity crossfade;
   it is a 3px rise now for the ghost reason below — and this one mattered
   most, because it fires when the fetch resolves, which is exactly the
   moment a backgrounded tab has no rendering opportunity to clear it with.
   Transform-only: an opacity-0 from-state on a mount animation strands the
   element invisible whenever the animation is running with a frozen clock
   (a document that has not had a rendering opportunity — background tab,
   headless/print renderer). Fill mode does NOT save it: the animation is
   active rather than pending, so `none` paints the from-state exactly as
   `both` does. Same class af4b741 removed from cardEnter. */
#dashboard-content:not([data-loading="true"]) .today-stathero-main > *,
#dashboard-content:not([data-loading="true"]) .today-stathero-aside > *,
#dashboard-content:not([data-loading="true"]) .today-stream-list > * {
  animation: todaySettle 180ms var(--ease-out) both;
}
@keyframes todaySettle { from { transform: translateY(3px); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  #dashboard-content:not([data-loading="true"]) .today-stathero-main > *,
  #dashboard-content:not([data-loading="true"]) .today-stathero-aside > *,
  #dashboard-content:not([data-loading="true"]) .today-stream-list > * {
    animation: none;
  }
}
/* Placeholders sit INSIDE the real text element and are inline-block, so the
   element's own line-height still sets the box height. This is what makes
   parity automatic: a bare `display:block; height:11px` span is shorter than
   the 17px line box it replaces, and every one of those 6px gaps compounds
   into a visible jump. */
.today-sk .skeleton { display: inline-block; vertical-align: middle; }
/* The two exceptions that genuinely own their box. */
.today-sk .today-sk-num {
  display: block;
  width: 64px;
  height: calc(1em * var(--today-stat-lh));
  border-radius: var(--radius-md);
}
.today-sk-bars .skeleton { display: block; }
.today-sk-lbl { width: 44px; height: 11px; }
/* Inline-flex link wrappers have no line box, so the placeholder's own
   height governs — match the rendered text height rather than the font size. */
.today-sk .today-stathero-month-link .skeleton,
.today-sk .today-ap-action .skeleton,
.today-sk .today-ap-more .skeleton { height: 16px; }
/* Tracks .today-spark-plot + .today-spark-bars, including the 34px label
   lane, so the loaded strip's bars land on the skeleton's bars. */
.today-sk-bars { display: flex; align-items: flex-end; gap: 5px; height: 52px; padding-right: 34px; }
/* UNIFORM, and low. The placeholder used to carry fourteen invented
   heights, so the strip animated from a fake shape to the real one and
   every bar visibly moved — the skeleton was making a claim about data it
   had not loaded. A flat low band claims only "bars go here". */
.today-sk-bars > span { flex: 1 1 0; border-radius: 3px 3px 0 0; height: 34% !important; }
@media (max-width: 767px) {
  .today-sk-bars { height: 34px; gap: 4px; }
}
.today-sk-line { display: block; height: 13px; }
/* Skeleton rows borrow .call-row itself, so height/padding/grid match by
   definition. The trailing cell must still reserve the ring's 40px box —
   without it the row collapses to min-height and the list grows on load. */
.call-row--sk { cursor: default; }
.call-row--sk > span:last-child { height: 40px; }

/* ── 14-day context strip ──────────────────────────────────────────────
   "5 calls today" is not an answer without a baseline — the hero showed a
   number with nothing to read it against, and the stat column was ~40%
   full. One thin bar per day for the last 14, today in accent. Bars are
   a background wash rather than the accent so the strip reads as context
   under the numbers, not as a second chart competing with them. */
.today-spark { margin-bottom: 0; }   /* separation is owned by .today-stathero-month */
/* Positioning context for the average guide, which is drawn ACROSS the
   bars rather than under them. */
.today-spark-plot {
  position: relative;
  padding-right: 34px;   /* the "avg N" label's lane, so it never sits on a bar */
}
.today-spark-bars {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  gap: 5px;
  /* 52, not 42 — fourteen bars at 42px in a column this wide read as a
     decorative rule rather than something you can compare days across. */
  height: 52px;
  /* The baseline the bars stand on. Without it a short bar floats. */
  box-shadow: inset 0 -1px 0 var(--separator);
}
.today-spark-bar {
  flex: 1 1 0;
  min-width: 0;
  border-radius: 3px 3px 0 0;
  background: color-mix(in srgb, var(--text-primary) 14%, transparent);
  /* Under ~3px a bar stops being perceptible; the floor is what keeps a
     one-call day a mark instead of nothing. */
  min-height: 3px;
}
.today-spark-bar--today { background: var(--accent); }
/* A day with no calls is a dot on the baseline, not a short bar. A stub
   would read as "one or two", which is not what happened. */
.today-spark-bar--zero {
  min-height: 2px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--text-primary) 22%, transparent);
  align-self: flex-end;
}

/* ── The average guide ────────────────────────────────────────────────
   A dashed rule at the 14-day mean with its value at the right end. This
   replaces the interpretation SENTENCE the deck proposed (owner ruling:
   too long, takes space) — the comparison it was going to spell out is
   readable off the geometry instead, in a fraction of the height. */
.today-spark-avg {
  position: absolute;
  left: 0;
  /* Absolute offsets resolve against the PADDING box, so `right: 34px`
     lands the line exactly on the bars' right edge and leaves the label
     lane the plot reserved. */
  right: 34px;
  z-index: 2;
  height: 0;
  border-top: 1px dashed color-mix(in srgb, var(--text-primary) 32%, transparent);
  pointer-events: none;
}
.today-spark-avg-label {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translate(calc(100% + 6px), -50%);
  font-family: var(--font-body);
  /* Still a tick label — but 10px was the only 10 on the page and off the
     token scale entirely. --text-caption (11) is the scale's smallest
     legitimate step and stays smaller than everything around it. */
  font-size: var(--text-caption);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Demoted out of the uppercase eyebrow family with the stat labels — it
   captions the strip, it does not head a section. */
.today-spark-cap {
  margin-top: var(--space-2);   /* line→line step (8); was a one-off 9 */
  /* Fixed 17px box, so moving 12 → 13 does not touch the mobile hero's
     12/32/12 rhythm — only the glyphs get bigger inside the same box. */
  line-height: 17px;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-secondary);
}
@media (max-width: 767px) {
  /* Vertical rhythm of the hero's data region (owner report 2026-08-09: the
     sentence, the strip and its caption read as one crammed clump under the
     eyebrow while the card had slack below).

     The rhythm the card is now built on, top to bottom:
       eyebrow  →12→  headline/stats   (label to its content: tight on purpose)
       headline →32→  strip            (statement to a separate module)
       bars     →12→  caption          (caption belongs TO the strip)
       strip    →20→  next section

     32 is a margin-TOP rather than extra bottom margin on the things above
     it, because two different elements precede the strip depending on the
     day: the quiet-day sentence (.today-context, mb 12 here) or the stat
     pair (.today-stats, mb 16). Correction 2026-08-09: .today-stathero-main
     is a FLEX column, so these margins sum rather than collapse — mobile's
     measured sentence→strip gap is 12 + 32 = 44, and the stat→strip gap is
     16 + 32 = 48. That is the approved mobile rendering and it is left
     exactly as-is; the desktop pass fixed the same drift the other way, by
     giving both predecessors one shared bottom margin (24) and leaving the
     strip's top margin at 0. */
  .today-spark { margin-top: var(--space-7); margin-bottom: var(--space-5); }
  .today-spark-cap { margin-top: var(--space-3); }
  .today-spark-bars { height: 34px; gap: 4px; }
}

/* Single-sentence context (quiet + dormant modes). Replaces the old
   title+sub split. one sentence tells the whole story.
   SUBJECT TIER (2026-08-09): --text-title, the same 18px the aside's
   "Aug 17 · 8:00 AM" lockup and the app-wide .section-title use. It was
   1.375rem (22) — a one-off two pixels under the page h1 (24), which is
   the worst place for a display size to sit: near enough to read as a
   mistake, far enough to read as a second voice. It is the left column's
   subject, the date lockup is the right column's, and parallel blocks get
   parallel type. Leading stays looser than .section-title's because this
   one is a SENTENCE that wraps; tracking is the tier's shared value. */
.today-context {
  /* Block→block step (24). NOTE for anyone editing this: .today-stathero-main
     is a flex column, so margins here do NOT collapse with the strip's —
     they sum. The gap below the sentence is therefore owned here, and
     .today-stats carries the same value so the quiet day and the busy day
     put the strip in the same place. */
  margin: 0 0 var(--space-6);
  max-width: 60ch;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: var(--tracking-title);
  color: var(--text-primary);
}

/* "This week · {range}" scope label between context and stats. Shown in
   quiet mode so the week-scoped numbers aren't misread as today's. */
/* Scope label + divider. Divider matches stats max-width so it doesn't
   imply a section that extends past the stat cluster. */
.today-scope {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  max-width: 460px;
  border-bottom: 1px solid var(--border);
}

/* Pills inside the hero */
.today-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.today-pills:empty { display: none; }
/* Pills row on mobile. prevent wrapping; the 2-pill rule guarantees
   exactly 2 pills (or 1) which fit side-by-side at 375px when padding
   + font-size are tuned to stay under 165px each. */
@media (max-width: 767px) {
  .today-pills {
    flex-wrap: nowrap;
    overflow: hidden;
  }
  .today-pills .today-pill {
    flex: 0 1 auto;
    min-width: 0;
  }
}

.today-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 10px 14px;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  /* 8px radius. B2B professional per Design Motion HQ reference.
     Full-pill (999px) read as playful/consumer; 8 matches the calendar
     block-off button's 6-8px range and the rest of the dashboard's
     button vocabulary. */
  border-radius: 8px;
  cursor: pointer;
  min-height: 40px;
  transition: background 180ms var(--ease-out), color 180ms var(--ease-out), border-color 180ms var(--ease-out), transform 180ms var(--ease-out);
  touch-action: manipulation;
}
.today-pill:hover {
  background: var(--bg-card);
  color: var(--text-primary);
  border-color: var(--border-hover);
  transform: translateY(-1px);
}
.today-pill:active { transform: translateY(0); }
.today-pill-num {
  font-weight: 700;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.today-pill--accent {
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-elevated));
  border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
  color: var(--accent-strong);
}
.today-pill--accent .today-pill-num { color: var(--accent-strong); }
.today-pill--accent:hover {
  background: color-mix(in srgb, var(--accent) 18%, var(--bg-elevated));
  border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
}
.today-pill--warning {
  background: color-mix(in srgb, var(--warning) 10%, var(--bg-elevated));
  border-color: color-mix(in srgb, var(--warning) 28%, var(--border));
  color: var(--pill-warning-fg);
}
.today-pill--warning .today-pill-num { color: var(--pill-warning-fg); }
.today-pill--warning:hover {
  background: color-mix(in srgb, var(--warning) 16%, var(--bg-elevated));
  border-color: color-mix(in srgb, var(--warning) 42%, var(--border));
}
.today-pill--ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text-secondary);
}
.today-pill--ghost:hover { color: var(--text-primary); background: var(--bg-elevated); }

/* ── Action panel (inside hero aside) ──────────────────────────────────
   Each section is a subsection within the aside. The aside handles
   outer padding + gap; each section handles its own typography +
   optional left-border bar (accent for UP NEXT, warning for FOLLOW-UP).
   No outer card treatment per section. the hero card is the container. */
/* Sections in the aside don't carry their own accent bar. the kind
   label's color + the aside's flex gap do the semantic separation.
   Adding bars made the aside busy; the labels are the signal. */
.today-ap-section { min-width: 0; }

.today-ap-kind {
  font-family: var(--font-display);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  text-transform: uppercase;
  margin-bottom: var(--space-3);
}
.today-ap-kind--accent { color: var(--accent-strong); }
.today-ap-kind--warning { color: var(--pill-warning-fg); }

.today-ap-time {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-2);   /* line→line step (8); mobile keeps 4 */
}
/* Balanced pairing. day anchor + time read as ONE line, not headline/
   subtitle. That was already the stated intent at 19/17 — but 1.12x is
   not "one line", it is two sizes hoping to be mistaken for one, and it
   cost the card two off-scale steps (19 and 17) to say so.
   SUBJECT TIER (2026-08-09): both halves are --text-title (18), the same
   step as .today-context and .section-title, and the hierarchy inside the
   line is carried entirely by weight (600 vs 500) and colour (primary vs
   secondary) — which is what the original comment said it wanted. The
   time also takes the display face and the tier's tracking so the pair is
   one lockup rather than two faces sharing a baseline.
   Mobile keeps its own approved 17/16 pair (see the ≤767 block). */
.today-ap-time strong {
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: var(--tracking-title);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}
.today-ap-rel {
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 500;
  letter-spacing: var(--tracking-title);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}
/* The appointment's PERSON. It used to sit at --text-body/500 — one step
   BELOW the .today-fu-who name in the section directly under it and two
   steps below the date above it, so the block's actual subject rendered as
   its smallest part (owner report 2026-08-09: "why is 'Ronnie' so tiny").
   --text-h2 is not a new value: it is what mobile already gives this exact
   element and what the follow-up rows' names use. One name size in the
   aside, at both widths. The date pair above keeps 19/17, so the block
   still descends anchor → time → name rather than flattening. */
.today-ap-who {
  font-size: var(--text-h2);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.today-ap-more {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: var(--space-3);   /* content→action step (12); mobile keeps 8 */
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--accent-strong);
  text-decoration: none;
  transition: color 180ms var(--ease-out);
}
.today-ap-more:hover { color: var(--accent); }

/* UPCOMING section. compact echo of UP NEXT. Each row reuses the exact
   same .today-ap-time / .today-ap-rel / .today-ap-who typography so the
   section reads as a "smaller UP NEXT list" without needing new type
   rules. Only difference: gap between the two rows inside a wrapper. */
.today-ap-upcoming-rows {
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.today-ap-upcoming-row > .today-ap-who {
  margin-bottom: 0;   /* no trailing margin within a row. the gap handles spacing */
}

/* THIS WEEK section. inline sentence format so it reads as a summary
   line, not a second stat block competing with the hero's main stats. */
.today-ap-section--week .today-ap-kind {
  margin-bottom: var(--space-3);   /* eyebrow→content step; mobile keeps 4 */
}
.today-ap-range-inline {
  font-weight: 500;
  color: var(--text-muted);
  letter-spacing: 0.04em;
}
.today-ap-week-line {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  line-height: 1.55;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--space-3);
  /* Allow wrap but keep natural flow for 3-digit edge cases. */
  overflow-wrap: break-word;
}
@media (max-width: 767px) {
  .today-ap-week-line { font-size: 12.5px; }
}
.today-ap-week-line strong {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.today-ap-dot {
  margin: 0 6px;
  color: var(--text-muted);
  opacity: 0.5;
}

/* ── Follow-up list variant of the next-up card ───────────────────────
   Task-list layout: date-grouped rows, each row has Mark resolved +
   Review call actions. Amber accent only on the card bar + header label
   + resolve button (semantic load). Rows stay neutral so the list reads
   as content, not a wall of warning. Count badge removed. the row
   count is self-evident from the visible list. */
.today-fu-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.today-fu-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.today-fu-group {
  display: flex;
  flex-direction: column;
}
/* Date group header. tight coupling to the row immediately below so
   it reads as the row's date label, not a floating header. */
/* Not an eyebrow — a date divider inside a section that already carries
   one ("FOLLOW-UP NEEDED"). So it does NOT join the uppercase overline
   family (two uppercase labels four pixels apart is one too many); it
   steps up to --text-sm and drops the 0.02em residual tracking, which was
   the only thing making it a sixth voice. Reads as: eyebrow 11 uppercase →
   day 13 muted → row time 13 mono / name 15 primary. */
.today-fu-day {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0;
  color: var(--text-muted);
  padding: 0;
  margin-bottom: 3px;
}
/* Single-line row: time · caller · trailing ring. Was a two-line stack
   (lead line + a wrapped action row); collapsing it halves the section's
   height and lets three follow-ups fit where two used to. */
/* Lead line + a two-button action pair. Wraps to its own line when the
   column is too narrow (mobile), sits inline when there is room (desktop
   aside is ~610px). The row itself is NOT a click target any more — Review
   call is the explicit affordance, and an invisible row-wide button
   underneath two visible ones is a redundant third target. */
.today-fu-row {
  position: relative;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding: 7px 10px;
  margin: 0 -10px;
  border-radius: var(--radius-md);
  overflow: hidden;
  max-height: 140px;   /* generous ceiling so the collapse transition has room */
  transition: opacity 200ms var(--ease-out), max-height 260ms var(--ease-out),
              padding 260ms var(--ease-out);
}
.today-fu-row--leaving {
  opacity: 0;
  max-height: 0;
  padding-top: 0;
  padding-bottom: 0;
  pointer-events: none;
}
.today-fu-lead {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  /* Capped, and that cap is the point (audit A3): the lead used to run to
     x=1037 while its own buttons started at x=1320 — 280px of blank
     between a row and the controls that act on it, with the row above
     showing the same number. Proximity is what maps a control to its
     subject; a gap that wide unmaps it. */
  flex: 1 1 200px;
  max-width: 340px;
}
.today-fu-line {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  min-width: 0;
}
/* .today-fu-reason is GONE (Roman, round 4). It went 11px muted -> 13px
   secondary on the reasoning that the sentence saying WHY someone is waiting
   was the content of the row rather than a caption under it. Making it bigger
   is what settled the question: a truncated one-line fragment of raw STT is
   not enough to act on and never was, so it was a second line of type earning
   nothing — the row is a time, a phone number and two buttons, and every
   follow-up gets opened anyway. The lead is a single line now, so
   .today-fu-lead's column stack holds exactly one child. */
.today-fu-actions {
  display: flex;
  gap: var(--space-2);
  flex-shrink: 0;
  margin-left: auto;
}

/* ── The action pair ───────────────────────────────────────────────────
   Both buttons wear the same bezel — matched height, radius, hairline and
   inset top-highlight — so they read as a deliberate pair rather than two
   unrelated controls. Hierarchy comes from the ICON, not from a fill:
   the previous "Mark resolved" was a green-filled pill, which made a
   PENDING action look like a state that had already happened. Semantic
   colour now lives on the glyph and on hover only. */
.today-fu-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 0 12px;
  border-radius: 8px;                       /* concentric inside the row's 10px */
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  white-space: nowrap;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--motion-hover) var(--ease-out),
              border-color var(--motion-hover) var(--ease-out),
              color var(--motion-hover) var(--ease-out),
              transform var(--motion-press) var(--ease-out);
}
[data-theme="light"] .today-fu-btn {
  background: var(--bg-card);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), var(--shadow-xs);
}
.today-fu-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
.today-fu-btn:active:not(:disabled) { transform: scale(0.96); }
.today-fu-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.today-fu-btn:disabled { opacity: 0.5; cursor: default; }

/* MARK HANDLED — the owner's spec, verbatim (2026-08-07). The deck
   proposed a success-TINTED rest state; that was rejected. A pending
   action must not wear the colour of the state it produces, or a job
   still on the list looks done.
   Rest:      normal white label, neutral surface, currentColor check.
   Committed: label still white, text becomes "Handled", CHECK turns green.
   No green fill on the button surface at any state — not at rest, not on
   hover, not after. Hover is a neutral lift like every other control. */
.today-fu-btn--resolve { color: var(--text-primary); }
.today-fu-btn--resolve svg { color: currentColor; }
/* The committed state. The glyph is the only thing that goes green. */
.today-fu-btn--resolve.is-handled svg { color: var(--pill-success-fg); }
@media (hover: hover) and (pointer: fine) {
  .today-fu-btn--resolve:hover:not(:disabled) {
    background: var(--bg-control-hover);
    border-color: var(--border-hover);
    color: var(--text-primary);
  }
  .today-fu-btn--review:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-hover);
    color: var(--text-primary);
  }
  .today-fu-btn--review:hover svg { transform: translateX(2px); }
}
.today-fu-btn--review svg {
  transition: transform var(--motion-hover) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .today-fu-btn { transition: background var(--motion-hover) linear, color var(--motion-hover) linear; }
  .today-fu-btn:active:not(:disabled) { transform: none; }
  .today-fu-btn--review svg { transition: none; }
}
/* Mobile: full 44px targets, and the pair splits the width evenly so the
   two buttons stay symmetric instead of sizing to their labels. */
@media (max-width: 767px) {
  .today-fu-actions { width: 100%; }
  .today-fu-btn { flex: 1 1 0; justify-content: center; min-height: 44px; }
}
/* Time is metadata, the caller is the subject — so the time steps back to
   mono/caption/muted and the number carries the row. Previously both were
   primary-coloured and the row had no focal point. */
/* --text-sm, not --text-caption: 11px put the call's own timestamp on the
   smallest step the scale has, beside a 15px name, and the owner could not
   read it. Mono + muted still say "metadata"; the size no longer says
   "footnote". 13 against the name's 15 keeps the row's focal point. */
.today-fu-time {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The caller's name leads the row, so it clears the reason under it rather
   than matching it. */
/* NAME TIER: --text-h2, the same token .today-ap-who uses. h2 and h3 are
   the same 15px, but naming the tier twice is how a tier drifts apart. */
.today-fu-who {
  font-size: var(--text-h2);
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
/* ── Activity stream ───────────────────────────────────────────────── */
.today-stream {
  padding: var(--space-5) var(--space-6) var(--space-4);
}
.today-stream-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: var(--space-3);
  margin-bottom: 0;
  border-bottom: 1px solid var(--border);
}
/* No override. "Recent calls" uses the app-wide .section-title base
   (display face, --text-title 18/600, --tracking-title) so Overview and
   every other page share one heading voice — which is what the note here
   already claimed while a 15px override sat directly above it saying
   otherwise. 15 also put the section heading a full step BELOW the 18px
   .call-row-num inside the rows it heads; a title cannot be smaller than
   its own content. 18 is the same subject tier the hero card above now
   uses, so the heading and the card speak at one level (2026-08-09).
   The row height is set by the 40px "See all calls" link, so nothing
   above or below this moves. */

/* List container. renderCallRows() fills this with .call-row-list.
   We only shape the outer padding. */
/* Rows are inset 12px from the card's 24px padding, so the list bleeds
   slightly wider than the header/footer — the standard inset-list offset. */
.today-stream-list {
  padding: var(--space-2) 0;
  margin: 0 -12px;
}

/* Now lives in .today-stream-head (right side), not under the list. */
.today-stream-more {
  flex-shrink: 0;
  padding-left: var(--space-3);
}
.today-stream-more:hover { color: var(--accent); transform: translateX(1px); }

/* ── Mobile ────────────────────────────────────────────────────────────
   Full spacing pass (v12): hero card was ~50% taller than needed on
   375px because desktop rhythm leaked through. Every vertical gap
   inside the hero is explicitly shrunk below. Desktop (≥1280) stays
   as-is. scoped under @media (max-width: 767px).
   ────────────────────────────────────────────────────────────────── */
@media (max-width: 767px) {
  /* 1. Hero outer. tighter card-to-card margin (was space-6 / 24). */
  .today-stathero {
    padding: 0;
    margin-bottom: var(--space-4);
  }
  /* Main region: 16 / 20 / 12 (top / sides / bottom). Bottom is tight
     so the mobile divider to the aside sits close to the pills. */
  .today-stathero-main {
    padding: var(--space-4) var(--space-5) var(--space-3);
  }
  /* Aside region: 12 / 20 / 16. Top is tight to close the main-aside
     gap. Sections inside take over from there. */
  .today-stathero-aside {
    padding: var(--space-3) var(--space-5) var(--space-4);
  }
  /* 2. Eyebrow → stats (was space-5 / 20). */
  .today-stathero-head { margin-bottom: var(--space-3); }
  /* 3. Stats → pills (was space-6 / 24). */
  .today-stats {
    gap: var(--space-4);
    margin-bottom: var(--space-4);
    max-width: none;
  }
  .today-stat-num {
    font-size: 2.25rem;
    letter-spacing: -0.03em;
    line-height: 0.95;
    margin-bottom: var(--space-2);
  }
  .today-stat-label {
    font-size: 10.5px;
    letter-spacing: 0.04em;
  }
  .today-context {
    margin-bottom: var(--space-3);
    font-size: 16px;
    line-height: 1.4;
    /* Restated, not inherited: the desktop pass eased the base tracking to
       --tracking-title. At 16px that adds ~4px of width to the sentence,
       and the approved mobile layout budgets it to one line. */
    letter-spacing: -0.018em;
  }
  .today-scope {
    font-size: var(--text-caption);
    padding-bottom: var(--space-3);
    margin-bottom: var(--space-3);
    max-width: none;
  }
  .today-pill {
    font-size: 14px;
    padding: 11px 16px;
    min-height: 44px;
  }

  /* 6. UP NEXT + section internals. kind label → time → who tighter. */
  .today-ap-kind { margin-bottom: var(--space-2); }
  .today-ap-who  { margin-bottom: var(--space-2); font-size: var(--text-h2); }

  /* UP NEXT time row on mobile: bring the pair even closer than desktop.
     17 / 16 (1.06x ratio) reads as one compound line. weight + color
     carry the hierarchy, not size. THIS MONTH weekly totals match so
     the two aside sections feel like a consistent pair. */
  /* The desktop pass of 2026-08-09 collapsed this pair onto one size (18)
     and moved it to the display face at --tracking-title. Mobile's hero is
     signed off as it stands, so every metric it was approved with is
     restated here rather than inherited: 17/700 + 16/500, body face on the
     time, no tracking, and the 4px / 8px rhythm steps it already had. */
  .today-ap-time strong { font-size: 1.0625rem; font-weight: 700; }  /* 17px */
  .today-ap-rel {
    font-size: 1rem;                       /* 16px */
    font-family: var(--font-body);
    letter-spacing: normal;
  }
  .today-ap-time { margin-bottom: 4px; }
  .today-ap-more { margin-top: var(--space-2); }
  .today-ap-wnum        { font-size: 1.0625rem; }  /* matches strong for rhythm */

  /* 7. Section divider margins tightened (was space-4 / 16 × 2 = 32). */
  .today-stathero-aside > .today-ap-section + .today-ap-section {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
  }

  /* 8. THIS MONTH internals tightened. */
  .today-ap-section--week .today-ap-kind { margin-bottom: 4px; }
  .today-ap-week-line {
    margin-bottom: var(--space-2);
    font-size: 12.5px;
  }

  /* Follow-up rows: the row IS the tap target now (44px min-height comes
     from the base rule), and the trailing ring already carries a 40px box.
     The old two-button layout's mobile overrides are gone with it. */

  /* Stream card. inherits the board's compact mobile rhythm. */
  .today-stream { padding: var(--space-4) var(--space-5) var(--space-3); }
  .today-stream-more { min-height: 44px; }
}

/* ── Very narrow (<360px): stack stats vertically if needed ────────── */
@media (max-width: 359px) {
  .today-stats { grid-template-columns: 1fr; gap: var(--space-3); }
  .today-stat {
    display: flex;
    width: 100%;
    max-width: none;
    align-items: baseline;
    gap: var(--space-3);
    justify-content: space-between;
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
  }
  .today-stat:last-child { border-bottom: none; padding-bottom: 0; }
  .today-stat-num { font-size: 2.2rem; margin-bottom: 0; }
}

/* ─── .card--lit. illuminated surface ────────────────────────────────────
   Matches the calendar card's lit-panel treatment (vertical gradient, inner
   top highlight, ambient drop shadow). Apply SELECTIVELY to primary content
   surfaces. Secondary cards stay flat so the hierarchy reads. */
.card.card--lit {
  background: var(--surface-lit-bg);
  border: var(--surface-lit-border);
  box-shadow:
    var(--surface-lit-highlight),
    var(--surface-lit-shadow);
}

/* ─── .kpi-card--lit. illuminated KPI tile ────────────────────────────────
   Same lit language applied to the KPI card family. Keep structural rules
   from .kpi-card, override only surface treatment so all four cards in a
   row read as one group (Gestalt similarity). Scoped to client dashboard.
   admin KPIs continue to use the flat base. */
.kpi-card.kpi-card--lit {
  background: var(--surface-lit-bg);
  border: var(--surface-lit-border);
  box-shadow:
    var(--surface-lit-highlight),
    var(--surface-lit-shadow);
}
.kpi-card.kpi-card--lit:hover {
  box-shadow:
    var(--surface-lit-highlight),
    var(--surface-lit-shadow-strong);
  transform: translateY(-1px);
}

/* Hero KPI value is larger */
.kpi-card.kpi-hero .kpi-value {
  font-size: 3.75rem;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums; /* explicit. guards against future overrides */
}

.kpi-trend {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-family: var(--font-mono);
  font-size: var(--text-overline);
  font-weight: 500;
  margin-top: var(--space-2);
  padding: 3px var(--space-2);
  border-radius: var(--radius-full);
  font-variant-numeric: tabular-nums;
  position: relative;
  z-index: 1;
}

.kpi-trend.up   { color: var(--pill-success-fg); background: var(--success-muted); }
.kpi-trend.down { color: var(--pill-danger-fg);  background: var(--danger-muted); }
.kpi-trend.flat { color: var(--text-muted); background: var(--bg-elevated); }

/* Dense KPI variant — admin-analytics strip (S7 de-island). The one
   modifier that replaces the former .aa-kpi parallel family: smaller
   value + a lower min-height floor, everything else stays canonical
   .kpi-card vocabulary (26px/1.625rem tile value vs 37.6px overview). */
.kpi-card--sm { min-height: 118px; }
.kpi-card--sm .kpi-value { font-size: 1.625rem; }
/* Muted "%" glyph trailing a rate value so the number reads first. */
.kpi-value .pct { color: var(--text-muted); font-size: var(--text-sm); margin-left: 3px; }
/* Sub-label under a KPI value (window / scope note, e.g. "last 30 days"). */
.kpi-sub {
  font-size: var(--text-overline);
  color: var(--text-muted);
  letter-spacing: 0.02em;
  margin-top: var(--space-1);
  position: relative;
  z-index: 1;
}

.kpi-sparkline {
  margin-top: var(--space-3);
  height: 36px;
  position: relative;
  z-index: 1;
}
.kpi-sparkline canvas {
  width: 100% !important;
  height: 36px !important;
}

/* KPI mobile: 2x2 grid, hero full-width */
@media (max-width: 767px) {
  .kpi-grid-3, .kpi-grid-4, .kpi-grid-admin, .kpi-grid-hero {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3);
  }
  .kpi-grid-3 > :first-child,
  .kpi-grid-hero > :first-child {
    grid-column: auto;
  }
  .kpi-card.kpi-hero {
    grid-column: 1 / -1;
    padding: var(--space-5) var(--space-5);
  }
  .kpi-card.kpi-hero.analytics-kpi-hero {
    grid-column: auto;
  }
  .kpi-card {
    padding: var(--space-4);
    min-height: 120px;
    border-radius: var(--radius-lg);
  }
  .kpi-label {
    font-size: 0.6875rem;
    margin-bottom: var(--space-2);
  }
  .kpi-label-icon {
    flex-basis: 15px;
    width: 15px;
    height: 15px;
  }
  .kpi-label-icon svg {
    width: 14px;
    height: 14px;
  }
  .kpi-label-icon--booked > svg:first-child {
    width: 15px;
    height: 15px;
  }
  .kpi-label-icon--booked > svg:last-child {
    width: 8px;
    height: 8px;
  }
  .kpi-value {
    font-size: 1.9rem;
  }
  .kpi-card.kpi-hero .kpi-value {
    font-size: 2.75rem;
  }
  .kpi-grid-hero > .kpi-card.kpi-hero {
    padding: var(--space-4);
  }
  .kpi-sparkline { height: 28px; }
  .kpi-sparkline canvas { height: 28px !important; }
  .kpi-label {
    font-size: var(--text-caption);
  }
}

/* ── THE CHIP PRIMITIVE ──────────────────────────────────────────────────
   2026-08-07: the earlier chrome pass tried this and backed out, because
   .badge had a SECOND canonical base ~11k lines below that pinned height
   22 / line-height 1 / letter-spacing .005em and silently won for every
   badge the JS emits. That base has now been RECONCILED: it declared no
   property this one doesn't, so its geometry block is deleted and only its
   colour tokens, icon rule and shape modifiers remain. There is one base,
   and it is here — first, so every semantic variant declared later in the
   file still wins on source order for colour.

   One height (24), one radius (6), one face (Inter 11/600, tracking 0),
   one tint recipe. Variants set --chip-hue + color and nothing else; the
   fill and the hairline are derived, so a new state cannot invent a fifth
   alpha. --chip-hue defaults to currentColor, which is what makes an
   unvarianted chip a legible neutral instead of the old opaque #1F1F25
   that was invisible on lit card gradients.

   Full-round is a MODIFIER (.badge--pill), not the default: a 6px chip
   next to 6px badges and 5px segments belongs to the radius ladder, and
   the pill silhouette is reserved for semantic status. */
.tag,
.pill,
.badge,
/* The call-detail drawer's chips (2026-08-09). They carried a verbatim copy
   of this recipe in call-detail.js's inline stylesheet, and a mobile override
   sitting under that copy had quietly grown them to a 12px face on 12px side
   padding while the list they mirror stayed at 11/8. Owner report: the drawer
   chips look "much longer and stretched" next to the same chip in the calls
   list. One selector, one recipe — the fork cannot reopen. */
.cd-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: var(--chip-h);
  padding: 0 8px;
  border-radius: var(--chip-radius);
  font-family: var(--font-body);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1;
  white-space: nowrap;
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, var(--chip-hue, currentColor) var(--chip-tint), transparent);
  border: 1px solid color-mix(in srgb, var(--chip-hue, currentColor) var(--chip-edge), transparent);
  color: var(--text-secondary);
}

.tag-success { --chip-hue: var(--success); color: var(--pill-success-fg); }
.tag-danger  { --chip-hue: var(--danger);  color: var(--pill-danger-fg); }
.tag-warning { --chip-hue: var(--warning); color: var(--pill-warning-fg); }
.tag-info    { --chip-hue: var(--info);    color: var(--pill-info-fg); }
.tag-neutral { color: var(--text-secondary); }
.tag-brand   { --chip-hue: var(--accent);  color: var(--accent); }

/* ── Glyphs inside a chip ───────────────────────────────────────────────
   The list's chips carry a 10px check (.pill-booked-check) or a 12px mask
   glyph (.cd-pill.cat-*::before). The drawer's carried real lucide icons at
   .icon-inline--sm's 14px, with .icon-inline--compact's 4px margin stacked ON
   TOP of the chip's own 5px gap — a 40% bigger glyph pushed 9px off its
   label. Sizing belongs to the chip, not to a general-purpose icon class, so
   it is stated once here.

   BOTH selectors on purpose: lucide REPLACES <i data-lucide> with an <svg>
   at runtime, so a rule written against either one alone covers half the
   page's life. .pill-booked-check is excluded — it is already optically
   sized at 10px and is the reference the sentiment glyph is judged against. */
.cd-pill > i[data-lucide],
.cd-pill > svg:not(.pill-booked-check) {
  width: var(--chip-glyph, 12px);
  height: var(--chip-glyph, 12px);
  margin: 0;
  flex-shrink: 0;
  /* line-height:1 makes the label's box cap-height plus descender depth, so
     its centre sits BELOW the cap-height centre a glyph should align to.
     Half a pixel up is the whole correction at this size. */
  transform: translateY(-0.5px);
}
/* Optical sizing, not equal boxes. A face glyph fills its box corner to
   corner; the check beside it covers barely half of one. At a shared 12px the
   smiley reads a size larger than everything it sits next to, which is the
   owner's "smiley not proportionate". The round faces come down to 11. */
.cd-pill-positive,
.cd-pill-negative {
  --chip-glyph: 11px;
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.tag-remove {
  cursor: pointer;
  opacity: 0.5;
  font-size: var(--text-body);
  line-height: 1;
  transition: opacity var(--duration-fast) ease, color var(--duration-fast) ease;
  min-width: 20px;
  min-height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.tag-remove:hover { opacity: 1; color: var(--danger); }

.tag-add-input {
  border: 1px dashed var(--border-hover);
  border-radius: var(--radius-full);
  padding: 3px 8px;
  font-size: var(--text-overline);
  min-width: 72px;
  outline: none;
  background: transparent;
  color: var(--text-primary);
  transition: border-color var(--duration-normal) ease;
}
.tag-add-input:focus { border-color: var(--border-focus); }

/* Single-dash semantic variants are declared at end-of-file so they win
   the cascade over the second .badge base below (Phase 8 fix: the later
   base was clobbering these, rendering every status badge neutral). */

/* ── Status (Phase 1.7 — 2026-05-24) ──
   Quiet `dot + label` pattern intended for status cells in tables
   (call log, admin tables). Replaces tinted-pill backgrounds that
   competed with row content. The dot carries the semantic color via
   a tiny ring shadow; the label stays at --fg-secondary so the row
   reads as data first, status second.

   Use:  <span class="status status--success">Booked</span>
         <span class="status status--warning">Follow-up</span>
         <span class="status status--danger">Failed</span>
         <span class="status">Voicemail</span>       (neutral)

   Reserve tinted pills (.badge / .pill) for detail-panel headers
   and KPI annotations — not table cells. */
.status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--fg-secondary);
  font-size: var(--text-body);
  line-height: 1.3;
  white-space: nowrap;
}
.status::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fg-tertiary);
  flex: none;
}
.status--success::before {
  background: var(--success);
  box-shadow: 0 0 0 3px rgba(var(--success-rgb), 0.12);
}
.status--warning::before {
  background: var(--warning);
  box-shadow: 0 0 0 3px rgba(var(--warning-rgb), 0.12);
}
.status--danger::before {
  background: var(--danger);
  box-shadow: 0 0 0 3px rgba(var(--danger-rgb), 0.12);
}
.status--info::before {
  background: var(--info);
  box-shadow: 0 0 0 3px rgba(var(--info-rgb), 0.12);
}
.status--accent::before {
  background: var(--accent);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12);
}

/* ── BOOKED — the chip that has earned a step, not a spotlight ───────────
   Owner ruling 2026-08-07: a booking is the product doing its most
   valuable thing, and the chip that says so should feel like it.

   ROUND 3, 2026-08-08. Rounds 1 and 2 answered that with an OPAQUE,
   saturated cap — the only non-tinted object in the chip family, a deep
   emerald with iOS cap lighting and a success bloom under it. Round 2
   deepened the ramp to #0A6B47/#064A31 on the reasoning that a jewel
   absorbs light rather than throwing it, and that is where it broke: the
   owner's verdict was "too dark, sticks out, distracting". He is right, and
   the diagnosis is not the darkness by itself. Every other chip in the row
   is a translucent tint of its own hue; this one was an opaque block. It
   did not read as the most important chip, it read as a chip from a
   different design system that had been pasted in — and on a white card it
   read as a hole.

   So the target moved from "the loudest object" to "unmistakably the same
   family, unmistakably a step above". The chip returns to the family
   grammar — a --success tint, a --success rim, --success ink, all through
   the same color-mix recipe every sibling uses — and buys its distinction
   on five channels instead of one:
     · tint 18% dark / 16% light, against the siblings' 12% / 10%.
     · a 70% / 60% rim, against the siblings' 30% / 24%. On a chip this
       small the rim is the loudest channel that costs no legibility, and a
       2.3x step in it is plainly visible at a glance.
     · ink one step past --pill-success-fg in each theme (see below).
     · a lit top edge and a 1px contact shadow — the only chip in the row
       that sits ON the surface rather than in it. Craft, not colour.
     · the check glyph, which no sibling carries at all.

   The ink is per-theme and it had to be MEASURED, not chosen. In dark, a
   denser tint over a dark card darkens the ground, so a light-mint ink
   gains contrast for free (#6FE0B8 measures 11.58:1 against the sibling's
   8.38:1). In LIGHT the same move runs backwards: a denser tint over a
   WHITE card also darkens the ground, and the ink there is dark, so simply
   raising 10% → 16% COSTS contrast. A candidate that did exactly that
   measured 3.38:1 — under AA, and worse than the 4.56:1 sibling it was
   meant to beat. #0F5132 restores it to 6.46:1. That asymmetry is the
   whole reason this rule has a light half.

   White ink was the other fork the owner offered, and it was built and
   rejected on numbers: white needs roughly a 38%+ fill to hold in dark
   (fine) and something far past 70% in light, at which point the chip is
   opaque mid-green and white measures 2.67:1. Keeping white ink means
   keeping a bespoke opaque ramp in light — which is precisely the thing
   round 2 got wrong. Success-coloured ink, like every other chip.

   Radius stays on the ladder and height stays 24 — it was never louder in
   geometry and it still isn't. */
.pill--booked,
.badge.pill--booked,
.badge-success.pill--booked,
.pill.pill--booked,
.cd-pill.pill--booked {
  position: relative;
  /* Load-bearing: it clips the arrival shine to the chip. */
  overflow: hidden;
  background: color-mix(in srgb, var(--success) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--success) 70%, transparent);
  color: #6FE0B8;
  font-weight: 600;
  letter-spacing: -0.005em;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.10),
    0 1px 2px rgba(0, 0, 0, 0.28);
}
[data-theme="light"] .pill--booked,
[data-theme="light"] .badge.pill--booked,
[data-theme="light"] .badge-success.pill--booked,
[data-theme="light"] .pill.pill--booked,
[data-theme="light"] .cd-pill.pill--booked {
  background: color-mix(in srgb, var(--success) 16%, transparent);
  border-color: color-mix(in srgb, var(--success) 60%, transparent);
  color: #0F5132;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.70),
    0 1px 2px rgba(20, 20, 22, 0.10);
}
.pill-booked-check {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
}

/* THE ARRIVAL — fires when a booking LANDS, and only then.
   Scale 0.9 → 1 on the critically-damped settle (the same curve the
   selection pill and the segmented thumb use, so "something arrived" means
   one thing across the app), plus a single shine sweeping the cap once.

   THE RE-FIRE GATE, which is the hard part. Every list surface here
   rebuilds with `container.innerHTML = …`, so the pill is a brand-new node
   on every filter change, search keystroke and page step — keying the
   animation off element creation would re-celebrate a booking from last
   Tuesday every time the operator typed a letter. The gate is therefore an
   ARRIVAL class, not node identity: `.is-arriving` is set by realtime.js
   and by nothing else, on exactly the one path where a call genuinely just
   appeared. Re-renders cannot produce it.

   First paint deliberately gets NO pill animation. The rows are already
   running their own staggered entrance, and a second animation inside a
   moving row is motion on motion; eight simultaneous shine sweeps is a
   wall, not a reward. The permanent material carries first paint. */
@keyframes bookedArrive {
  from { transform: scale(0.9); }
  to   { transform: scale(1); }
}
@keyframes bookedShine {
  from { transform: translateX(-160%) skewX(-18deg); }
  to   { transform: translateX(260%) skewX(-18deg); }
}
.is-arriving .pill--booked {
  animation: bookedArrive var(--motion-settle) var(--ease-settle) both;
}
/* The shine is kept — it is the celebrated moment, and the owner asked for it
   to stay — but its alpha was tuned against an OPAQUE deep-emerald cap and
   had to be re-measured against the tint that replaced it. 0.52 white over a
   deep cap is a highlight; over an 18% tint on a dark card it composites to a
   pale grey band, which is a flash rather than a reflection. Hence 0.30 in
   dark. Light runs the opposite way for the same reason the ink does: a white
   sweep over a 16% tint on a white card is very nearly invisible, so it goes
   UP to 0.88 to register at all. Per-theme, like everything else on this
   chip. */
.is-arriving .pill--booked::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 36%;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.30) 50%,
    rgba(255, 255, 255, 0) 100%);
  pointer-events: none;
  animation: bookedShine 620ms var(--ease-out) 140ms both;
}
[data-theme="light"] .is-arriving .pill--booked::after {
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.88) 50%,
    rgba(255, 255, 255, 0) 100%);
}
@media (prefers-reduced-motion: reduce) {
  .is-arriving .pill--booked { animation: none; }
  .is-arriving .pill--booked::after { display: none; }
}

.badge-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  display: inline-block;
}
.badge-dot.green  { background: var(--success); }
.badge-dot.yellow { background: var(--warning); }
.badge-dot.blue   { background: var(--info); }
.badge-dot.red    { background: var(--danger); }
.badge-dot.gray   { background: var(--text-muted); }

/* Pill variants — geometry comes from the chip primitive above. */
.pill-success { --chip-hue: var(--success); color: var(--pill-success-fg); }
.pill-warning { --chip-hue: var(--warning); color: var(--pill-warning-fg); }
.pill-danger  { --chip-hue: var(--danger);  color: var(--pill-danger-fg); }
.pill-brand   { --chip-hue: var(--accent);  color: var(--accent); }

/* Category modifiers. stack on .pill or .cd-pill. Question / Other /
   legacy categories fall through to the neutral base (.pill / .cd-pill-default).
   Tokens defined in global.css. Spam sets neutral styling explicitly so it
   works on .cd-pill too (which has no default fill) and then recedes via opacity. */
.pill.cat-booking,
.cd-pill.cat-booking   { --chip-hue: var(--success); color: var(--pill-success-fg); }
.pill.cat-complaint,
.cd-pill.cat-complaint { --chip-hue: var(--danger);  color: var(--pill-danger-fg); }
.pill.cat-returning,
.cd-pill.cat-returning { --chip-hue: var(--info);    color: var(--pill-info-fg); }
.pill.cat-spam,
.cd-pill.cat-spam      {
  color: var(--text-secondary);
  /* Recede, but keep the chip silhouette readable on dark rows — at 0.55
     with no border the dark variant vanished entirely (audit A4). The
     neutral tint now comes from the primitive, so only the fade is local. */
  opacity: 0.65;
}
/* Light already rendered correctly at the original recipe — pin it. */
[data-theme="light"] .pill.cat-spam,
[data-theme="light"] .cd-pill.cat-spam {
  opacity: 0.55;
}

/* Status badges. backward compat */
.status-active     { --chip-hue: var(--success); color: var(--pill-success-fg); }
.status-paused     { --chip-hue: var(--warning); color: var(--pill-warning-fg); }
.status-onboarding { --chip-hue: var(--info);    color: var(--pill-info-fg); }
.status-churned    { color: var(--text-muted); }
.status-testing    { --chip-hue: var(--info);    color: var(--pill-info-fg); }
/* Defensive: ensure no pseudo-element adds a dot to legacy .status-* badges.
   .status-* variants are legacy text-only badges and the explicit .badge-dot
   sibling element provides any leading dot when present. Reset ::before so
   unforeseen cascade can't double-render. */
.status-active::before, .status-paused::before, .status-onboarding::before,
.status-churned::before, .status-testing::before { content: none; }
.status-archived   { color: var(--text-muted); }

/* Sentiment */
.sentiment-positive { color: var(--pill-success-fg); display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }
.sentiment-neutral  { color: var(--text-secondary); display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }
.sentiment-negative { color: var(--pill-danger-fg); display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }

/* Client calls table — icon-only sentiment keeps its semantic color
   (VF-7, owner 2026-07-03: positive = green, negative = red). Neutral and
   unknown stay muted. Text-tier tokens so both themes clear 3:1 non-text. */
/* 16px, not 14 (audit F8). At 14 the smile and the flat mouth differ by
   about two pixels of curve — the two most common values in the column
   were the two hardest to tell apart. The title/aria-label carries the
   word for anyone who still cannot read the face. */
.cell-sentiment { display: inline-flex; align-items: center; color: var(--text-muted); vertical-align: middle; }
.cell-sentiment svg { width: 16px !important; height: 16px !important; }
.cell-sentiment--positive { color: var(--pill-success-fg); }
.cell-sentiment--negative { color: var(--pill-danger-fg); }

/* Client calls status ring (VF-3, owner ruling 2026-07-03 — supersedes the
   Phase-3 "nothing when handled" inversion). The circle is ALWAYS visible
   on client rows, tinted by what the row means: amber = open follow-up,
   green = booked/positive, neutral = everything else. data-ring is set by
   the shared row/card builders in call-table.js; strokes use the --pill-*-fg
   text-tier tokens so both themes clear non-text contrast. Checked state
   settles to a dimmed filled mark (visibly done, never gone) — see the
   .star-btn.starred rules with the base machinery further down. */
.star-btn[data-ring="warning"]:not(.starred) .ring-svg circle { stroke: var(--pill-warning-fg); }
.star-btn[data-ring="success"]:not(.starred) .ring-svg circle { stroke: var(--pill-success-fg); }

/* ── Buttons ─────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: 500;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  /* Hover state transitions tightened to --duration-fast (100ms). Transform
     (press feedback scale(0.97)) keeps --duration-normal (200ms) because
     fast-scale reads like a glitch. Linear/Raycast benchmark. */
  transition: background var(--duration-fast) var(--ease-in-out), color var(--duration-fast) var(--ease-in-out), border-color var(--duration-fast) var(--ease-in-out), box-shadow var(--duration-fast) var(--ease-in-out), transform var(--duration-normal) var(--ease-out), opacity var(--duration-fast) var(--ease-in-out);
  white-space: nowrap;
  min-height: 36px;
  min-width: 36px;
  -webkit-tap-highlight-color: transparent;
  /* Counts live in button labels ("Load 1,284 more", "Delete 3 selected")
     and they change under the pointer — proportional figures make the
     label jump sideways mid-interaction. */
  font-variant-numeric: tabular-nums;
}

/* UI refresh 2026-08-07: the press has to arrive ON pointer-down, and the
   base 200ms transform above is the delay Apple describes as the moment
   directness "falls off a cliff". Shortening every property on :active
   (not just transform) is deliberate — a press should snap in and ease
   back out, so this is the in-curve and the base rule is the out-curve. */
.btn:active {
  transition-duration: var(--motion-press);
}

/* lucide REPLACES <i data-lucide> with an <svg> carrying the <i>'s classes, so
   an `i`-only rule silently stops matching the moment icons hydrate — every
   button icon in the app was falling back to lucide's intrinsic 24px and this
   authored 15px never applied. Dual form per the codebase's established
   pattern (.admin-detail-text-action i, … svg).
   Safe by construction: no .btn in this app contains a hand-authored <svg>
   (verified by scan), so this only ever matches a lucide glyph. Tap targets
   are unaffected — the chassis owns them (.btn min-height/min-width: 36px,
   .btn-icon 44px), never the glyph. */
.btn i,
.btn svg { width: 15px; height: 15px; flex-shrink: 0; }

/* `.btn { display: inline-flex }` is an AUTHOR-origin declaration, so it beats
   the UA stylesheet's `[hidden] { display: none }` outright (origin wins over
   specificity) — every `<button class="btn" hidden>` in the app was rendering
   anyway. Same footgun the .notif-prompt-card / .agents-test-chip-slot rules
   already patch per-component; this fixes it for the whole button family.
   Known beneficiaries: #agents-deploy-btn (hidden per Roman 2026-07-26) and
   #bcast-back in admin-clients.js. */
.btn[hidden] { display: none; }

/* UI refresh 2026-08-07: the filled variants are layered like an iOS cap —
   a bright inset top edge and a dark inset bottom edge so the surface
   reads as a lit solid rather than a flat rectangle, a 1px contact shadow
   that anchors it to the page, and two ambient layers carrying the lift.
   Tokenised per theme (--shadow-btn-*): light gets far less accent bloom,
   because saturated indigo glow on white reads cheap. Backgrounds are
   deliberately untouched, so the AA-audited label contrast is preserved. */
.btn-primary {
  background: var(--accent);
  color: var(--text-inverse);
  box-shadow: var(--shadow-btn-primary);
}
.btn-primary:hover {
  background: var(--accent-hover);
  box-shadow: var(--shadow-btn-primary-hover);
  transform: translateY(-1px);
}
/* Press inverts the lighting: the ambient lift collapses and the cap
   sinks into its own shadow, which is what a physical key does. */
.btn-primary:active {
  transform: scale(0.97);
  box-shadow: var(--shadow-btn-press);
}

.btn-secondary {
  background: var(--bg-elevated);
  color: var(--text-primary);
  border: 1px solid var(--border);
}
.btn-secondary:hover {
  border-color: var(--border-hover);
  background: var(--bg-card-hover);
  transform: translateY(-1px);
}
.btn-secondary:active {
  transform: scale(0.97);
}

.btn-ghost {
  color: var(--text-secondary);
  padding: var(--space-1) var(--space-2);
}
/* A ghost button has no chrome to lift — the tint IS the affordance, and
   floating a borderless label off the surface reads as a glitch. Removed
   the -1px it shared with the filled variants. */
.btn-ghost:hover {
  color: var(--text-primary);
  background: var(--accent-muted);
}
.btn-ghost:active {
  transform: scale(0.97);
}

/* #fff on purpose, not --text-inverse: the fill is a fixed deep red in both
   themes, so the ink that sits on it is fixed too. --text-inverse would put
   near-black on it in dark mode. */
.btn-danger {
  background: var(--danger-fill);
  color: #fff;
  box-shadow: var(--shadow-btn-fill);
}
/* Hover darkens the fill, the way every other filled variant swaps to its
   own hover token. It used to be `opacity: 0.9`, which let the page show
   through a solid cap — and on a light page that lifted the fill until
   white ink measured 4.07:1, so the hover state failed AA even after the
   fill itself had been fixed. */
.btn-danger:hover {
  background: var(--danger-fill-hover);
  transform: translateY(-1px);
}
.btn-danger:active {
  transform: scale(0.97);
  box-shadow: var(--shadow-btn-press);
}

/* reduced-motion opt-out for the press scale. It has to live HERE, beside
   the variants: each of them declares `transform` at (0,2,0), a media query
   contributes no specificity, and global.css loads first — so the copy of
   this block that used to sit there lost on source order and never fired. */
@media (prefers-reduced-motion: reduce) {
  .btn-primary:active,
  .btn-secondary:active,
  .btn-ghost:active,
  .btn-danger:active {
    transform: none;
  }
}

/* Premium pass: consistent keyboard focus ring across every button variant.
   :focus-visible only, so mouse clicks don't show the ring. */
.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg-page), 0 0 0 4px var(--accent);
}
.btn-primary:focus-visible {
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.22),
              inset 0 -1px 0 rgba(0,0,0,0.16),
              0 0 0 2px var(--bg-page), 0 0 0 4px var(--accent-strong);
}

.btn-sm {
  font-size: var(--text-sm);
  padding: var(--space-1) var(--space-3);
  min-height: 30px;
}

.btn-icon {
  padding: var(--space-1);
  min-height: 44px;
  min-width: 44px;
}

.btn:disabled,
.btn.loading {
  opacity: 0.5;
  pointer-events: none;
  cursor: not-allowed;
}

/* Disabled primary buttons: saturated indigo at opacity 0.5 still reads
   as primary, making the disabled state ambiguous. Desaturate so the
   blocked state is unmistakable. .btn-primary.loading is intentionally
   excluded — loading should retain primary identity (user just clicked). */
.btn-primary:disabled {
  filter: saturate(0.3);
  opacity: 0.45;
}

.btn.loading .btn-label {
  visibility: hidden;
}

.btn.loading::after {
  content: '';
  position: absolute;
  width: 14px;
  height: 14px;
  border: 2px solid rgba(var(--accent-rgb), 0.25);
  border-top-color: var(--text-inverse);
  border-radius: 50%;
  animation: spin 0.5s linear infinite;
}

.btn .spinner {
  width: 14px;
  height: 14px;
  border: 2px solid rgba(var(--accent-rgb), 0.25);
  border-top-color: var(--text-inverse);
  border-radius: 50%;
  animation: spin 0.5s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Touch targets for mobile. 44px minimum on all buttons */
@media (max-width: 767px) {
  .btn { min-height: 44px; }
  /* Export and every error-state Try again are .btn-sm; small is a type
     size on a phone, not a hit size. */
  .btn-sm { min-height: 44px; }
  .btn-icon { min-height: 44px; min-width: 44px; }
}

/* ── Segmented Control ───────────────────── */
/* The trough. Neutral, not accent-tinted (2026-08-07): with an elevated
   neutral thumb riding in it, an indigo-washed groove reads as a tinted
   box rather than a recess, and the accent belongs to the selected label.
   Lit from the top INSIDE, which is what makes a groove look sunken. */
.seg-control,
.segmented,
.segmented-control {
  display: inline-flex;
  position: relative;
  overflow: visible;
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(12px) saturate(1.3);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  border: 0.5px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--radius-md);
  padding: 3px;
  gap: 2px;
  /* The TROUGH owns the control's height (36, the default step), and the
     buttons stretch into it. It used to be the other way round, so a page
     that wanted roomier segment padding got a 44px control next to 36px
     selects — the tallest of the five heights on admin-calls. */
  min-height: 36px;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.26), 0 1px 0 rgba(255, 255, 255, 0.04);
}

/* Trough fill stays at the audited 0.04: an unselected label is
   --text-muted ON this surface, and deepening it to 0.055 dropped that to
   4.44:1 against a white card. The groove now comes from the inset
   highlight below, not from a darker fill. */
[data-theme="light"] .seg-control,
[data-theme="light"] .segmented,
[data-theme="light"] .segmented-control {
  background: rgba(0, 0, 0, 0.04);
  border: 0.5px solid rgba(0, 0, 0, 0.05);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07), 0 1px 0 rgba(255, 255, 255, 0.7);
}

.seg-btn,
.segmented-btn {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  padding: var(--space-1) var(--space-3);
  /* Concentric with the 8px container minus its 3px padding. Was 5px
     already; the sliding .seg-indicator below was 6px and is now matched. */
  border-radius: 5px;
  color: var(--text-muted);
  transition-property: color, background, box-shadow, scale;
  transition-duration: var(--duration-normal);
  transition-timing-function: var(--ease-out);
  white-space: nowrap;
  /* Floor only — the trough's 36px and `align-items: stretch` set the
     real height. Mobile's 44px tap floor still overrides both. */
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
  position: relative;
  z-index: 1;
  font-variant-numeric: tabular-nums;
}

.seg-btn:hover,
.segmented-btn:hover {
  color: var(--text-secondary);
}

/* UI refresh 2026-08-07 — the iOS segmented control. The selected segment
   was a solid indigo block, which is the vocabulary of a primary ACTION;
   a segmented control is a one-of-N choice and shouting it competes with
   the real actions on the page. iOS answers with an elevated THUMB in a
   recessed trough: the surface carries the elevation, the label carries
   the accent. That is the same selection grammar the sidebar's pill now
   uses, so "selected" means one thing across the whole app — and it keeps
   PRODUCT.md's rule that hierarchy comes from weight, not colour.
   (Filter chips stay filled on purpose: they are independent on/off
   toggles, not a one-of-N segment, and iOS tints those too.) */
.seg-btn.active,
.segmented-btn.active {
  background: var(--seg-thumb-bg);
  color: var(--seg-thumb-label, var(--accent));
  font-weight: 600;
  box-shadow: var(--seg-thumb-shadow);
  transform: scale(1);
}

/* L-3 (2026-05-16) is RETIRED here, 2026-08-07. It existed to stop the
   agent-edit tab strip inheriting a saturated accent FILL that competed
   with primary actions — a real problem when .seg-btn.active was a solid
   indigo block. It isn't one any more: the base active state above is now
   a quiet elevated thumb with an accent label, which is exactly what L-3
   was reaching for. Keeping the fork would leave the 11-tab strip as the
   one segmented control in the app that looks different, which is the
   inconsistency L-3 was written to avoid. The strip now shares the thumb.
   Its only remaining local rule is the disabled state below. */

/* v55. tactile feedback on segmented + filter-chip presses. Uses `scale:`
   (independent scale property) so it doesn't fight the box-shadow transform
   on the active state. `transform: scale()` would override the 1× above.
   2026-08-07: shortened to --motion-press so the feedback lands on
   pointer-down instead of easing in over the base 200ms, and lifted to
   0.97 to match the rest of the button family. */
.seg-btn:active,
.segmented-btn:active {
  scale: 0.97;
  transition-duration: var(--motion-press);
}

/* The two-layer ring, punched against the control's own trough rather
   than --bg-page — the global rule cuts a page-coloured hole in the
   middle of the segmented control. */
.seg-btn:focus-visible,
.segmented-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .seg-btn:active,
  .segmented-btn:active { scale: 1; }
}

/* Period toggle. full-width with equal buttons on mobile */
.period-toggle {
  flex-shrink: 0;
}

@media (max-width: 767px) {
  /* ALL segmented controls stretch full-width on mobile, buttons share space equally */
  .segmented,
  .segmented-control,
  .seg-control,
  .period-toggle {
    width: 100%;
    display: flex;
  }

  .segmented .seg-btn,
  .segmented .segmented-btn,
  .segmented-control .seg-btn,
  .segmented-control .segmented-btn,
  .seg-control .seg-btn,
  .period-toggle .seg-btn,
  .period-toggle .segmented-btn {
    flex: 1;
    text-align: center;
    padding: var(--space-2) var(--space-1);
    min-height: 44px;  /* HIG tap-target floor; was 40px */
    font-size: var(--text-sm);
    white-space: nowrap;
  }

  .seg-btn,
  .segmented-btn {
    min-height: 44px;  /* HIG tap-target floor; was 40px */
  }
}

/* Sliding indicator for segmented controls. Same thumb as the static
   .active state above, so a control that slides and a control that
   doesn't are visually identical — only one of them animates. Radius is
   concentric: 8px container − 3px padding = 5px (was 6px). Travel is on
   the critically damped settle, matching the sidebar's pill. */
.seg-indicator {
  position: absolute;
  top: 3px;
  bottom: 3px;
  height: auto;
  left: 3px;
  background: var(--seg-thumb-bg);
  border-radius: 5px;
  box-shadow: var(--seg-thumb-shadow);
  /* Travel is transform-only — see initSlidingIndicator in js/utils.js for
     why, and for the fixed-reference-width trick that lets scaleX carry the
     difference between segments. Left and width used to animate here. */
  transform-origin: left center;
  transition: transform var(--motion-settle) var(--ease-settle);
  z-index: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .seg-indicator { transition: none; }
}

.has-indicator .seg-btn.active,
.has-indicator .segmented-btn.active {
  background: transparent !important;
  color: var(--seg-thumb-label, var(--accent)) !important;
  box-shadow: none !important;
}
/* …but not the focus ring. That `box-shadow: none !important` exists to stop
   the button painting its own thumb behind the sliding one — it was never
   meant to erase keyboard focus, and it silently did, because the ring is
   also a box-shadow. It matters here specifically: the roving-tabindex
   handler in utils.js focuses the ACTIVE segment by design, and
   admin-clients.js opens its feedback panel with
   initialFocusSelector: '.seg-btn.active'. Matching !important is the only
   thing that outranks the rule above. */
.has-indicator .seg-btn.active:focus-visible,
.has-indicator .segmented-btn.active:focus-visible {
  box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent) !important;
}

/* ── Responsive period labels ────────────── */
.seg-label-short { display: none; }

@media (max-width: 767px) {
  .seg-label-full { display: none; }
  .seg-label-short { display: inline; }
}

/* ── Search ──────────────────────────────── */
.search-wrapper {
  position: relative;
  flex: 1;
  min-width: 300px;
}

.search-wrapper .search-icon {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
  width: 16px;
  height: 16px;
}

/* Height is EXPLICIT, not min-height. With min-height 36 and 10px of
   vertical padding the box fell out of its own padding at 37.5-38px and
   sat 2px proud of the 36px select beside it on three pages. A control
   scale only holds if the controls are told their height.

   …and it has to be told BOTH, because a `height` alone never held: most
   search fields also carry `.form-input`, whose `min-height: 40px` is a
   different property, so specificity never entered into it — the floor
   simply won and the field rendered at 40 against 36px neighbours. The
   giveaway was that the two search fields WITHOUT `.form-input`
   (admin-clients, admin-agents) were the only ones ever measuring 36:
   the rendered height was tracking an unrelated class rather than this
   rule. Pinning min-height to the same number is what makes the authored
   value the used one, on every consumer, with or without `.form-input`. */
.search-wrapper .search-input {
  width: 100%;
  height: 36px;
  min-height: 36px;
  padding: 0 14px 0 40px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  background: var(--bg-input);
  color: var(--text-primary);
  transition: border-color var(--duration-normal) ease;
}

/* Active-field cue only. The ring is the app's ONE focus ring, and it is
   an outline — see the note in global.css. */
.search-wrapper .search-input:focus {
  border-color: var(--border-focus);
}

.search-wrapper .search-input::placeholder {
  color: var(--text-muted);
}

@media (max-width: 767px) {
  .search-wrapper {
    min-width: unset;
    width: 100%;
  }
  /* 44, not the 42 that used to be authored here: 42 was never what three
     of the five search fields rendered, because `.form-input`'s mobile
     `min-height: 44px` floor raised them and the two fields without that
     class stayed at 42. 44 is the app's own tap floor (.btn carries it at
     this width too), so pinning both properties to it makes every search
     field agree with every button beside it instead of splitting on a
     class the geometry should never have depended on. */
  .search-wrapper .search-input {
    height: 44px;
    min-height: 44px;
    padding: 0 14px 0 40px;
    font-size: 0.9375rem;
  }
}

/* ── Tables ──────────────────────────────── */
.table-container {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-radius: var(--radius-xl);
  /* The card edge drops from .12 to the hairline tier: at .12 it was
     louder than the row rules inside it, so the container shouted and the
     data whispered. Depth is the shadow's job now that the dark ramp
     actually renders. */
  border: 1px solid var(--border-subtle);
  box-shadow: var(--shadow-md);
  /* Scroll cue (design-sprint T6): when the table overflows sideways a
     right-edge shadow whispers "more columns"; the local-attachment cover
     layer slides over it at scroll-end — and on tables that fit, the cover
     sits on the shadow permanently. Pure CSS, no JS bookkeeping. */
  background:
    linear-gradient(to left, var(--bg-card) 30%, transparent) right center / 56px 100% no-repeat local,
    linear-gradient(to left, var(--table-scroll-shadow, rgba(0, 0, 0, 0.35)), transparent) right center / 20px 100% no-repeat scroll,
    var(--bg-card);
}
[data-theme="light"] .table-container { --table-scroll-shadow: rgba(17, 17, 21, 0.10); }

/* The calls table is a wide-table surface (U1): the same opt-in the to-do
   board uses, on the shell measure rather than a px of its own. */
#content.calls-shell { --shell-measure: var(--shell-wide); }

/* STICKY HEADERS. `.data-table th` has declared `position: sticky; top: 0`
   for a long time and it never worked here, because `overflow-x: auto`
   on this container makes the CONTAINER the nearest scrollport for those
   th's — and the container has no vertical overflow, so the header has
   nothing to stick within and leaves the viewport with the rest of the
   page after ~7 rows.

   HISTORY, kept because the trade was real. The obvious fix, `overflow-x:
   clip`, un-scrollports the container so sticky resolves against the
   viewport. On 2026-08-07 it was rejected: the admin calls table then
   measured ~1550px wide against a ~1100px content column at 1440, so
   clipping would have made several columns permanently unreachable. The
   container became a scrollport on BOTH axes instead (`max-height:
   calc(100vh - 200px); overflow-y: auto`), which gave sticky what it
   needs at the price of a second vertical scrollbar nested inside the
   page's — accepted knowingly, and the most visible generic-admin tell on
   the operator's most-used table (O4).

   WHY CLIP IS SAFE NOW (2026-09-04). The premise expired on 2026-08-16:
   both calls tables are `table-layout: fixed` with a percentage colgroup
   (_adminCallsColgroup / _clientColgroup in js/call-table.js), so a table
   is exactly its container's width at any width with any data — there is
   no horizontal overflow left to orphan. The two-axis scrollport outlived
   the reason for it. And admin-calls now runs on the wide shell (U1,
   #content.calls-shell above), so at ≥1600 the seven columns get up to
   1680px of column rather than 1280.

   Reasoned at 1440 with the rail open: 1440 − 260 rail − 80 gutters =
   1100px column; the admin colgroup sums to 100% of it, and every cell
   ellipsises inside its track. Nothing to scroll sideways; the header
   sticks to the viewport; ONE scrollbar. `.table-card` clips with
   `overflow: clip` for the same reason — `hidden` is a scrollport too, and
   would have caught the header at the card's top edge instead.

   The `local`-attachment cover layer in the scroll cue above is inert on a
   table that fits (it sits on the shadow permanently), which is now every
   render of these two tables.

   Opt-in via a modifier so the blast radius is the two calls tables and
   not every .table-container in the app. */
@media (min-width: 1024px) {
  .table-container--sticky {
    overflow-x: clip;
  }
}

/* Keyboard reach (design-sprint T6): call rows are focusable; ring inset so
   the container's rounded corners don't clip it. Matches the app's accent
   focus language. The analytics breakdown row (O14) is the same object with
   a different verb (pin, not open), so it shares the ring rather than
   carrying a second one. */
tr.clickable:focus-visible,
tr.aa-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-family: var(--font-body);
  font-size: var(--text-body);
  /* Matches the cell padding, so the row rules start and end where the
     content does. Surfaces that retune td padding retune this with it. */
  --table-sep-inset: var(--space-4);
}

.data-table {
  /* The column header's own height, so the day sub-headers know where to
     stop. Measured (Chrome, Inter 11/600 + 12px padding) at 42px; kept as a
     token because two rules depend on it agreeing. Only the desktop sticky
     container uses it — below 1024px the list renders as cards. */
  --table-head-h: 42px;
}

.data-table th {
  text-align: left;
  font-weight: 600;
  color: var(--text-muted);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid rgba(128, 128, 128, 0.12);
  white-space: nowrap;
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--bg-card);
}

/* Fixed column model — see _clientColgroup() in js/call-table.js for why. */
.data-table--fixed { table-layout: fixed; }
.data-table--fixed td,
.data-table--fixed th { overflow: hidden; text-overflow: ellipsis; }

/* ── Day sub-headers (audit A6) ────────────────────────────────────────
   "Aug 6" was printed thirteen times in one viewport with nothing dividing
   the days. The day is stated once, the rows carry the time alone, and the
   header sticks directly under the column header so you always know which
   day you are looking at — the same shape as a macOS grouped list. */
/* `.data-table th` is (0,1,1) and declares `top: 0; z-index: 3` — a bare
   `.day-group-head` loses to it and sticks at the container's top edge, ON
   TOP of the column header. The class has to be qualified to outrank it. */
.data-table .day-group-head {
  position: sticky;
  top: var(--table-head-h);
  z-index: 2;
  text-align: left;
  padding: var(--space-3) var(--space-4) 6px;
  font-family: var(--font-display);
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  white-space: nowrap;
  border-bottom: none;
  /* Opaque: rows scroll UNDER this, so it cannot be translucent without
     the row showing through its own label. */
  background: var(--bg-card);
  box-shadow: inset 0 -1px 0 var(--separator);
}
/* The rule the group header already draws makes the first row's own
   separator a doubled line. */
.data-table tbody .day-group-row + tr td {
  background-image: none;
}

.data-table th.sortable {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.data-table th.sortable:hover { color: var(--text-secondary); }
/* Lucide chevrons-up-down sized to match the caption-weight header type. */
.data-table th .sort-icon {
  display: inline-block;
  margin-left: 4px;
  width: 14px;
  height: 14px;
  vertical-align: -3px;
  opacity: 0.45;
  transition: opacity 120ms ease;
}
.data-table th.sortable:hover .sort-icon { opacity: 0.7; }
.data-table th.sorted .sort-icon { opacity: 1; }

/* 12px, down from 20px. At 20px a call row measured 84px tall and its
   content measured 44px, so more than half of every row was air — and both
   the one-line and the two-line caller variants came out at exactly 84px,
   which is the tell that padding, not content, was setting the height. A
   call list is something an operator scans; 84px puts seven rows on a
   laptop screen. 12px brings the row to ~68px and about ten. The analytics
   tables already ran at 12px (.aa-table-card below), so this is the base
   catching up with the value the app had already settled on rather than a
   new number. */
.data-table td {
  padding: var(--space-3) var(--space-4);
  vertical-align: middle;
}

/* Row rules, same object as the overview's inset-grouped list: one 1px
   --separator, inset to the content edge, square ends. They were per-cell
   `border-bottom`s at rgba(128,128,128,.08) running the full table width
   into the container's 16px corner radius, a third weight next to the .12
   header rule above them and the .06 card edge around them.

   Painted as a background-image band on the cell's top edge rather than a
   border, because that is the only way to inset the ENDS of the line: the
   two outer cells drop the inset side. (A `tr::before` is the obvious
   move and is wrong — the engine wraps a pseudo-element inside a
   table-row in an anonymous table-cell, which shifts every real cell one
   column right. Verified in Chrome before switching.)

   The header keeps its full-bleed border on purpose: a header rule is
   structural — it separates chrome from data — and a row rule is a list
   separator. macOS draws that distinction the same way. */
.data-table tbody tr + tr td {
  background-image: linear-gradient(var(--separator), var(--separator));
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: left top;
}
.data-table tbody tr + tr td:first-child {
  background-size: calc(100% - var(--table-sep-inset)) 1px;
  background-position: right top;
}
.data-table tbody tr + tr td:last-child {
  background-size: calc(100% - var(--table-sep-inset)) 1px;
  background-position: left top;
}
/* A full-width cell (colspan dividers, empty states) is both ends at once. */
.data-table tbody tr + tr td:first-child:last-child {
  background-size: calc(100% - var(--table-sep-inset) * 2) 1px;
  background-position: center top;
}

/* Mono font for numeric table data only — by CLASS, never by position.
   `td:nth-child(5)` used to lead this list, from a column order that no
   longer exists: on the client calls table (direction hidden) the 5th cell
   is Category, so human words like "Returning" were rendering in JetBrains
   Mono next to an Inter status chip. Every numeric cell that wants mono
   already ships an explicit `.mono` span or one of the classes below. */
.data-table .td-duration,
.data-table .td-data {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Numeric columns right-align so digits line up (cells already tagged .num) */
th.num,
td.num { text-align: right; }

/* `background` is a shorthand — it animates background-image and
   background-position too, and the row separators ARE background-images on
   the cells, so the shorthand put every hairline on a timer it did not need
   to be on. Name the one property that changes, and use the app's easing
   token instead of the UA's `ease`. */
.data-table tbody tr {
  transition: background-color var(--motion-hover) var(--ease-out),
              box-shadow var(--motion-hover) var(--ease-out);
}

/* Hover state: deepened background tint for lift perception. The former
   3px accent inset stripe is gone (VF-5 side-stripe sweep, owner
   2026-07-03) — the tint alone carries hover. */
.data-table tbody tr:hover {
  background: rgba(var(--accent-rgb), 0.05);
}

/* Same rule as the overview list: a full-bleed highlight must not sit
   against an inset line, or the tint reads as overhanging both ends of
   it. The hovered row's own rule and its next sibling's go together. */
.data-table tbody tr:hover td,
.data-table tbody tr:hover + tr td,
.data-table tbody tr.active td,
.data-table tbody tr.active + tr td,
/* Press has to suppress them too. The :active rule below tints the ROW,
   but the separators are background-images on the CELLS, so without this a
   pressed row kept its hairlines while hover dropped them — and on touch,
   where press is the only state that ever shows, that was the only state
   getting it wrong. */
.data-table tbody tr.clickable:active td,
.data-table tbody tr.clickable:active + tr td {
  background-image: none;
}

.data-table tbody tr.active {
  background: rgba(var(--accent-rgb), 0.065);
}

/* Press answers with a tint step, never a scale — a 25-row table that
   shrinks under the pointer reads as a bug (admin-calls E6). */
.data-table tbody tr.clickable:active {
  background: rgba(var(--accent-rgb), 0.10);
  transition-duration: 0ms;
}

/* Feels-more-Apple: on Retina, table separators refine to a 0.5px hairline
   (one physical pixel at 2dppx) — the iOS/macOS list-divider weight,
   matching the sidebar's existing 0.5px border. Width only; colors and
   rules unchanged. */
@media (min-resolution: 2dppx) {
  .data-table th { border-bottom-width: 0.5px; }
  .data-table tbody tr + tr td { background-size: 100% 0.5px; }
  .data-table tbody tr + tr td:first-child,
  .data-table tbody tr + tr td:last-child { background-size: calc(100% - var(--table-sep-inset)) 0.5px; }
  .data-table tbody tr + tr td:first-child:last-child { background-size: calc(100% - var(--table-sep-inset) * 2) 0.5px; }
}

.data-table tbody tr.clickable {
  cursor: pointer;
}

[data-theme="light"] .data-table tbody tr:hover {
  background: rgba(var(--accent-rgb), 0.045);
}

/* The last row's rule went with the per-cell border it was cancelling —
   `tr + tr::before` never draws below the final row. */

/* Booked rows: no stripe (VF-5). The premium Booked badge + green status
   ring carry the state. */

/* Booked indicator cell */
.data-table td.td-booked {
  white-space: nowrap;
}

.data-table td.td-booked .booked-indicator {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 500;
}

.data-table td.td-booked .booked-indicator .booked-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.data-table td.td-booked .booked-indicator.is-booked .booked-dot {
  background: var(--success);
}
.data-table td.td-booked .booked-indicator.is-booked {
  color: var(--pill-success-fg);
}

.data-table td.td-booked .booked-indicator.not-booked .booked-dot {
  background: var(--text-muted);
  opacity: 0.5;
}
.data-table td.td-booked .booked-indicator.not-booked {
  color: var(--text-muted);
}

/* ── Call Cards (mobile) ─────────────────────────────────────────────
   Three-zone card: primary (caller + optional org), optional signals
   (pills), footer (duration · timestamp). Star is absolute top-right.
   State reads from the pills + status ring; background tints (follow-up
   amber, booked hover green) are the only card-level state channels
   (VF-5: no stripes). */
/* ── Call row — inset grouped list (js/call-table.js → renderCallRows) ──
   The overview's Recent-calls surface. Flat rows inside one container,
   hairline separators, no per-row border/shadow/radius, so it never
   nests a card inside a card. Row height is set here rather than derived
   from content — that is what lets the loading skeleton be a zero-shift
   twin of the loaded row (see the .call-row-sk rules in the skeleton
   block). Trailing control is the handled ring; the row itself opens the
   detail panel, so there is no second "open" affordance. */
.call-row-list {
  display: flex;
  flex-direction: column;
}
.call-row {
  position: relative;
  /* The row's own horizontal padding, so the separator inset and the
     padding can never drift apart (mobile retunes one value, not two). */
  --row-pad-x: 12px;
  display: grid;
  /* icon · caller (the elastic one) · pill · meta · ring. FOUR things and
     a ring, down from five and a ring: the summary column is gone (round 3)
     and the caller takes the space it was holding rather than the row
     closing up around the gap. A row you scan should have few enough
     elements that the eye lands on the one that matters — the number —
     without picking it out of a line of similar-sized text.

     The pill and meta columns are FIXED, not `auto` (Roman, 2026-08-10).
     With `auto` each column sized to its own row's content, so the chips
     started at a different x on every line and nothing down the list lined
     up — half of why this card read cluttered. Fixed widths give the pill
     one x for the whole list, which is what lets a row with no pill read as
     "nothing to do" instead of as ragged spacing. 116px fits "Follow-up",
     the widest of the two; 92px fits "22 hours ago" before timeAgo rolls
     over to a date. */
  grid-template-columns: 22px minmax(0, 1fr) 116px 92px 40px;
  align-items: center;
  gap: var(--space-4);
  /* Fixed row height for BOTH states. The trailing ring's 40px box plus
     padding already lands near this; pinning it means the loading twin and
     the loaded row clamp to the same number instead of drifting by whatever
     the tallest cell happens to be. 64px, not 56: with one fewer column the
     row can afford the air, and the whole complaint was that it read
     crammed. */
  min-height: 64px;
  padding: 10px var(--row-pad-x);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--motion-hover) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
/* THE SEPARATOR (owner report 2026-08-07: "the edges look broken").
   It was `box-shadow: 0 -1px 0` on a row with `border-radius: 8px`. An
   outer box-shadow is the border box painted again at the offset and
   clipped to the SAME radius, so the 1px band between the two arcs closes
   to zero over the last 8px at each end: the rule tapered off and died
   before it reached the row's edges. That is the broken edge, and no
   colour or alpha change could have fixed it.

   A pseudo-element rectangle has no radius, so its ends are square, and
   it is inset to the row's own content edge — which is what an
   inset-grouped list does on macOS and iOS: the rule starts where the
   text starts. The `+` combinator means there is no rule above the first
   row and none below the last, by construction. */
.call-row + .call-row::before {
  content: '';
  position: absolute;
  top: 0;
  inset-inline: var(--row-pad-x);
  height: 1px;
  background: var(--separator);
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .call-row:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); }
  /* The highlight is full-bleed and the rule is inset, so a rule left
     touching a highlighted row reads as 12px of highlight hanging past
     each end of the line — the overhang in the audit. Both the hovered
     row's own rule and its next sibling's go, so the highlight never has
     a line to overhang. `content` is never set here, so these match
     nothing on the first row. */
  .call-row:hover::before,
  .call-row:hover + .call-row::before { opacity: 0; }
}
.call-row:active { background: color-mix(in srgb, var(--text-primary) 8%, transparent); }
.call-row:active::before,
.call-row:active + .call-row::before { opacity: 0; }
/* Full-row hit target that carries keyboard focus. */
.call-row-open {
  position: absolute; inset: 0; z-index: 1;
  border: 0; background: transparent; border-radius: inherit; cursor: pointer;
}
.call-row-open:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.call-row-icon {
  position: relative;
  display: grid; place-items: center;
  color: var(--text-muted);
}
.call-row-icon svg { width: 15px; height: 15px; }
.call-row-new-dot {
  position: absolute; top: -1px; right: -3px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
}
/* The one thing in the row you are actually looking for, so it is allowed
   to be the one thing that is large. --text-title (18px), not --text-h3:
   h2 and h3 are both 15px in this scale, so "promote it to h2" would have
   been a no-op — 18px is the next real step the scale has. */
.call-row-num {
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* .call-row-summary is retired everywhere — see renderCallRows in
   js/call-table.js for why the sentence left the row. */
/* Left-aligned in its fixed column, so every pill shares one x. */
.call-row-signals {
  display: flex; align-items: center; gap: 6px;
  flex-shrink: 0;
  justify-content: flex-start;
}
/* Was --text-caption (11px) in mono, which is where the "everything is
   tiny" reading came from: the row had a 20px number and then three
   different flavours of small. 13px, and the mono stays because duration
   is a number you compare down a column. */
.call-row-meta {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-align: right;
}
.call-row-meta-sep { margin: 0 6px; opacity: 0.5; }
/* Ring sits in the trailing cell; 40px box keeps the hit area honest
   while the glyph stays 20px. */
.call-row-star {
  z-index: 2;
  justify-self: end;
  display: grid; place-items: center;
  width: 40px; height: 40px;
}
.call-row-star .ring-svg { width: 20px; height: 20px; }

/* Mobile: the trailing meta drops under the number, pills keep their own
   line. Same rows, no card chrome, no horizontal squeeze. */
@media (max-width: 767px) {
  .call-row {
    --row-pad-x: 10px;
    grid-template-columns: 22px 1fr 40px;
    grid-template-areas: 'i n r' '. s r' '. m r';
    row-gap: 4px;
    padding: 10px var(--row-pad-x);
    min-height: 64px;
  }
  .call-row-icon { grid-area: i; align-self: start; margin-top: 3px; }
  .call-row-num { grid-area: n; }
  .call-row-signals { grid-area: s; flex-wrap: wrap; }
  .call-row-meta { grid-area: m; text-align: left; }
  .call-row-star { grid-area: r; align-self: center; }
}

/* Mobile cards: /admin-calls only since C5 (2026-09-05). The client list
   draws .call-row below 768px, the same rows as the Overview, so the
   client-only card rules (the handled ring's corner placement) are gone. */
.call-card-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.call-card {
  position: relative; /* anchor for the absolute new-dot */
  background: var(--bg-card);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-in-out), border-color var(--duration-fast) var(--ease-in-out), box-shadow var(--duration-fast) var(--ease-in-out), transform var(--duration-fast) var(--ease-in-out);
  -webkit-tap-highlight-color: transparent;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.call-card:hover {
  background: rgba(var(--accent-rgb), 0.04);
  border-color: var(--border-hover);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

.call-card:active {
  transform: scale(0.99);
  transition: transform var(--duration-fast) ease;
}

.call-card-open {
  position: absolute;
  inset: 0;
  z-index: 1;
  border: 0;
  border-radius: inherit;
  background: transparent;
  cursor: pointer;
}

.call-card-open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Booked card: no stripe (VF-5) — the premium Booked pill + green ring
   carry the state; hover keeps its faint green cast. */
.call-card.booked:hover {
  background: rgba(52, 211, 153, 0.04);
}
[data-theme="light"] .call-card.booked:hover {
  background: rgba(var(--success-rgb), 0.03);
}

/* ── Primary zone. caller + optional org sub-line ───────────────── */
.call-card-primary {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding-right: 44px; /* leave space for the absolute star */
  min-width: 0;
}

.call-card-caller {
  display: flex;
  align-items: center;
  gap: 0; /* children control their own gaps for tight control around the new-dot */
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-h3);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  min-width: 0;
}

.call-card-caller-prefix {
  display: inline-flex;
  align-items: center;
  gap: 6px; /* tight cluster: direction icon + optional new-dot */
  flex-shrink: 0;
}

.call-card-dir-icon {
  width: 14px;
  height: 14px;
  color: var(--text-muted);
  flex-shrink: 0;
}

.call-card-new-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}

.call-card-number {
  margin-left: 8px; /* space between prefix cluster and the number */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.call-card-org {
  font-size: var(--text-sm);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Signals row. wrapping pill row. Renders only if at least one
   pill survives the suppression rules in getCallPillVisibility(). */
.call-card-signals {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

/* ── Footer. muted metadata line (duration · timestamp) ─────────── */
.call-card-footer {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.call-card-footer-sep {
  opacity: 0.5;
}

/* ── Booked pill on cards: unified onto .pill--booked (VF-4) ────── */

/* ── Sentiment pills. reuse the pill-success/danger token family so
   a positive call reads in the same green as Booking, and a negative
   call reads in the same red as Complaint. Neutral is never rendered. */
.pill.sentiment-positive {
  background: var(--pill-success-bg);
  color: var(--pill-success-fg);
}
.pill.sentiment-negative {
  background: var(--pill-danger-bg);
  color: var(--pill-danger-fg);
}

@media (max-width: 767px) {
  .call-card {
    padding: var(--space-4);
  }
  .call-card-caller {
    font-size: 1.0625rem; /* keep ~17px on mobile */
  }
  .call-card-org {
    font-size: var(--text-sm);
  }
}

/* ── Forms ───────────────────────────────── */
.form-group { margin-bottom: var(--space-4); }

/* ── Greeting clip panel (Fix 3 Phase 2, 2026-05-14) ─────
   Lives inside the General tab's Greeting card, between the
   Welcome message textarea and the Pause-before-speaking slider.
   Mirrors the spacing rhythm of the rest of the form. */
.gr-section > * + * {
  margin-top: var(--space-3);
}
.gr-section .gr-row-label {
  font-size: var(--text-caption);
  color: var(--text-secondary);
  margin-bottom: var(--space-1);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.gr-pending-list > * + * {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.gr-pending-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.gr-controls-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.gr-controls-row .form-hint {
  margin: 0;
}

.form-label {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: var(--space-1);
}

.form-input,
.form-select,
.form-textarea {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-body);
  color: var(--text-primary);
  background: var(--bg-input);
  transition: border-color var(--duration-normal) ease, box-shadow var(--duration-normal) ease;
  min-height: 40px;
}

/* Border only — the second half of the double ring. See global.css. The
   `outline: none` that used to sit here was worse than redundant: at
   (0,2,0) it outranked `input:focus-visible` (0,1,1), so it was deleting
   the keyboard ring on every .form-* field in the app. The global
   `*:focus { outline: none }` (0,1,0) still suppresses the mouse case and
   loses to focus-visible, which is the behaviour that was wanted. */
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
  border-color: var(--border-focus);
}

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

/* ONE chevron for the whole app (admin-calls D2). The glyph here was a
   solid 4×4 triangle — the macOS `<select>` mark — sitting twelve pixels
   from a lucide chevron on the same toolbar, so two different arrow
   languages met on one row. It is now the lucide `chevron-down` path at
   the same 2px stroke and round joins as every other icon, at the muted
   text tier so it recedes behind the value it belongs to.

   The colour is baked into the data URI (a background-image cannot read
   `currentColor`), which is why light gets its own declaration below —
   the dark #8A8A98 disappears on a white field.

   `background-size` is explicit because the source is a 24-unit lucide
   viewBox: without it the mark renders at 24px and crowds the edge. */
.form-select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238A8A98' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 15px 15px;
  padding-right: var(--space-8);
  cursor: pointer;
}
[data-theme="light"] .form-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B6B78' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.form-textarea {
  min-height: 80px;
  resize: vertical;
  /* Form controls don't inherit the document's line-height, and `normal`
     sets Inter at 13px about four pixels tighter than the rest of the app's
     prose. This lived on the duplicate base that used to shadow this one. */
  line-height: var(--leading-body);
}

/* ── Checkboxes + radios ─────────────────────────────────────────────────
   The app shipped 45 native controls on the agent editor alone: 13×13px
   OS-blue boxes sitting next to a hand-built indigo design system. They
   were the loudest remaining "unstyled form" tell, and at 13px they were
   also the smallest hit targets in the product.

   These rules are deliberately UNSCOPED. A checkbox is a checkbox on every
   page, and the alternative — a `.form-check` class threaded through eleven
   files — guarantees the drift it was meant to prevent. Two carve-outs:

     · `.sr-only` inputs (the deleted wizard-step4 used visually-hidden
       radios) keep the utility's 1×1 clip. `.sr-only` alone is (0,1,0) and
       would LOSE to
       `input[type=radio]` (0,1,1), so the exclusion is explicit here.
     · Switch patterns (.notif-switch, .agency-switch) hide their input and
       paint a track sibling. Their rules already zero the input's box and
       win on specificity/order, but the reset below states it outright so a
       future reorder can't silently un-hide sixty switches.

   Geometry: 16px box, 4px radius (the --radius-xs step), 1px border. The
   glyph is a clip-path check that scales from 0 — a real transition, not a
   background-image swap, so checking something is felt. */
input[type="checkbox"]:not(.sr-only),
input[type="radio"]:not(.sr-only) {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 16px;
  height: 16px;
  /* Floors, not decoration: the deleted js/wizard-step3.js shipped an inline
     `style="width:auto"` on one checkbox, which would otherwise shrink the
     box to its 10px glyph. A min pair survives any consumer's inline width. */
  min-width: 16px;
  min-height: 16px;
  margin: 0;
  padding: 0;
  display: inline-grid;
  place-content: center;
  background: var(--bg-input);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xs);
  color: var(--text-inverse);
  cursor: pointer;
  /* Optical, not geometric: 16px box against 13px body text sits high on
     the baseline. -3px seats the box on the text's optical centre. */
  vertical-align: -3px;
  transition-property: background-color, border-color, box-shadow;
  transition-duration: var(--motion-hover);
  transition-timing-function: var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
input[type="radio"]:not(.sr-only) {
  border-radius: var(--radius-full);
}

input[type="checkbox"]:not(.sr-only)::before,
input[type="radio"]:not(.sr-only)::before {
  content: "";
  width: 10px;
  height: 10px;
  background: currentColor;
  transform: scale(0);
  transition: transform var(--motion-hover) var(--ease-out);
}
input[type="checkbox"]:not(.sr-only)::before {
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
input[type="radio"]:not(.sr-only)::before {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  clip-path: none;
}

input[type="checkbox"]:not(.sr-only):hover,
input[type="radio"]:not(.sr-only):hover {
  border-color: var(--border-focus);
}

input[type="checkbox"]:not(.sr-only):checked,
input[type="radio"]:not(.sr-only):checked {
  background: var(--accent);
  border-color: var(--accent);
}
input[type="checkbox"]:not(.sr-only):checked::before,
input[type="radio"]:not(.sr-only):checked::before {
  transform: scale(1);
}
input[type="checkbox"]:not(.sr-only):indeterminate {
  background: var(--accent);
  border-color: var(--accent);
}
input[type="checkbox"]:not(.sr-only):indeterminate::before {
  transform: scale(1);
  height: 2px;
  border-radius: 1px;
  clip-path: none;
}

/* The app's one focus ring, punched against the card the control sits on. */
input[type="checkbox"]:not(.sr-only):focus-visible,
input[type="radio"]:not(.sr-only):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent);
}

/* Disabled reads through tokens, not an opacity wash — a .4 opacity box on
   a dark card lands near 1.7:1 and disappears rather than reading as
   "off, and not yours to change". */
input[type="checkbox"]:not(.sr-only):disabled,
input[type="radio"]:not(.sr-only):disabled {
  background: var(--bg-card-subtle);
  border-color: var(--border-subtle);
  color: var(--fg-disabled);
  cursor: not-allowed;
}
input[type="checkbox"]:not(.sr-only):disabled:hover,
input[type="radio"]:not(.sr-only):disabled:hover {
  border-color: var(--border-subtle);
}
input[type="checkbox"]:not(.sr-only):disabled:checked,
input[type="radio"]:not(.sr-only):disabled:checked {
  background: var(--bg-control-hover, var(--bg-elevated));
  border-color: var(--border);
}

@media (prefers-reduced-motion: reduce) {
  input[type="checkbox"]:not(.sr-only),
  input[type="radio"]:not(.sr-only),
  input[type="checkbox"]:not(.sr-only)::before,
  input[type="radio"]:not(.sr-only)::before { transition: none; }
}

/* Switch escape hatch — stated, not inferred. See the note above. The
   min-* pair matters as much as width/height: the floors above would
   otherwise re-inflate an input these patterns had collapsed to zero.
   The questionnaire's .q-toggle used to be listed here too; it became a
   Yes/No radio pair on 2026-08-15 (a switch cannot draw "not answered"),
   so that selector is gone rather than left to rot. */
.notif-switch input[type="checkbox"],
.agency-switch input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 0;
  height: 0;
  min-width: 0;
  min-height: 0;
  opacity: 0;
  border: 0;
  background: none;
  box-shadow: none;
}
.notif-switch input[type="checkbox"]::before,
.agency-switch input[type="checkbox"]::before { content: none; }

/* ── Range sliders ───────────────────────────────────────────────────────
   Fifteen native ranges rendered as OS chrome. The thumb here is the same
   object as the segmented control's — an elevated cap riding in a recessed
   rail — so "a thing you drag" means one shape across the app.

   The 320px cap is the audit's: a 0–1 value stretched across a 900px column
   makes every step feel identical and puts the thumb a screen away from the
   number it changes. */
.form-range {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  max-width: 320px;
  /* The visible rail is 4px; the control is 20px so the drag target is not
     a hairline. Vertical padding is on the wrapper, not here. */
  height: 20px;
  margin-top: var(--space-2);
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* The filled portion is driven by --range-p (0..1), written by
   enhanceRangeFields(). A slider that never got enhanced falls back to
   0 and renders as a plain rail rather than breaking — the fill is an
   enhancement, not a dependency. Firefox uses ::-moz-range-progress and
   ignores the gradient. */
.form-range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--radius-full);
  background: linear-gradient(to right,
    var(--accent) 0 calc(var(--range-p, 0) * 100%),
    var(--border-strong) calc(var(--range-p, 0) * 100%) 100%);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.22);
}
.form-range::-moz-range-track {
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--border-strong);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.22);
}
.form-range::-moz-range-progress {
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--accent);
}
.form-range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-full);
  /* Opaque base UNDER the segmented control's highlight gradient. The
     segmented thumb can be translucent because it rides in a lit trough;
     this one sits on the page, where 11%-white reads as a smudge. */
  background-color: var(--bg-elevated);
  background-image: var(--seg-thumb-bg);
  box-shadow: var(--seg-thumb-shadow, var(--shadow-sm));
  border: 0.5px solid var(--border-strong);
  /* (track 4 − thumb 16) / 2 centres the cap on the rail. */
  margin-top: -6px;
  transition: transform var(--motion-press) var(--ease-out);
}
.form-range::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: var(--radius-full);
  background-color: var(--bg-elevated);
  background-image: var(--seg-thumb-bg);
  box-shadow: var(--seg-thumb-shadow, var(--shadow-sm));
  border: 0.5px solid var(--border-strong);
  transition: transform var(--motion-press) var(--ease-out);
}
.form-range:active::-webkit-slider-thumb { transform: scale(1.12); }
.form-range:active::-moz-range-thumb { transform: scale(1.12); }
.form-range:focus-visible {
  outline: none;
}
.form-range:focus-visible::-webkit-slider-thumb {
  box-shadow: var(--seg-thumb-shadow, var(--shadow-sm)), 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent);
}
.form-range:focus-visible::-moz-range-thumb {
  box-shadow: var(--seg-thumb-shadow, var(--shadow-sm)), 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent);
}
.form-range:disabled {
  cursor: not-allowed;
}
.form-range:disabled::-webkit-slider-track,
.form-range:disabled::-webkit-slider-runnable-track {
  background: var(--border-subtle);
}
.form-range:disabled::-webkit-slider-thumb {
  background-color: var(--bg-card-subtle);
  background-image: none;
  border-color: var(--border-subtle);
  box-shadow: none;
}
.form-range:disabled::-moz-range-thumb {
  background-color: var(--bg-card-subtle);
  background-image: none;
  border-color: var(--border-subtle);
  box-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
  .form-range:active::-webkit-slider-thumb { transform: none; }
  .form-range:active::-moz-range-thumb { transform: none; }
}

/* Slider + its scale + a typed value, as one field. Built by
   enhanceRangeFields() in js/admin-agent-edit.js so all fifteen ranges get
   it from one place rather than fifteen call sites. */
.range-field {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
.range-field .form-range {
  margin-top: 0;
  flex: 1 1 auto;
  min-width: 0;
}
.range-field-scale {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 320px;
}
.range-field-rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 320px;
}
.range-field-num {
  width: 88px;
  flex: none;
  min-height: 32px;
  padding: var(--space-1) var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--text-primary);
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.range-field-num:focus {
  border-color: var(--border-focus);
}
.range-field-num:disabled {
  color: var(--text-muted);
  background: var(--bg-card-subtle);
  cursor: not-allowed;
}

/* Two consumers were getting their spacing from the native checkbox's own
   3px/4px margin, which the custom control zeroes. Neither is a bug the
   custom control introduced so much as one it stopped hiding:
   .w3-day-pill has no rule at all, and `.form-label.flex.gap-sm` never
   became a flex container because .form-label's `display: block` (0,1,0)
   loads after the .flex utility and wins the tie. */
.w3-day-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.form-label.flex { display: flex; }

/* ── Checkbox / radio label rows ─────────────────────────────────────────
   The label IS the hit target, so it carries the 40px floor. No horizontal
   padding: the control has to stay flush with the section's left edge or
   every checked row reads as indented. */
.checkbox-label,
.radio-label,
.toggle-label {
  min-height: 40px;
}

.form-error {
  font-size: var(--text-body);
  color: var(--pill-danger-fg);
  margin-top: var(--space-1);
}

.form-hint {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: var(--space-1);
}

/* form-hint variant: inline icon + text, nested under a section title.
   Replaces the long inline style="margin-top: -4px; margin-bottom:
   var(--space-3); display: flex; align-items: center; gap: 6px;
   color: var(--text-muted)" pattern that was peppered through
   admin-agent-edit.js (Batch A token cleanup, 2026-05-08). */
.form-hint-with-icon {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  /* -4px to nest visually under a section title without an extra row */
  margin-top: calc(var(--space-1) * -1);
  margin-bottom: var(--space-3);
  color: var(--text-muted);
}
.form-hint-with-icon i,
.form-hint-with-icon svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* Typed-token gate (AB-1, 2026-05-08).
   Destructive multi-agent operations (sync, rollback discard) require
   the user to type a literal token before the confirm button enables.
   Stripe-dashboard / Linear convention. The gate sits inside the modal
   body above the footer; the confirm button is disabled until the
   input value matches the expected token exactly (case-sensitive). */
.typed-token-gate {
  margin-top: var(--space-3);
  padding: var(--space-3);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.typed-token-gate .form-label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.typed-token-gate code {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  background: var(--bg-card-solid);
  padding: 1px 6px;
  border-radius: 4px;
  color: var(--accent);
  font-weight: 600;
}
.typed-token-gate__hint {
  margin-top: var(--space-1);
  font-size: var(--text-caption);
  color: var(--text-muted);
}

/* Section divider for grouping related form-groups inside a modal or panel.
   Renders as a small uppercase label with an inline horizontal rule
   extending to the right. Used to demarcate visually-distinct concerns
   (e.g., "Metadata" vs "Indexing") inside a single-column form. */
.form-section-divider {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--text-overline);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  font-weight: 600;
}
.form-section-divider:first-child {
  margin-top: 0;
}
.form-section-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* Touch-friendly form inputs on mobile */
@media (max-width: 767px) {
  .form-input,
  .form-select,
  .form-textarea {
    min-height: 44px;
    font-size: 1rem; /* Prevents iOS zoom */
  }
}

/* ── Modals ──────────────────────────────── */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: var(--glass-backdrop-bg);
  backdrop-filter: var(--glass-backdrop-filter);
  -webkit-backdrop-filter: var(--glass-backdrop-filter);
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  animation: fadeUp var(--motion-layout) var(--ease-out);
}

.modal {
  background: var(--bg-card-solid);
  border: none;
  box-shadow: var(--ring-shadow), var(--shadow-xl);
  border-radius: var(--radius-xl);
  width: 100%;
  max-width: 480px;
  max-height: 90vh;
  overflow-y: auto;
  /* Overlay pop-in grammar (unified): scale-in overlays settle with the
     soft spring; exits keep --ease-in (see .modal-overlay.is-closing). */
  animation: modalEnter var(--motion-dialog-enter) var(--ease-spring-soft);
}

@keyframes modalEnter {
  from {
    opacity: 0;
    transform: scale(0.95) translateY(8px);
    filter: blur(4px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
    filter: blur(0px);
  }
}

/* v55. Modal exit animation. Previously modals vanished via display:none
   (the "element just vanishes" antipattern per make-interfaces-feel-better).
   Now a subtle, shorter-than-enter exit: -12px translateY + opacity 0 +
   4px blur, 150ms easeIn. Applied by JS adding .is-closing, waiting for
   animationend (or a 160ms timer), then unmounting. The `forwards` fill
   keeps the final state during the brief window before unmount. */
.modal-overlay.is-closing {
  animation: modalOverlayExit var(--motion-dialog-exit) var(--ease-in) forwards;
}
.modal-overlay.is-closing .modal {
  animation: modalExit var(--motion-dialog-exit) var(--ease-in) forwards;
}
@keyframes modalOverlayExit {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes modalExit {
  from { opacity: 1; transform: translateY(0);     filter: blur(0);   }
  to   { opacity: 0; transform: translateY(-12px); filter: blur(4px); }
}
@media (prefers-reduced-motion: reduce) {
  .modal-overlay.is-closing,
  .modal-overlay.is-closing .modal {
    animation: none;
    opacity: 0;
  }
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-5) var(--space-6);
  border-bottom: 1px solid var(--border);
}
.modal-header h3 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  /* Titles carry user-typed content (a 60-char folder name). Without these the
     h3's min-content width pushes the close button past the sheet edge, where
     the mobile `overflow: hidden` clips it away entirely. `anywhere` (not
     `break-word`) is what actually reduces the intrinsic min-content size. */
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Pack 3.4. shared close-button utility. 44px tap target, subtle hover,
   focus-visible ring. Used by .modal-close, notification close, and any
   future dismissable surface so sizes and states stay consistent. */
/* .modal-close close-button utility (44px tap target, subtle hover, focus
   ring). The former co-declared .icon-close-btn fork was retired — its render
   sites now emit the canonical .btn.btn-ghost.btn-icon. */
.modal-close {
  color: var(--text-muted);
  padding: var(--space-1);
  border: 0;
  background: transparent;
  border-radius: var(--radius-sm);
  transition: background var(--motion-hover) var(--ease-out), color var(--motion-hover) var(--ease-out);
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.modal-close:hover {
  background: var(--bg-elevated);
  color: var(--text-primary);
}
.modal-close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.modal-body { padding: var(--space-6); }

/* ── Agent editor (admin-agent-edit.html) ────────────────────────────────
   Tabbed editor for a single agent: header with editable name + status,
   segmented tab nav, scrolling tab panels, sticky bottom action bar with
   the autosave chip. */

/* THE ACTION BAR HAS TO REACH THE FLOOR.
   `position: sticky; bottom: 0` does nothing on a page that doesn't
   scroll — sticky only pulls an element toward an edge it would otherwise
   scroll past. On the short tabs (LLM, Voice) the document is exactly one
   viewport tall, so the save bar came to rest wherever the content ended:
   measured at y=526 on a 900px viewport, floating in the middle of an
   empty page with nothing under it.

   Making the editor's own shell a column and letting the tab content
   absorb the slack puts the bar at the bottom of a >=100vh #content, which
   is where a save bar belongs, without a magic min-height that guesses at
   the height of the chrome above it. Scoped by :has() to the one page that
   has this bar — no other surface changes. */
#content:has(> .agent-edit-actionbar--sticky) {
  display: flex;
  flex-direction: column;
}
#content:has(> .agent-edit-actionbar--sticky) > .agent-edit-tab-content {
  flex: 1 1 auto;
}
/* Stretch is the flex default and would blow an inline-flex link out to
   the full column width, taking its hover area with it. */
#content:has(> .agent-edit-actionbar--sticky) > .back-link {
  align-self: flex-start;
}

.agent-edit-header {
  margin-bottom: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.agent-edit-title-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
/* The agent's name is editable and nothing said so: a transparent border
   on a transparent field reads as a heading, and the 320px floor left a
   short name trailed by an empty box that looked like a rendering bug.
   The field now sizes to its content and states the affordance on hover —
   a visible edge plus a pencil in the gutter — while resting as a plain
   heading. */
.agent-edit-name-field {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: 0 1 auto;
  min-width: 0;
}
.agent-edit-name {
  font-family: var(--font-display);
  font-size: var(--text-h1);
  font-weight: 600;
  color: var(--text-primary);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-md, 8px);
  padding: var(--space-2) var(--space-3);
  /* Content-width via the `size` attribute the field now carries; the
     floor only stops a one-character name collapsing. */
  min-width: 8ch;
  max-width: 100%;
  flex: 0 1 auto;
  text-overflow: ellipsis;
  transition-property: background-color, border-color;
  transition-duration: var(--motion-hover);
  transition-timing-function: var(--ease-out);
}
.agent-edit-name:hover {
  background: var(--bg-card-hover);
  border-color: var(--border);
}
.agent-edit-name-pencil {
  position: absolute;
  right: calc(var(--space-2) * -1);
  width: 14px;
  height: 14px;
  color: var(--text-muted);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-hover) var(--ease-out);
}
.agent-edit-name-field:hover .agent-edit-name-pencil,
.agent-edit-name:focus + .agent-edit-name-pencil {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .agent-edit-name,
  .agent-edit-name-pencil { transition: none; }
}
/* No `outline: none` here — see the .form-input note above. At (0,2,0) it
   outranks `input:focus-visible` (0,1,1) and deletes the keyboard ring; the
   global `*:focus { outline: none }` already suppresses the mouse case and
   correctly loses to focus-visible. */
.agent-edit-name:focus {
  border-color: var(--border-focus);
  background: var(--bg-input);
}
.agent-edit-meta {
  color: var(--text-muted);
  font-size: var(--text-sm);
  padding-left: var(--space-3);
}
.agent-edit-meta strong {
  color: var(--text-secondary);
  font-weight: 500;
}

/* .agent-version-badge retired — version rows render Draft/Published/Live
   through the canonical .status-pill. */

.agent-edit-unsaved {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-caption);
  color: var(--pill-warning-fg);
  font-weight: 500;
}
.agent-edit-unsaved i,
.agent-edit-unsaved svg { width: 12px; height: 12px; }

.agent-edit-tabs {
  overflow-x: auto;
  /* allow horizontal scroll if 9 tabs don't fit */
  /* overflow-x: auto computes overflow-y to auto as well, so the strip
     clipped the selected thumb's outer shadow at its 3px padding and the
     thumb read flat here alone (2026-08-07 — the old L-3 fork set
     box-shadow: none, so there was nothing to clip). 8px clears it; the
     matching negative margin keeps the strip's spacing where it was. */
  padding-bottom: 8px;
  margin-bottom: calc(var(--space-5) - 8px);
  scrollbar-width: none;
  scroll-behavior: smooth;
  /* The strip is a scroller with eleven tabs and the scrollbar hidden, so
     nothing said there was more. The mask fades the segments themselves at
     whichever end still has tabs behind it — classes are toggled in
     admin-agent-edit.html, so a strip that fits is never masked and a
     strip scrolled to its end stops advertising an end that isn't there. */
  -webkit-mask-image: var(--tabs-mask, none);
  mask-image: var(--tabs-mask, none);
}
.agent-edit-tabs::-webkit-scrollbar { display: none; }
.agent-edit-tabs.can-scroll-start {
  --tabs-mask: linear-gradient(to right, transparent 0, #000 18px, #000 100%);
}
.agent-edit-tabs.can-scroll-end {
  --tabs-mask: linear-gradient(to right, #000 0, #000 calc(100% - 18px), transparent 100%);
}
.agent-edit-tabs.can-scroll-start.can-scroll-end {
  --tabs-mask: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
}
@media (prefers-reduced-motion: reduce) {
  .agent-edit-tabs { scroll-behavior: auto; }
}

/* A tab whose slice of the draft differs from the published version.
   Toggled by refreshTabDots in admin-agent-edit.html. The dot sits inline
   after the label, centred on it, rather than pinned to a corner; in the
   accent hue because it says "something is here", not "something is
   wrong". The 1px ring in the card surface keeps it off the active thumb's
   fill and off the accent-coloured active label. */
.agent-edit-tabs .seg-btn.has-unpublished::after {
  content: '';
  flex: none;
  width: 5px;
  height: 5px;
  margin-left: var(--space-1);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 1px var(--bg-card);
}

/* Disabled tabs read through tokens. The old `opacity: .4` took the
   label to roughly 1.7:1 — invisible rather than unavailable.
   [disabled] segments are handled app-wide now (search `.seg-btn[disabled]`);
   this one keeps the --soon variant, which is a class, not a state. */
.agent-edit-tab--soon {
  color: var(--fg-disabled);
  cursor: not-allowed;
}

.agent-edit-tab-panel:focus { outline: none; }

/* Boot skeleton — the shape of THIS page, not a generic card.
   It was a title bar and two text lines, so the first paint promised a
   breadcrumb and delivered an eleven-tab editor; everything jumped when
   the real content landed. Reserving the status pills and the tab strip
   costs four rules and removes the jump. */
.agent-edit-boot {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.agent-edit-boot-crumbs { height: 14px; width: 260px; }
.agent-edit-boot-title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.agent-edit-boot-name { height: 34px; width: 280px; border-radius: var(--radius-md); }
.agent-edit-boot-pill { height: 22px; width: 88px; border-radius: var(--radius-full); }
.agent-edit-boot-tabs {
  height: 36px;
  width: 100%;
  max-width: 780px;
  border-radius: var(--radius-md);
}
.agent-edit-boot-card { margin-bottom: 0; }
.agent-edit-boot-heading {
  height: 18px;
  width: 180px;
  margin-bottom: var(--space-4);
}

/* Inline notice — a fact the operator needs before touching the controls
   under it. Info-toned, not a warning: nothing is broken, the tab just
   isn't the whole story. */
.agent-edit-notice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--pill-info-border);
  border-radius: var(--radius-md);
  background: var(--pill-info-bg);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.5;
}
.agent-edit-notice[hidden] { display: none; }
.agent-edit-notice i,
.agent-edit-notice svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  margin-top: 1px;
  color: var(--pill-info-fg);
}
.agent-edit-notice strong { color: var(--text-primary); font-weight: 600; }
.agent-edit-notice code {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-primary);
}

.agent-edit-tab-content {
  /* Leave space at the bottom for the sticky action bar */
  padding-bottom: var(--space-10);
}
.agent-edit-tab-panel { /* default. visible */ }
.agent-edit-tab-panel[hidden] { display: none; }
.agent-edit-tab-panel--placeholder {
  background: var(--bg-card-subtle);
  border: 1px dashed var(--border);
  border-radius: 12px;
  padding: var(--space-8);
}

.agent-edit-section {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: var(--space-5) var(--space-6);
  margin-bottom: var(--space-4);
}
.agent-edit-section-title {
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}

/* Section-head with a right-aligned action button (e.g. "Test voice") */
.agent-edit-section-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
  gap: var(--space-3);
}
.agent-edit-section-head .agent-edit-section-title {
  margin: 0;
  padding: 0;
  border: 0;
}

/* Collapsible advanced expander (uses native <details>) */
.advanced-expander {
  margin-top: var(--space-4);
  padding: var(--space-3);
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.advanced-expander > summary {
  cursor: pointer;
  font-weight: 500;
  color: var(--text-secondary);
  padding: var(--space-1) 0;
  font-size: var(--text-sm);
  user-select: none;
}
.advanced-expander > summary:hover {
  color: var(--text-primary);
}
.advanced-expander[open] > summary {
  margin-bottom: var(--space-3);
  color: var(--text-primary);
}
.advanced-expander .advanced-body { /* container for the rendered fields */ }

/* Small utility to hide branches that toggle on provider switches */
.is-hidden { display: none !important; }

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}
.form-row .form-group { margin-bottom: 0; }

.form-label-value {
  margin-left: var(--space-2);
  color: var(--text-muted);
  font-weight: 400;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
}
.form-hint-inline {
  margin-left: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-caption);
}

/* .form-range's geometry now lives with the other custom controls in the
   Forms section above — `accent-color` was the last trace of OS chrome. */

.toggle-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  user-select: none;
  font-weight: 500;
  color: var(--text-primary);
}
.radio-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.radio-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  user-select: none;
}
.radio-label > span:first-of-type {
  font-weight: 500;
  color: var(--text-primary);
  margin-right: var(--space-2);
}

.checkbox-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
}
.checkbox-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  user-select: none;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* ── Sticky bottom action bar ────────────────────────────────────────── */
.agent-edit-actionbar {
  /* Named, because the two numbers this replaced (5 here, 60 on the sticky
     variant below) were both invented locally and one of them sat between
     --z-rail and --z-fab for no reason. The bar is page chrome: above the
     content it pins over, below every menu and dialog realm. */
  --z-editor-actionbar: 20;
  position: sticky;
  bottom: 0;
  z-index: var(--z-editor-actionbar);
  /* Was `var(--bg-card)` — fully opaque — under a blur(8px), so the blur
     had nothing behind it to work on and only bought a compositing layer.
     Either the surface is translucent or the filter is a lie; this is a
     sticky bar with page content running underneath it, which is the one
     place the material genuinely applies. */
  background: rgba(var(--bg-card-rgb), 0.80);
  border-top: 1px solid var(--border-subtle);
  padding: var(--space-3) var(--space-6);
  margin: 0 calc(-1 * var(--space-6));
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-4);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
}
@media (prefers-reduced-transparency: reduce) {
  .agent-edit-actionbar {
    background: var(--bg-card);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}
.actionbar-left, .actionbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.autosave-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.autosave-chip i,
.autosave-chip svg { width: 14px; height: 14px; }
.autosave-chip.is-saving { color: var(--pill-info-fg); }
.autosave-chip.is-saving i,
.autosave-chip.is-saving svg { animation: spin 1s linear infinite; }
.autosave-chip.is-error { color: var(--pill-danger-fg); }
.autosave-chip.is-offline { color: var(--pill-warning-fg); }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* One save, one sweep. The agent editor's tab panels used to fade on
   every click, decorating a pure disclosure; that is gone and the motion
   budget is spent here instead, where "saved" was otherwise the same
   pixels as the page at rest. admin-agent-edit.html adds .is-just-saved
   from setChip('saved') only, which the engine emits after a real write,
   never from the idle paint on load. The line sweeps in and fades out so
   the resting chip stays plain. */
.autosave-chip { position: relative; }
.autosave-chip::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: var(--success);
  transform: scaleX(0);
  transform-origin: left;
  opacity: 0;
  pointer-events: none;
}
.autosave-chip.is-just-saved::after {
  animation: autosaveSweep 400ms var(--ease-out) forwards;
}
@keyframes autosaveSweep {
  0%   { transform: scaleX(0); opacity: 1; }
  60%  { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .autosave-chip.is-just-saved::after { animation: none; }
}
.autosave-meta {
  font-size: var(--text-caption);
  color: var(--text-muted);
}
/* "Draft saved · v61 live": two facts, not one sentence. The draft state
   keeps the chip's quiet weight; the live version is the fact that
   matters on this page, so it carries the primary colour. */
.autosave-chip-draft { color: var(--text-secondary); }
.autosave-chip-sep { color: var(--text-muted); }
.autosave-chip-live { color: var(--text-primary); font-weight: 500; }

/* ── Desktop-only notice ──────────────────────────────────────────────────
   Used by admin-agents.html / admin-agent-edit.html when viewport is too
   narrow. Replaces page chrome wholesale. */
.desktop-only-notice {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-8);
  max-width: 640px;
  margin: 0 auto;
  color: var(--text-primary);
}
.desktop-only-notice h1 {
  font-size: var(--text-h1);
  font-weight: 600;
  margin: 0 0 var(--space-3);
}
.desktop-only-notice p {
  color: var(--text-muted);
  margin: 0 0 var(--space-6);
  line-height: 1.5;
}
.desktop-only-notice a {
  color: var(--accent);
  text-decoration: none;
  font-weight: 500;
}
.desktop-only-notice a:hover { text-decoration: underline; }

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-6);
  border-top: 1px solid var(--border);
}

/* ── Mobile: modals present as bottom sheets ──────────────────────────────
   Rebuilt 2026-07-25 after Roman hit it on a phone: sheets opened with the
   header ~40% down the screen and the bottom (including the action buttons)
   cut off. Three compounding causes, all fixed here:

   1. VIEWPORT UNIT. `inset: 0` + `90vh` both resolve against the LARGE
      viewport (URL bar hidden). With the URL bar showing, the overlay's
      bottom edge — and therefore the bottom-anchored sheet — sits below the
      visible area, so the sheet hangs off the bottom and its top is pushed
      down. Pinning height/max-height to `dvh` sizes it to what is actually
      on screen. `vh` is kept as the preceding fallback declaration.
   2. SPECIFICITY. `.modal.modal-lg { max-width: 720px; width: 90% }`
      (components.css, further down) is (0,2,0) and sits outside any media
      query, so it beat this block's (0,1,0) `.modal` width rules — modal-lg
      sheets (the Clients Feedback + Message-all composers) never went
      full-width and rendered as an inset slab with side gutters. Restated
      at matching specificity below.
   3. SCROLL CONTAINER. The whole `.modal` scrolled, so a tall body pushed
      the footer past the fold with no way to reach it. The body now owns
      the scroll and the header/footer stay pinned — that is what makes the
      sheet feel native.

   Scoped to modals that actually have a `.modal-body` (`:has`) because a
   handful of hand-rolled modals put content straight in `.modal`; those keep
   the old whole-sheet scroll rather than being clipped by `overflow: hidden`.

   ── 2026-07-26: FULL-HEIGHT TALL SHEETS ─────────────────────────────────
   Roman: the Clients Feedback sheet opened roughly half height on his phone.
   Content-sized (`height: auto`, capped at 90dvh) is right for a two-button
   confirm and wrong for a scrolling feed, so height is now split by class,
   NOT applied to every modal:

     .modal.modal-lg  → full height. The four users are all tall content
                        sheets: Clients ▸ Feedback, Message-all (broadcast),
                        Agent ▸ tool editor, Agent ▸ extractor editor.
     .modal (plain)   → unchanged. Content-sized, capped at 90dvh, so a
                        confirm dialog never stretches to the whole screen.

   FULL means iOS full-sheet, not full-bleed: the sheet stops one
   --sheet-top-inset short of the top so the rounded top corners, the drop
   shadow and a sliver of backdrop stay visible (that sliver is also the
   remaining backdrop-dismiss target — the header ✕ and Escape are unchanged).
   On a notched device in standalone the inset becomes the status-bar safe
   area, which is where an iOS sheet stops anyway.

   KEYBOARD: --kb-inset is published on :root by the visual-viewport bridge in
   js/support-chat-ui.js while a field inside a sheet is focused — the height
   the soft keyboard is covering, nothing else. A soft keyboard shrinks the
   visual viewport but not 100dvh, so without it the pinned footer sits behind
   the keys. Absent the var (desktop, no focus, no visualViewport support) it
   resolves to 0 and the layout is exactly the no-keyboard one. */
@media (max-width: 767px) {
  :root {
    --sheet-vh: 100vh;
    /* iOS page-sheet gap. env() is 0 in a browser tab (the URL bar already
       owns that strip) and the status-bar inset in an installed PWA. */
    --sheet-top-inset: max(env(safe-area-inset-top, 0px), 12px);
  }
  @supports (height: 100dvh) {
    :root { --sheet-vh: 100dvh; }     /* the VISIBLE viewport, URL bar included */
  }

  .modal-overlay {
    padding: 0;
    align-items: flex-end;          /* explicit bottom anchor on the container */
    z-index: var(--z-panel);
    /* The overlay IS the backdrop, so it is sized by the LARGE viewport and
       never by the visual one. Sizing a translucent layer against a viewport
       that shrinks under the keyboard leaves the region behind the keys
       uncovered — the page shows through the moment anything there is
       visible. The keyboard instead eats PADDING, which lifts the bottom-
       anchored sheet clear of the keys while the backdrop still covers the
       whole screen. (Same correction as .panel-sheet--chat in css/chat.css.) */
    top: 0;
    height: var(--sheet-vh);
    padding-bottom: var(--kb-inset, 0px);
    overscroll-behavior: contain;
  }
  .modal {
    align-self: flex-end;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    max-height: 90vh;
    max-height: 90dvh;
    height: auto;
    max-width: 100%;
    width: 100%;
    padding-bottom: max(var(--space-6), env(safe-area-inset-bottom));
    animation: modalSlideUp var(--motion-dialog-enter) var(--ease-out);
  }
  /* `.modal.modal-lg { max-width: 720px; width: 90% }` further down the file
     is (0,2,0). Listing `.modal.modal-lg` alongside `.modal` above would only
     TIE it — a selector list takes the specificity of its most specific
     matching selector, and ties go to source order, which that later rule
     wins. Adding the overlay ancestor makes this (0,3,0) so the sheet is
     genuinely full-bleed. (Measured: without this, sheets rendered 351px
     wide on a 390px screen.) */
  .modal-overlay .modal.modal-lg {
    max-width: 100%;
    width: 100%;
    /* The tall sheets, and only these. The overlay spans the whole screen, so
       the sheet subtracts the top gap and whatever the keyboard is covering —
       leaving the backdrop, not a hole, behind the keys. */
    height: calc(var(--sheet-vh) - var(--sheet-top-inset) - var(--kb-inset, 0px));
    max-height: none;
  }
  /* Header pinned, body scrolls, footer always reachable. */
  .modal:has(> .modal-body) {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding-bottom: 0;              /* the footer/body carries the safe area now */
  }
  .modal:has(> .modal-body) > .modal-header,
  .modal:has(> .modal-body) > .modal-footer { flex-shrink: 0; }
  .modal:has(> .modal-body) > .modal-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  /* Safe area lands on whichever element is last in the sheet. */
  .modal:has(> .modal-body):not(:has(> .modal-footer)) > .modal-body {
    padding-bottom: max(var(--space-6), env(safe-area-inset-bottom));
  }
  .modal:has(> .modal-body) > .modal-footer {
    padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
  }
}

@keyframes modalSlideUp {
  from {
    opacity: 0;
    transform: translateY(100%);
    filter: blur(2px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0px);
  }
}

/* ── Pagination ──────────────────────────── */
.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) 0;
  font-family: var(--font-body);
  font-size: var(--text-body);
  color: var(--text-secondary);
  gap: var(--space-3);
}

.pagination-info {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

.pagination-buttons {
  display: flex;
  gap: var(--space-1);
}

@media (max-width: 767px) {
  .pagination {
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
  }
  .pagination-buttons .btn {
    min-height: 44px;
    min-width: 44px;
  }
}

/* ── Filter Bar ──────────────────────────── */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
  margin-bottom: var(--space-5);
}

.filter-bar .search-box,
.filter-bar .search-wrapper {
  flex: 1;
  min-width: 280px;
  align-self: flex-end;
}

.filter-bar > .form-select,
.filter-bar > .form-input {
  width: auto;
  min-width: 140px;
  min-height: 36px;
}

.filter-bar .search-wrapper .form-input,
.filter-bar .search-wrapper .search-input {
  width: 100%;
  min-width: 0;
}

/* Labeled filter groups */
.filter-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 140px;
}

/* B4 — two "small label" systems lived eighty pixels apart: these filter
   labels at 11/500/+0.02em sentence case, and the column headers directly
   below them at 11/600/+0.04em uppercase. Same role, same size, two
   different treatments. The table header's is the one that reads as a
   label rather than as small text, so it wins for both. */
.filter-label {
  font-size: var(--text-caption);
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.filter-group .form-select {
  width: 100%;
  min-height: 36px;
}

/* Calls-page filter bar layout.
   - >= 1440px: single row, comfortable spacing.
   - 768-1439px: two rows (search alone on top, period + filters below).
     Avoids the "everything pinched into one line" look at typical laptop
     widths (1280-1440).
   - < 768px: stacked, handled by the .filter-bar mobile rules below. */
@media (min-width: 768px) {
  #admin-filter-bar {
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-3);
    row-gap: var(--space-3);
    /* Centre, not flex-end. Every control on this row is 36px now, so
       baseline-ish alignment has nothing left to do and centring keeps the
       chip lane from hanging off the bottom of the row. */
    align-items: center;
  }

  #admin-filter-bar .search-wrapper {
    flex: 1 1 100%;
    min-width: 0;
    max-width: none;
  }

  #admin-filter-bar .period-toggle {
    flex: 0 1 auto;
    min-width: 0;
  }

  /* The three `#admin-filter-bar .filter-group*` rules that used to sit here
     were deleted 2026-08-08, and they are why the popover had voids in it.
     They sized a filter group as a flex ITEM in a horizontal toolbar
     (`flex: 0 1 130px`, first-of-type `160px` + `margin-left: auto`) — which
     was right when the cluster was docked in the bar. It no longer is: since
     the one-control refactor, `place()` moves .filter-cluster into
     #filter-sheet-body unconditionally, at every width, and #filter-sheet-body
     is a DESCENDANT of #admin-filter-bar. So these rules never stopped
     matching, and at (1,1,0) they outranked `.filter-panel__body .filter-group`
     at (0,2,0).
     Inside the panel the cluster is a COLUMN, where flex-basis is the main
     axis — height. Every filter row was therefore 130px tall (160px for
     Client) around ~53px of label and select, which is the "huge vertical
     padding" the founder measured. The rules were orphaned by the refactor
     that moved the cluster; deleting them is the fix, not scoping them.

     Comfortable button padding at all desktop widths. The previous tight
     `var(--space-1) var(--space-2)` made the period toggle look pinched on
     small laptops. */
  /* Padding only. The 36px min-height that used to live here fought the
     trough and produced the 44px control the audit measured. */
  #admin-filter-bar .seg-btn,
  #admin-filter-bar .segmented-btn {
    padding: 6px var(--space-3);
  }

  /* Spacer pushes the right-side filters away from the period toggle so
     the row reads as two groups, not one long mash. */
  #admin-filter-bar .filter-bar-spacer {
    flex: 1 1 auto;
    min-width: 0;
  }
}

/* Wide desktops: keep search on its own row but give it some breathing
   room (centered max-width feel). */
@media (min-width: 1440px) {
  #admin-filter-bar .search-wrapper {
    flex: 1 1 100%;
    max-width: none;
  }
}

@media (min-width: 768px) and (max-width: 1099px) {
  #admin-filter-bar .seg-label-full {
    display: none;
  }

  #admin-filter-bar .seg-label-short {
    display: inline;
  }
  /* The two filter-group flex-basis overrides that lived here are gone for
     the reason given at the ≥768 block above: the cluster is inside the
     popover now, where a basis is a row height. */
}

@media (max-width: 767px) {
  .filter-group {
    width: 100%;
  }
  .filter-group .form-select {
    min-height: 42px;
    font-size: 1rem;
  }
}

@media (max-width: 767px) {
  .filter-bar {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    margin-bottom: var(--space-4);
  }

  .filter-bar .search-wrapper {
    min-width: unset;
  }

  /* The bar is a COLUMN here, so flex-basis is the block axis: any page that
     caps its search width with `flex: 0 1 <px>` for desktop was setting a
     HEIGHT the moment it got here. Neutralising the basis and the cap in the
     shared rule means a page-level width cap can only ever be a width. */
  .filter-bar .search-box,
  .filter-bar .search-wrapper {
    width: 100%;
    flex: 0 0 auto;
    max-width: none;
  }

  .filter-bar > .form-select {
    width: 100%;
    min-height: 42px;
  }

  .filter-bar > .form-input {
    width: 100%;
    min-height: 42px;
  }

  .filter-bar .seg-control,
  .filter-bar .segmented,
  .filter-bar .segmented-control {
    width: 100%;
    display: flex;
  }

  .filter-bar .seg-control .seg-btn,
  .filter-bar .segmented .seg-btn,
  .filter-bar .segmented-control .seg-btn,
  .filter-bar .segmented .segmented-btn,
  .filter-bar .segmented-control .segmented-btn {
    flex: 1;
    text-align: center;
    min-height: 38px;
    font-size: var(--text-sm);
  }

  /* ── Admin calls toolbar: one row, not four (owner report 2026-08-09) ──
     The shared .filter-bar column above put every control on a line of its
     own, so admin-calls opened with title, a lone Export icon, a full-width
     search, the range control and a lone Filters button stacked before the
     first call row — most of a phone screen spent on chrome.

     Grid rather than wrap, because the order the eye wants is not the order
     the DOM has: search / Filters / Export share the top line and the range
     control keeps a full-width line of its own under them. Named areas say
     that outright instead of leaving it to source order plus a wrap point.
     Scoped by id so no other page's .filter-bar is touched. */
  #admin-filter-bar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-areas:
      "search filters export"
      "period period period";
    align-items: center;
    gap: 8px;
  }
  #admin-filter-bar .search-wrapper { grid-area: search; width: auto; min-width: 0; }
  #admin-filter-bar .filter-trigger-wrap { grid-area: filters; }
  #admin-filter-bar .filter-bar__end { grid-area: export; }
  #admin-filter-bar .segmented-control { grid-area: period; }
  /* The cluster is relocated into the filter panel by place() on init. Until
     that runs it is still a child here, and auto-placement would give it a
     third row for one paint. It has nothing to show in the bar either way. */
  #admin-filter-bar > .filter-cluster { display: none; }

  /* One tappable height across the row. 44 is the floor, and the three
     controls have to agree on it or the row reads as ragged. */
  #admin-filter-bar .search-wrapper .search-input,
  #admin-filter-bar .filter-trigger,
  #admin-filter-bar .filter-bar__end .btn {
    min-height: 44px;
  }
  /* Export gives up its word, not its meaning: the caret goes with the label
     and aria-label carries the name for anyone not reading the icon. A square
     44 keeps the tap target honest. */
  #admin-filter-bar .filter-bar__end .btn > span,
  #admin-filter-bar .filter-bar__end .btn .download-caret {
    display: none;
  }
  #admin-filter-bar .filter-bar__end .btn {
    min-width: 44px;
    padding-left: 0;
    padding-right: 0;
    justify-content: center;
  }

  /* WHILE TYPING, the field takes the word back off Filters. At 360 the
     resting field is ~150px, which is fine for the prompt and thin for
     reading a query back; dropping the trigger's label returns ~60px of it.
     Filters and Export deliberately stay in the row rather than sliding out:
     a control that vanishes on focus cannot be tapped, because the tap that
     would reach it is the one that blurs the field. The icon and the count
     badge stay, so the trigger keeps its meaning and its state. */
  #admin-filter-bar:focus-within .filter-trigger > span:not(.filter-trigger__count) {
    display: none;
  }
}

/* ── Calls Toolbar ───────────────────────── */
/* v55. Shadows over borders. The previous `border: 1px solid var(--border)
   + box-shadow: var(--shadow-sm)` combo renders a hard edge in dark mode
   and fights with mixed-background surfaces. Switched to the --ring-shadow
   token used on .kpi-card and .chart-card so toolbar depth matches the
   rest of the card system. Layered shadows adapt to any background since
   they use transparency; solid borders don't. */
.calls-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  margin-bottom: var(--space-5);
  background: var(--bg-card);
  /* Same fill, same radius, same shadow as the table below it — and it was
     the only one of the two without an edge. One hairline, both surfaces
     (audit A2). */
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--ring-shadow);
  transition: box-shadow 150ms ease-out;
}

.calls-toolbar-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.calls-toolbar-search {
  flex: 1;
  min-width: 280px;
}

.calls-toolbar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}

.calls-toolbar-period {
  flex: 0 1 auto;
}

.calls-toolbar-filters {
  flex: 1;
  min-width: 0;
}

.calls-toolbar .filter-chips {
  margin-bottom: 0;
}

@media (max-width: 767px) {
  .calls-toolbar {
    gap: var(--space-2);
    padding: var(--space-3);
    margin-bottom: var(--space-4);
  }

  .calls-toolbar-row {
    gap: 10px;
  }

  .calls-toolbar-search {
    min-width: 0;
    width: 100%;
  }

  /* Export button shows inline on mobile again (2026-06-12): the
     Settings popover moved to the overview header, so the calls page
     no longer has a popover to host its Download entry. */

  .calls-toolbar-period,
  .calls-toolbar-filters {
    width: 100%;
  }
}

/* ── Empty State ─────────────────────────── */
.empty-state {
  text-align: center;
  padding: var(--space-10) var(--space-8);
  color: var(--text-secondary);
}

.empty-state-icon {
  width: 56px;
  height: 56px;
  margin: 0 auto var(--space-4);
  color: var(--text-muted);
  opacity: 0.4;
  /* Center the inline Lucide svg inside the box. The icon renders class-less
     at its default 24px under the global `svg { display:block }` rule, so a
     bare block child sits flush-left — ~16px off the box's centre, leaving
     the icon misaligned with the centred empty-state text below. */
  display: flex;
  align-items: center;
  justify-content: center;
}

.empty-state h3 {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-1);
}

.empty-state p {
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: 1.5;
  max-width: 360px;
  margin: 0 auto;
  color: var(--text-muted);
}

@media (max-width: 767px) {
  .empty-state {
    padding: var(--space-8) var(--space-4);
  }
}

/* Entrance for empty states — a rare, first-impression surface that had no
   motion. Gentle rise + a soft icon spring. Caught by the global
   reduced-motion reset.

   TRANSFORM-ONLY, deliberately (same fix as global.css's contentEnter): these
   both carry `backwards` fill, so an animation that hasn't been given a start
   time yet — restored bfcache page, an inactive tab, the settling window of a
   cross-document view transition — pins the element at its from-state. With
   `opacity: 0` in the keyframe that state is INVISIBLE, which is exactly how
   admin-calls ghosted. A rise reads the same and can never gate visibility;
   the fade is supplied by the ::view-transition root cross-fade. */
.empty-state {
  animation: emptyStateEnter var(--duration-slow) var(--ease-out) backwards;
}
.empty-state .empty-state-icon {
  animation: emptyStateIconPop 360ms var(--ease-spring-out) 120ms backwards;
}
@keyframes emptyStateEnter {
  from { transform: translateY(8px); }
  to { transform: none; }
}
/* Its own copy of the pop the greeting icon used to share; opacity is NOT
   animated — the icon's resting 0.4 comes from .empty-state-icon and stays
   put the whole time (sharing one keyframe used to brighten it to full, then
   snap back to 40% the instant the animation released). */
@keyframes emptyStateIconPop {
  from { transform: scale(0.5) rotate(-15deg); }
  to { transform: scale(1) rotate(0); }
}

/* Compact: the same shape inside a section card, drawer or modal, where the
   page-level 40px inset would cost more than the content it frames. The icon
   shrinks with it so the block keeps its proportions. This is a size on the
   card shape, not a fourth vocabulary (U3 folded .empty-state-compact,
   .empty-state--compact-pad, .empty-state-icon-sm and .empty-state-title-sm
   into it; the title size was already the base h3's 1rem). */
.empty-state--compact {
  padding: var(--space-6) var(--space-4);
}
.empty-state--compact .empty-state-icon {
  width: 40px;
  height: 40px;
  margin-bottom: var(--space-2);
}

/* Pack 2.1. formalize the "actions" slot. Filtered, error, and first-run
   empty states should include one path forward (a button or link). Pure
   "no data" messages are the only exception. */
.empty-state-actions {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  margin-top: var(--space-4);
  flex-wrap: wrap;
}
.empty-state-actions > * { flex-shrink: 0; }
@media (max-width: 560px) {
  .empty-state-actions {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
  }
}

/* ── The other two empty shapes (U3) ──────────────────────────────────
   Three empty states, named for their container, and the container is the
   selection rule: "a bordered card is for content that needs containing;
   around a single muted sentence it is a box drawn for its own sake"
   (admin-clients.html, phone numbers).
     .empty-state         a card or page owns the space: icon + h3 + p + actions
     .empty-state--line   a row or section already has a frame: one muted line
     .empty-state--cell   inside a <td colspan>: the line, inset by the table
   --line and --cell are SIBLINGS of .empty-state, never stacked on it. The
   card shape carries an entrance animation, a 40px inset and container rules
   (.clients-card .empty-state, #calls-container > .empty-state) written for
   a block that holds the page; a line inside a row must inherit none of it.

   An empty state is the absence of content, so it is drawn at the weight of
   content, not as a dashed box with a fill and a 24px glyph, which is more
   chrome than the row it stands in for and makes "nothing here yet" the
   loudest thing on the tab. Borderless, centred, muted, one step under body:
   the card it sits in is already the container, and the add affordance
   beneath it is already the instruction.
   12px is deliberately between --text-sm and --text-caption: at 13px this
   reads as a row of content that is missing, at 11px it reads as a caption
   on a row that is not there. The size sits on the container so bare text
   and <p> text render the same. */
.empty-state--line,
.empty-state--cell {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: var(--space-5) var(--space-4);
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
}
/* The td already insets the row horizontally; only the vertical is ours. */
.empty-state--cell { padding: var(--space-5) 0; }
/* display:flex on a class outranks the UA's [hidden] { display:none }, so a
   toggled line (the scatter overlays) needs the attribute restated. */
.empty-state--line[hidden],
.empty-state--cell[hidden] { display: none; }
.empty-state--line i,
.empty-state--line svg { width: 15px; height: 15px; flex-shrink: 0; opacity: 0.75; }
.empty-state--line p,
.empty-state--cell p { margin: 0; }

/* ── Handled ring (design pass T3.5 — was Star Toggle) ──
   The client's done-mark: open ring = call still needs him, green drawn
   check = handled. Same 44px chassis, class names, and spring as the old
   star so every render site swaps in place. */
.star-btn {
  color: var(--text-secondary);
  transition-property: color, background, scale;
  transition-duration: var(--motion-hover);
  transition-timing-function: var(--ease-out);
  padding: var(--space-1);
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  -webkit-tap-highlight-color: transparent;
}
.star-btn:hover {
  color: var(--text-primary);
  background: var(--bg-elevated);
}
.star-btn:active { scale: 0.96; }
.star-btn .ring-svg {
  width: 20px;
  height: 20px;
  display: block;
  overflow: visible;
}
.star-btn .ring-svg circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  transition: stroke 120ms var(--ease-out), fill 120ms var(--ease-out);
}
.star-btn .ring-svg path {
  fill: none;
  stroke: var(--pill-success-fg);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 12;
  stroke-dashoffset: 12;
}
/* Check-off (VF-3): ring fills + check draws in over 200ms ease-out-quart,
   then the whole mark settles to a dimmed filled state — the 240ms delay on
   the svg's opacity lets the draw land at full strength before it recedes.
   Un-checking swaps back instantly (base .ring-svg has no opacity
   transition; undoing isn't a celebration). Hover/focus lift the dim to
   hint the undo affordance. */
.star-btn.starred .ring-svg {
  opacity: 0.7;
  transition: opacity 160ms var(--ease-out) 240ms;
}
.star-btn.starred:hover .ring-svg,
.star-btn.starred:focus-visible .ring-svg {
  opacity: 1;
  transition-delay: 0ms;
}
.star-btn.starred .ring-svg circle {
  fill: var(--success-muted);
  stroke: rgba(var(--success-rgb), 0.35);
  transition: stroke 200ms cubic-bezier(0.25, 1, 0.5, 1), fill 200ms cubic-bezier(0.25, 1, 0.5, 1);
}
/* HANDLED ≠ VIEWED (2026-08-08). Both states drew the same green check, so
   "I did the work this call asked for" and "I glanced at this call" were
   pixel-identical and only the aria-label told them apart. Green is the
   reward for clearing a follow-up; acknowledging an ordinary call gets a
   neutral mark. The follow-up flag is the discriminator the copy already
   uses (utils.js → handledVerb), so the two cannot drift. */
.star-btn.starred[data-follow-up="false"] .ring-svg circle {
  fill: color-mix(in srgb, var(--text-primary) 9%, transparent);
  stroke: color-mix(in srgb, var(--text-primary) 26%, transparent);
}
.star-btn.starred[data-follow-up="false"] .ring-svg path {
  stroke: var(--text-secondary);
}
.star-btn.starred .ring-svg path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 200ms cubic-bezier(0.25, 1, 0.5, 1);
}
.star-btn.is-toggling .ring-svg {
  animation: star-toggle-pop 200ms cubic-bezier(0.25, 1, 0.5, 1);
}

@keyframes star-toggle-pop {
  0% { transform: scale(0.9); }
  60% { transform: scale(1.06); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .star-btn .ring-svg circle,
  .star-btn .ring-svg path,
  .star-btn.starred .ring-svg,
  .star-btn.starred .ring-svg circle,
  .star-btn.starred .ring-svg path {
    transition: none;
  }
  .star-btn.is-toggling .ring-svg { animation: none; }
}

/* ── Progress Bar ────────────────────────── */
.progress-bar {
  height: 5px;
  background: var(--bg-elevated);
  border-radius: var(--radius-full);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--border-subtle);
}

.progress-fill {
  height: 100%;
  border-radius: var(--radius-full);
  transition: width 0.6s var(--ease-out);
}

/* Green uses SUCCESS (emerald), not accent (indigo) */
.progress-fill.green   { background: var(--success); }
.progress-fill.success { background: var(--success); }
.progress-fill.yellow  { background: var(--warning); }
.progress-fill.red     { background: var(--danger); }

/* ── Booked indicator (inline) ──────────── */
.booked-yes { color: var(--pill-success-fg); font-weight: 600; }
.booked-no  { color: var(--text-muted); }

/* ── Direction Icon ──────────────────────── */
.direction-icon {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  /* inline-flex takes its baseline from the icon, riding the whole cell
     ~3px above its row's text — align by middle instead (2026-06-09). */
  vertical-align: middle;
}

/* ── Save Indicator ─────────────────────── */
.save-indicator {
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: 500;
  color: var(--pill-success-fg);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.save-indicator.visible {
  opacity: 1;
  transform: translateY(0);
}

/* ── Call Detail Panel ───────────────────── */
/* v56. Enter fades the backdrop + slides the sheet (desktop right→in,
   mobile bottom→up). Exit via `.is-closing` reverses both: backdrop fades
   out over 200ms, sheet slides away via panelOut/panelUpOut keyframes.
   No more hard-vanish on close. */
.panel-overlay {
  position: fixed;
  inset: 0;
  background: var(--glass-backdrop-bg);
  backdrop-filter: var(--glass-backdrop-filter);
  -webkit-backdrop-filter: var(--glass-backdrop-filter);
  z-index: var(--z-drawer);
  animation: panelOverlayIn var(--motion-layout) var(--ease-out);
  /* Center in-flow dialogs (e.g. .notif-settings-sheet. broadcast modal,
     notification settings). Right-side .panel-sheet drawers use
     position: fixed so they're removed from this flex flow and keep
     anchoring themselves. */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
@keyframes panelOverlayIn {
  from { opacity: 0; backdrop-filter: blur(0) saturate(1); -webkit-backdrop-filter: blur(0) saturate(1); }
  to   { opacity: 1; }
}

/* Exit state. JS adds .is-closing immediately before unmount; CSS runs
   the 200ms reverse, then JS adds .hidden to commit the removal. */
.panel-overlay.is-closing {
  animation: panelOverlayOut var(--motion-layout) var(--ease-in) forwards;
}
@keyframes panelOverlayOut {
  from { opacity: 1; }
  to   { opacity: 0; }
}
.panel-overlay.is-closing .panel-sheet {
  animation: panelOut var(--motion-layout) var(--ease-in) forwards;
}
.panel-overlay.panel-mobile.is-closing .panel-sheet {
  animation: panelUpOut var(--motion-layout) var(--ease-in) forwards;
}
@media (prefers-reduced-motion: reduce) {
  .panel-overlay, .panel-overlay.is-closing, .panel-overlay.is-closing .panel-sheet {
    animation: none;
  }
}

.panel-sheet {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 440px;
  max-width: 100%;
  background: var(--bg-sheet);
  border-left: 1px solid var(--border-subtle);
  /* Directional. The drawer arrives from the RIGHT and sits over the list,
     so its shadow has to fall LEFT, onto the thing it covers. It was
     --shadow-xl, which points straight down and (pre-retune) at 10% black
     was invisible anyway. */
  box-shadow: -24px 0 60px rgba(0, 0, 0, 0.45),
              -1px 0 0 rgba(0, 0, 0, 0.30),
              inset 1px 0 0 rgba(255, 255, 255, 0.06);
  overflow-y: auto;
  /* Explicit, and it has to be. `visible` computes to `auto` on the axis
     that is left alone when the other one scrolls, so declaring only
     overflow-y made every sheet horizontally scrollable by construction —
     an accidental sideways swipe slid the drawer's content out of frame
     with nothing to slide back to. Landed only after the thing actually
     overflowing (the transcript latency-pill tooltip, css/transcript.css)
     was made to fit, so this clips nothing; it is the guard that stops the
     next wide child from quietly reintroducing the pan. */
  overflow-x: hidden;
  animation: panelIn 0.25s var(--ease-out);
  -webkit-overflow-scrolling: touch;
}

@keyframes panelIn {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}

@keyframes panelOut {
  from { transform: translateX(0); }
  to   { transform: translateX(100%); }
}

/* Mobile: full-screen panel */
.panel-overlay.panel-mobile .panel-sheet {
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  border-left: none;
  border-top: none;
  border-radius: 0;
  padding-top: 0;
  padding-bottom: 0;
  /* Rises from the bottom on mobile, so the shadow follows it. */
  box-shadow: 0 -18px 48px rgba(0, 0, 0, 0.45);
  animation: panelUpIn 0.25s var(--ease-out);
  /* Theme 6. kill iOS rubber-band bounce on the body when the sheet
     content reaches its scroll-top. The grey overscroll space Roman saw
     was the page underneath bouncing through; `contain` traps it. */
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}

@keyframes panelUpIn {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

@keyframes panelUpOut {
  from { transform: translateY(0); }
  to   { transform: translateY(100%); }
}

/* Drag handle for mobile bottom sheet */
.panel-handle {
  width: 40px;
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--border-hover);
  margin: var(--space-2) auto var(--space-4);
  flex-shrink: 0;
  display: none;
}

.panel-overlay.panel-mobile .panel-handle {
  display: block;
}

/* The CALL drawer runs wider than the generic 440px sheet, but not as wide
   as it briefly did. 440 gave the transcript a ~38-character measure, which
   is narrower than any readable column; the first correction overshot to
   720px, where the same measure came out at 72 characters — past the top of
   the comfortable band and, on a large screen, no longer reading as a drawer
   over a list (Roman, round 3).
   Measured in a headless harness against the real bubble markup, counting
   characters on the rendered lines rather than trusting the `ch` unit —
   `ch` is the width of a zero, which is wider than average prose and
   undercounts by about a fifth. 560px puts a transcript line at ~54
   characters; 720px put it at 72.
   A flat width, not a vw clamp. A clamp with any vw term worth having
   dipped to ~538px on a 1280 laptop, and those 22px cost four characters a
   line — the measure is the whole reason this number is not 440, so it
   should not be the thing that flexes.
   Scoped to ≥768px so the mobile full-bleed sheet is untouched (its rule
   loses on specificity to an id selector, so a media query is the guard,
   not source order); .panel-sheet's own max-width:100% covers the narrow
   end of that range. */
@media (min-width: 768px) {
  #call-detail-panel .panel-sheet {
    width: 560px;
  }
}

.panel-sheet::-webkit-scrollbar { width: 4px; }
.panel-sheet::-webkit-scrollbar-track { background: transparent; }
.panel-sheet::-webkit-scrollbar-thumb {
  background: var(--border-hover);
  border-radius: 2px;
}

/* ── Welcome state ───────────────────────── */
.welcome-card {
  text-align: center;
  padding: var(--space-10) var(--space-7);
  background: var(--bg-card);
  box-shadow: var(--ring-shadow);
  border-radius: var(--radius-xl);
  position: relative;
  overflow: hidden;
}

.welcome-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.04) 0%, transparent 50%, rgba(var(--accent-rgb), 0.02) 100%);
  pointer-events: none;
  border-radius: inherit;
}

.welcome-card > * {
  position: relative;
  z-index: 1;
  animation: welcomeFadeUp 0.5s var(--ease-out) backwards;
}

.welcome-hero { animation-delay: 0ms; }
.welcome-heading { animation-delay: 150ms; }
.welcome-desc { animation-delay: 250ms; }
.welcome-checklist { animation-delay: 400ms; }
.welcome-preview { animation-delay: 600ms; }

/* Transform-only — same ghost-page hazard as cardEnter; this one is
   `backwards`-filled too. See the note on cardEnter in global.css. */
@keyframes welcomeFadeUp {
  from { transform: translateY(12px); }
  to   { transform: translateY(0); }
}

/* Welcome hero illustration */
.welcome-hero {
  position: relative;
  width: 140px;
  height: 140px;
  margin: 0 auto var(--space-7);
}

.welcome-glow {
  position: absolute;
  inset: -20px;
  background: radial-gradient(circle, rgba(var(--accent-rgb), 0.15), transparent 70%);
  border-radius: 50%;
  animation: welcomeGlowPulse 3s ease-in-out infinite;
}

@keyframes welcomeGlowPulse {
  0%, 100% { opacity: 0.5; transform: scale(0.95); }
  50% { opacity: 1; transform: scale(1.05); }
}

.welcome-icon-circle {
  position: absolute;
  inset: 30px;
  background: rgba(var(--accent-rgb), 0.1);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(var(--accent-rgb), 0.15);
}

.welcome-icon-ring {
  position: absolute;
  border-radius: 50%;
  border: 1px solid rgba(var(--accent-rgb), 0.08);
}

.welcome-ring-outer {
  inset: 0;
  animation: ringPulse 3s ease-in-out infinite;
}

.welcome-ring-inner {
  inset: 15px;
  animation: ringPulse 3s ease-in-out 0.5s infinite;
}

@keyframes ringPulse {
  0%, 100% { opacity: 0.3; transform: scale(1); }
  50% { opacity: 0.7; transform: scale(1.02); }
}

.welcome-heading {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  margin: 0 0 var(--space-2);
}

.welcome-desc {
  color: var(--text-secondary);
  font-size: var(--text-body);
  max-width: 420px;
  padding-inline: 5%;
  margin: 0 auto var(--space-7);
  line-height: 1.6;
}

.welcome-checklist {
  display: inline-flex;
  flex-direction: column;
  gap: var(--space-3);
  text-align: left;
  margin-bottom: var(--space-8);
}

.welcome-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-body);
  font-weight: 500;
}

.welcome-check i,
.welcome-check svg { width: 20px; height: 20px; flex-shrink: 0; }
.welcome-check.done { color: var(--pill-success-fg); }
.welcome-check.done i,
.welcome-check.done svg { color: var(--success); }
.welcome-check.waiting { color: var(--text-muted); }
.welcome-check.waiting i,
.welcome-check.waiting svg { color: var(--text-muted); }

/* No dimming (was opacity 0.55). The preview is the hero's own empty
   state now, and that state is already quiet by construction: .is-zero
   mutes the figures and the zero-day bars are 2px dots. Dimming it further
   made real-looking numbers deniable; there are no numbers to deny. */
.welcome-preview {
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
  max-width: 480px;
  margin: 0 auto;
}

.welcome-preview-label {
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-align: center;
  margin-bottom: var(--space-4);
  font-weight: 500;
}

[data-theme="light"] .kpi-card.kpi-hero {
  background:
    radial-gradient(circle at 86% 18%, rgba(var(--accent-rgb), 0.14) 0%, rgba(var(--accent-rgb), 0.04) 28%, transparent 52%),
    linear-gradient(160deg, rgba(var(--accent-rgb), 0.08) 0%, rgba(var(--accent-rgb), 0.03) 44%, var(--bg-card) 100%);
  box-shadow:
    0 0 0 1px rgba(var(--accent-rgb), 0.12),
    0 14px 30px rgba(var(--accent-rgb), 0.08),
    var(--ring-shadow);
}

/* ── Audio Player Card ───────────────────── */
.audio-player-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
}

.audio-player-card audio {
  width: 100%;
  height: 40px;
}

/* Hero KPI layout. first card wider */
.kpi-grid-hero {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--space-5);
}

@media (max-width: 767px) {
  .kpi-grid-hero {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3);
  }
}

/* ── Dashboard-specific components ────────── */
.greeting-bar-main { display:flex;align-items:center;gap:var(--space-3);flex:1 1 auto;min-width:0; }  /* was width:100% — forced the date eyebrow onto its own row (audit B1) */
.dashboard-split { margin-top: var(--space-8); gap: var(--space-7); }
.dashboard-recent-section { margin-top: var(--space-8); }
.dashboard-chart-wrap { height: 280px; }
.dashboard-section-divider { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--border-subtle); }
.quick-stat-row-fill { flex: 1; }
.quick-stat-value--body { font-family: var(--font-body); font-size: var(--text-sm); }
.quick-stat-row--interactive {
  cursor: pointer;
  border-radius: 6px;
  margin: 0 -8px;
  padding-left: 8px;
  padding-right: 8px;
  transition: background var(--duration-normal) var(--ease-in-out);
}
@media (hover: hover) {
  .quick-stat-row--interactive:hover { background: rgba(99, 102, 241, 0.06); }
}
.quick-stat-value--warning { color: var(--pill-warning-fg); }
.quick-stat-arrow {
  width: 14px;
  height: 14px;
  display: inline;
  vertical-align: -2px;
  opacity: 0.7;
}
.status-inline-icon {
  width: 12px;
  height: 12px;
  display: inline;
  vertical-align: -2px;
  margin-right: 3px;
}
.status-inline-icon--spin {
  animation: spin 1s linear infinite;
}

.insight-banner { display:flex;align-items:flex-start;gap:14px;padding:20px 24px;margin-bottom:var(--space-6);background:linear-gradient(135deg,rgba(var(--accent-rgb),0.08),rgba(var(--accent-rgb),0.03));border:1px solid rgba(var(--accent-rgb),0.12);border-radius:var(--radius-lg);font-size:var(--text-body);line-height:1.6;position:relative;overflow:hidden; }
.insight-banner::after { content:'';position:absolute;top:0;left:-100%;width:50%;height:100%;background:linear-gradient(90deg,transparent,rgba(var(--accent-rgb),0.03),transparent);animation:insightShimmer 2s 0.5s ease forwards;pointer-events:none; }
@keyframes insightShimmer { to{left:200%} }
.insight-icon { flex-shrink:0;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(var(--accent-rgb),0.15);color:var(--accent); }
.insight-icon i,
.insight-icon svg { width:18px;height:18px; }

.live-dot { display:inline-flex;align-items:center;margin-left:10px;position:relative;vertical-align:middle; }
.live-dot-core { width:7px;height:7px;border-radius:50%;background:var(--success); }
.live-dot-ping { position:absolute;width:7px;height:7px;border-radius:50%;background:var(--success);animation:livePing 1.5s cubic-bezier(0,0,0.2,1) infinite; }
@keyframes livePing { 0%{transform:scale(1);opacity:0.75} 75%,100%{transform:scale(2.5);opacity:0} }

.greeting-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1em;
  height: 1em;
  margin-right: 6px;
  /* Icon sits slightly below the text baseline. aligns the optical
     center of a circle icon with lowercase-x height across font sizes.
     -0.12em was tuned by eye across 19/21/30px h1 values. */
  vertical-align: -0.12em;
  color: var(--text-muted);
  /* Settle, not a pop. The old shape was scale(0.5) rotate(-15deg) over 500ms
     on --ease-spring after a 300ms delay: a glyph inflating from half size,
     overshooting past 1 and untwisting, a beat after the greeting it belongs
     to had already landed. Four separate tells, and together they read as a
     sticker being applied rather than as the page settling.

     One motion instead, inside the budget: 0.94 → 1 on --ease-out over 240ms,
     no rotation, no overshoot, centred origin. 0.94 is close enough that the
     eye reads it as the icon arriving at rest rather than as a size change,
     which is the whole difference between premium and cheap here. The 80ms
     delay puts it just behind #content's rise (contentEnter, global.css) so
     the glyph settles into a headline that has already stopped moving.

     No opacity leg, deliberately: an opacity-0 from-state on a mount
     animation strands the element invisible under a frozen clock — see the
     transform-only note above .today-board. At 0.94 there is nothing to fade
     in from anyway, and contentEnter (global.css) is transform-only for the
     same reason, so this stays cohesive with it. */
  animation: greetingIconSettle 240ms var(--ease-out) 80ms backwards;
  transform-origin: center;
}

/* contentEnter is gated in global.css; this one is its own rule and needs
   its own gate. */
@media (prefers-reduced-motion: reduce) {
  .greeting-icon { animation: none; }
}

/* svg selector required post-Lucide-swap; without it the glyph stayed at
   the 24px default — oversize and optically high next to the greeting. */
.greeting-icon i,
.greeting-icon svg { width: 100%; height: 100%; }

/* Transform-only: `backwards` + a delay is the worst shape for the ghost bug,
   so the from-state has to be something that is safe to paint and hold. See
   cardEnter in global.css. */
@keyframes greetingIconSettle {
  from { transform: scale(0.94); }
  to   { transform: scale(1); }
}

/* Greeting bar. subtle bottom hairline anchors it as a header row
   so it reads as part of the page flow, not orphaned text above the
   cards. Margin-bottom increased slightly so the border isn't flush
   against the hero. */
.greeting-bar {
  display: flex;
  align-items: baseline;  /* date eyebrow shares the greeting's baseline (audit B1) */
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding-bottom: var(--space-4);
  margin-bottom: var(--space-5);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.greeting-bar h1 { font-family:var(--font-display);font-size:var(--text-h1);font-weight:600;letter-spacing:-0.025em;color:var(--text-primary);margin:0;line-height:1.12; }
.greeting-right { display:flex;align-items:center;gap:16px;flex-wrap:wrap; }
.greeting-date { font-size:var(--text-sm);color:var(--text-muted);white-space:nowrap; }

/* Feels-more-Apple (5c): iOS-style large-title → condensed sticky header.
   Opt-in per page via [data-condense-header]; page-transitions.js toggles
   .is-condensed once the page scrolls. The shrink animates via these
   transitions (zeroed by the global reduced-motion reset → instant snap).
   Currently applied to the overview greeting bar. */
[data-condense-header] {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg-page);
  /* Tracks #content's horizontal padding so ::before can reach the page
     edge; see the pseudo below. Overridden per breakpoint at the bottom of
     this block. */
  --condense-gutter: var(--space-8);
  /* Breathing room between the viewport edge and the title once stuck —
     the bar had none, so scrolled content grazed the h1's ascenders in the
     window before .is-condensed lands. Held constant (the condensed state
     sets the same value) so only the bottom padding, bottom margin and
     type size condense. The negative margin cancels it out of normal flow,
     leaving the resting position of the title exactly where it was. */
  padding-top: var(--space-3);
  margin-top: calc(-1 * var(--space-3));
  transition: padding var(--motion-layout) var(--ease-out),
              margin-bottom var(--motion-layout) var(--ease-out),
              border-color var(--motion-hover) var(--ease-out);
}
/* The bar is a normal-flow child of #content, so its own background stops
   at the content box and leaves #content's side gutters uncovered —
   anything that overflows a card (shadow, hover lift, focus ring, the
   realtime pulse) scrolls visibly past the stuck header in those strips.
   Repaint the background out to the gutter edge. Absolutely positioned, so
   it is not a flex item of .greeting-bar; z-index -1 keeps it under the
   bar's own content but still inside the bar's stacking context (z-index
   20), i.e. above the scrolling page. */
[data-condense-header]::before {
  content: '';
  position: absolute;
  inset: 0 calc(-1 * var(--condense-gutter));
  background: var(--bg-page);
  z-index: -1;
  pointer-events: none;
}
@media (max-width: 767px) {
  [data-condense-header] { --condense-gutter: 16px; }
}
@media (min-width: 768px) and (max-width: 1023px) {
  [data-condense-header] { --condense-gutter: var(--space-5); }
}
[data-condense-header] h1 {
  transition: font-size var(--motion-layout) var(--ease-out);
}
[data-condense-header].is-condensed {
  padding-top: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  /* The hairline belongs to the RESTING header only (Roman, 2026-08-10).
     At rest it anchors the title to the page and reads well. Once the bar
     sticks, the greeting has shrunk into a strip whose background is
     already an opaque --bg-page band — that band, not a rule, is what
     separates the header from the content scrolling under it. So the line
     stops separating anything and just sits there: an inset 1px rule with
     a condensed title floating above it, hugging the first card because
     .is-condensed also tightens margin-bottom. Fades at --motion-hover
     (100ms, the token for border colour) against the layout transition's
     slower shrink, so the line is gone before the resize finishes rather
     than riding it down. Colour only — the 1px still holds its space, so
     nothing shifts. */
  border-bottom-color: transparent;
}
[data-condense-header].is-condensed h1 {
  font-size: var(--text-title);
}

/* Quick stats: vertical list */
/* ─── At-a-glance rows: illuminated data surface ────────────────────────
   Row rhythm from spacing only (no hairline dividers). Icons on neutral
   elevated background with muted semantic color (warning/danger reserved
   for true alerts). Values in Inter with tabular-nums for product-tier feel. */
.quick-stat-row { display:flex;align-items:center;gap:14px;padding:14px 0; }
.quick-stat-row + .quick-stat-row { border-top: none; }
.quick-stat-icon {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: var(--bg-elevated);
}
.quick-stat-icon i,
.quick-stat-icon svg { width:14px;height:14px; }
.quick-stat-label { flex:1;font-size:var(--text-sm);color:var(--text-secondary);font-weight:400; }
.quick-stat-value {
  font-family: var(--font-body);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-h2);
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}

@media (max-width: 767px) {
  .greeting-bar { flex-direction:column;align-items:flex-start; }
  /* Column layout shrink-wraps the main row, leaving the bell/gear
     hugging the greeting text — stretch it so they reach the right edge. */
  .greeting-bar-main { width:100%; }
  /* Slight mobile bump: 19px was a touch under-scale given it's the only
     personal / anchoring element on the page. 21px keeps the overline
     voice of the stats loud but lets the "Good morning, Roman" land. */
  .greeting-bar h1 { font-size:1.3125rem; }
  .greeting-right { width:100%; }
  .dashboard-split,
  .dashboard-recent-section {
    margin-top: var(--space-6);
  }
  .dashboard-chart-wrap {
    height: 220px;
  }
  .insight-banner {
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
    font-size: var(--text-sm);
  }
  .insight-icon {
    width: 32px;
    height: 32px;
  }
  .welcome-card {
    padding: var(--space-7) var(--space-4);
    border-radius: var(--radius-lg);
  }
  .welcome-hero {
    width: 100px;
    height: 100px;
    margin-bottom: var(--space-5);
  }
  .welcome-icon-circle { inset: 20px; }
  .welcome-icon-circle i,
  .welcome-icon-circle svg { width: 24px !important; height: 24px !important; }
  .welcome-ring-inner { inset: 10px; }
  .welcome-heading { font-size: 1.25rem; }
  .welcome-desc { font-size: var(--text-sm); }
  .quick-stat-row {
    padding: var(--space-3) 0;
  }
  .quick-stat-row--interactive {
    margin: 0 calc(-1 * var(--space-2));
    padding-left: var(--space-2);
    padding-right: var(--space-2);
  }
}

/* ── Utility classes for inline style replacement ── */
.section-gap { margin-top: var(--space-8); }
.badge-faded { opacity: 0.5; }
.kpi-rate-layout { display: flex; align-items: flex-end; gap: var(--space-4); margin-top: auto; min-height: 64px; }
.kpi-rate-ring { position: relative; width: 64px; height: 64px; flex-shrink: 0; }
.freshness-bar { display: flex; justify-content: flex-end; margin-bottom: var(--space-2); }
/* Freshness indicator contained inside the greeting-right row (was a free-
   floating row below the insight banner; see Phase 6 Bug E). Inline-flex so
   it aligns with the date/billing pills; muted so it stays in tertiary
   hierarchy. */
.greeting-freshness {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  /* --text-xs is a phantom token (never defined) — this inherited the parent's
     size instead of rendering tertiary. --text-caption is the real 11px step. */
  font-size: var(--text-caption);
  color: var(--text-muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.greeting-freshness .status-inline-icon { width: 12px; height: 12px; }
/* Section wrapper + eyebrow (audit Tier 2 #8). The eyebrow re-introduces
   the overline discipline used on overview ("YOUR DAY AT A GLANCE") so
   the three sections read as clearly staged chapters, not just headings
   dropped between cards. Scoped to .chart-section-heading-wrap so the
   admin analytics (which doesn't use numbered sections) is untouched. */
.chart-section-heading-wrap {
  padding-top: var(--space-6);
}
.chart-section-heading { font-family: var(--font-body); font-size: var(--text-h2); font-weight: 600; letter-spacing: -0.004em; }
.chart-section-desc { font-size: var(--text-sm); color: var(--text-muted); margin-top: var(--space-1); }
/* Icon color variants. single-accent covenant. Info + accent collapse into
   indigo. Warning/danger reserved for genuinely semantic uses (follow-up
   needed, etc.). Neutral is the default background for non-semantic rows. */
.quick-stat-icon--info,
.quick-stat-icon--accent {
  background: var(--accent-muted);
  color: var(--accent-strong);
}
.quick-stat-icon--warning {
  background: var(--bg-elevated);
  color: var(--warning);
}
.quick-stat-icon--danger {
  background: var(--bg-elevated);
  color: var(--danger);
}
.quick-stat-icon--neutral {
  background: var(--bg-elevated);
  color: var(--text-muted);
}

/* ── Feedback Feed ──────────────────────── */
.feedback-feed {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.feedback-card {
  background: var(--bg-card);
  box-shadow: var(--ring-shadow);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  cursor: pointer;
  transition: box-shadow var(--duration-normal) ease, transform var(--duration-normal) ease;
}

.feedback-card:hover {
  box-shadow: var(--ring-shadow-hover);
  transform: translateY(-1px);
}

.feedback-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-2);
}

.feedback-stars {
  display: flex;
  gap: 2px;
}

.feedback-star {
  width: 16px;
  height: 16px;
  color: var(--text-muted);
}

.feedback-star.filled {
  color: var(--warning);
}

.feedback-client {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.feedback-comment {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-style: italic;
  line-height: 1.5;
  margin-bottom: var(--space-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.feedback-card-meta {
  display: flex;
  gap: var(--space-2);
  font-size: var(--text-caption);
  color: var(--text-muted);
}

.feedback-card-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

.feedback-ack-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-caption);
  font-weight: 500;
  color: var(--text-muted);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: none;
  cursor: pointer;
  transition: background var(--duration-normal) var(--ease-in-out), color var(--duration-normal) var(--ease-in-out), border-color var(--duration-normal) var(--ease-in-out);
}

.feedback-ack-btn:hover {
  background: var(--success-muted);
  border-color: var(--success);
  color: var(--pill-success-fg);
}

.feedback-ack-btn.acked {
  background: var(--success-muted);
  border-color: transparent;
  color: var(--pill-success-fg);
  pointer-events: none;
}

.feedback-ack-btn i,
.feedback-ack-btn svg { width: 14px; height: 14px; }

/* Feedback tabs */
.feedback-tabs {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

#feedback-toolbar {
  align-items: center;
  gap: var(--space-3);
}

#feedback-toolbar .feedback-tabs {
  margin-bottom: 0;
}

#feedback-toolbar .filter-group {
  margin-bottom: 0;
}

#feedback-toolbar .feedback-client-inline {
  display: inline-flex;
  align-items: center;
}

#feedback-toolbar .feedback-client-inline-label {
  font-size: var(--text-caption);
  font-weight: 500;
  color: var(--text-muted);
  line-height: 1;
  white-space: nowrap;
}

#feedback-toolbar .feedback-client-select {
  width: 166px;
  min-width: 166px;
}

@media (max-width: 767px) {
  #feedback-toolbar {
    align-items: stretch;
  }

  #feedback-toolbar .feedback-client-inline {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
  }

  #feedback-toolbar .feedback-client-inline-label {
    line-height: normal;
  }

  #feedback-toolbar .feedback-client-select {
    width: 100%;
    min-width: 0;
  }
}

.feedback-tab {
  font-size: var(--text-sm);
  font-weight: 500;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  color: var(--text-muted);
  background: none;
  border: none;
  cursor: pointer;
  transition: background var(--duration-normal) var(--ease-in-out), color var(--duration-normal) var(--ease-in-out);
}

.feedback-tab:hover { color: var(--text-primary); }
.feedback-tab.active { background: var(--accent-muted); color: var(--accent); }

@media (max-width: 767px) {
  .feedback-card { padding: var(--space-3) var(--space-4); }
  .feedback-card-top { flex-direction: column; align-items: flex-start; gap: var(--space-1); }
}

.notif-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  width: 40px;
  height: 40px;
  border: none;
  background: transparent;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  transition: color var(--duration-normal) var(--ease-out), background var(--duration-normal) var(--ease-out);
  touch-action: manipulation;
}
.notif-bell:hover {
  background: var(--bg-elevated);
  color: var(--text-primary);
}
.notif-bell:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Lucide swaps the class-less <i data-lucide> for an unmatched <svg>,
   so the glyph rendered at the 24px default — target svg too. */
.notif-bell i,
.notif-bell svg { width: 20px; height: 20px; }
@media (max-width: 767px) {
  .notif-bell { width: 36px; height: 36px; }
  .notif-bell i,
  .notif-bell svg { width: 19px; height: 19px; }
}

/* Header settings gear (client overview) — the client's Settings entry.
   Mobile-only: on desktop the sidebar footer already carries Notifications /
   theme / Sign out. A bottom-nav entry was tried (C1, 2026-09-05) and made
   six tabs with Calendar and Jobs on; too many for a phone, so the gear. */
.header-settings-btn { display: none; }
@media (max-width: 767px) {
  .header-settings-btn { display: inline-flex; }
}

/* ── Admin Clients Detail ───────────────── */
.admin-list-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  transition: color var(--duration-fast) var(--ease-out);
}

.back-link:hover { color: var(--text-primary); }
.back-link i,
.back-link svg { width: 16px; height: 16px; }

.admin-detail-back {
  margin-bottom: var(--space-7);
}

.admin-client-detail {
  --admin-detail-divider: color-mix(in srgb, var(--border) 38%, transparent);
  --admin-detail-highlight: color-mix(in srgb, var(--border) 45%, transparent);
  --admin-detail-hover: color-mix(in srgb, var(--bg-card) 48%, transparent);
  --admin-danger-border: color-mix(in srgb, var(--danger) 30%, var(--border));
  --admin-danger-border-strong: color-mix(in srgb, var(--danger) 55%, var(--border));
  /* No max-width of its own (U1, 2026-09-04): the workspace runs at the
     shell's measure like every other admin page, so Clients -> a client no
     longer narrows the frame by 240px. */
  padding-bottom: var(--space-10);
}

.admin-detail-hero {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-7);
  margin-bottom: var(--space-9);
}

.admin-detail-hero--loading {
  justify-content: flex-start;
}

.admin-detail-hero-main {
  display: flex;
  align-items: flex-start;
  gap: var(--space-6);
  min-width: 0;
}

.admin-detail-avatar,
.admin-detail-avatar-skeleton {
  width: 80px;
  height: 80px;
  border-radius: var(--radius-lg);
  flex: 0 0 auto;
}

.admin-detail-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 42%),
    var(--accent-muted);
  color: var(--accent-strong);
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: 700;
  letter-spacing: -0.02em;
  box-shadow: inset 0 1px 0 var(--admin-detail-highlight);
}

/* Logo variant — the tint steps aside so the artwork owns the square. */
.admin-detail-avatar.has-logo { background: var(--bg-elevated); }
.admin-detail-avatar__img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The hero avatar doubles as the Edit-profile entry: the logo lives here, so
   "change it" belongs on it. The whole 80px (64px mobile) square is the target
   and the badge is always visible, since touch has no hover to reveal it. */
.admin-detail-avatar--edit {
  position: relative;
  padding: 0;
  overflow: hidden;
  transition: box-shadow var(--motion-hover) var(--ease-out),
              transform var(--motion-hover) var(--ease-out);
}
.admin-detail-avatar--edit:hover {
  box-shadow: inset 0 1px 0 var(--admin-detail-highlight), 0 0 0 2px var(--accent-muted);
}
.admin-detail-avatar--edit:active { transform: scale(0.98); }
.admin-detail-avatar--edit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.admin-detail-avatar__badge {
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--bg-card);
  color: var(--text-secondary);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28), inset 0 0 0 1px var(--border-subtle);
}
.admin-detail-avatar__badge svg { width: 13px; height: 13px; }

@media (prefers-reduced-motion: reduce) {
  .admin-detail-avatar--edit { transition: none; }
  .admin-detail-avatar--edit:active { transform: none; }
}

.admin-detail-hero-skeleton-copy {
  min-width: 280px;
  padding-top: var(--space-2);
}

.admin-detail-identity {
  min-width: 0;
  padding-top: var(--space-1);
}

.admin-detail-title-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.admin-detail-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.035em;
  color: var(--text-primary);
}

.admin-detail-industry {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.admin-detail-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.admin-detail-meta-copy {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.admin-detail-meta-copy:hover {
  color: var(--text-primary);
}

.admin-detail-meta-separator {
  color: var(--text-muted);
  opacity: 0.65;
}

.status-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 24px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid currentColor;
  font-size: var(--text-caption);
  font-weight: 600;
  line-height: 1;
}

.status-chip-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.admin-detail-hero-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
  flex: 0 0 auto;
}

.admin-detail-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition:
    color var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

.admin-detail-icon-btn:hover:not(:disabled) {
  color: var(--text-primary);
  background: var(--admin-detail-hover);
  border-color: var(--border);
}

.admin-detail-icon-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.admin-detail-icon-btn i,
.admin-detail-icon-btn svg {
  width: 16px;
  height: 16px;
}

.admin-detail-more {
  position: relative;
}

.admin-detail-menu {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  min-width: 176px;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-card-solid);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-dropdown);
}

.admin-detail-menu button {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  text-align: left;
  cursor: pointer;
}

.admin-detail-menu button:hover {
  background: var(--bg-card-hover);
  color: var(--text-primary);
}

.admin-detail-menu i,
.admin-detail-menu svg {
  width: 14px;
  height: 14px;
}

.admin-detail-edit-panel {
  margin-bottom: var(--space-8);
  padding: var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
}

.detail-grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
}

.detail-grid-tight {
  gap: var(--space-3);
}

.form-group-tight {
  margin-bottom: var(--space-2);
}

.detail-actions-row {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.admin-detail-section {
  margin-top: var(--space-7);
}

.admin-detail-section:first-of-type {
  margin-top: 0;
}

/* The onboarding-answers mount is a <div>, so the section after it is still
   :first-of-type and lost its top margin — gluing "Agents" to the onboarding
   card (audit P2-2). Restore the section rhythm explicitly (mirrors the base
   section gap above). */
#onboarding-answers-mount + .admin-detail-section {
  margin-top: var(--space-7);
}

/* Same :first-of-type trap, other direction. Service-areas & booking mounts
   into a <div> in the MIDDLE of the page, so its section is the first (and
   only) one of its type inside that wrapper and zeroed its own top margin —
   welding its heading to the Dashboard-preferences card above it. */
#service-setup-mount > .admin-detail-section {
  margin-top: var(--space-7);
}

.admin-detail-section-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-3);
}

.admin-detail-section-title {
  margin: var(--space-1) 0 0;
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}

.admin-detail-count {
  color: var(--text-muted);
  font-weight: 500;
}

.admin-detail-section-caption {
  margin: var(--space-1) 0 0;
  max-width: 560px;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.45;
}

.admin-detail-text-action {
  position: relative;
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}
/* The "+ Add …" action is bare text at roughly 62x22 — well under the 40px
   floor, and it is the only way to add an agent. Visual weight is right for
   a text action, so the target grows instead.
   One consumer today: .drawer-add-agent-btn in the client-agents drawer,
   which overrides this to 44 further down the file. The comment here used to
   claim a second, workspace-side consumer and point at "the drawer's own add
   button" as if that were a different control; it is the same one. */
.admin-detail-text-action::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  min-width: 40px;
  height: 40px;
  transform: translate(-50%, -50%);
}
.admin-detail-text-action:active:not(:disabled) { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) {
  .admin-detail-text-action:active:not(:disabled) { transform: none; }
}

.admin-detail-text-action:disabled {
  color: var(--text-muted);
  cursor: not-allowed;
  opacity: 0.7;
}

.admin-detail-section-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  padding: var(--space-6);
}

.admin-detail-list {
  padding-top: 0;
  padding-bottom: 0;
}

.admin-detail-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: 68px;
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--admin-detail-divider);
}

.admin-detail-row:first-child {
  padding-top: 0;
}

.admin-detail-row:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.admin-detail-row-main {
  min-width: 0;
  flex: 1;
}

.admin-detail-row-sub {
  display: block;
  width: fit-content;
  margin-top: var(--space-1);
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  text-align: left;
}

button.admin-detail-row-sub {
  cursor: pointer;
}

button.admin-detail-row-sub:hover {
  color: var(--text-primary);
}

.admin-detail-id-chip {
  display: inline-block;
  max-width: 100%;
  margin-top: var(--space-2);
  padding: 3px var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--bg-card-subtle);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  cursor: pointer;
  /* Long retell agent IDs would wrap to 3 lines and push the action
     icons down; truncate with ellipsis on a single line at every viewport
     width and rely on the title attribute for full text on hover. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin-detail-id-chip:hover {
  color: var(--text-secondary);
  border-color: var(--border-hover);
}

.admin-detail-row-side {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
}

.admin-detail-row-action,
.admin-user-actions {
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.admin-detail-row:hover .admin-detail-row-action,
.admin-detail-row:focus-within .admin-detail-row-action,
.admin-detail-row:hover .admin-user-actions,
.admin-detail-row:focus-within .admin-user-actions {
  opacity: 1;
}

.admin-agent-status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-muted);
  flex-shrink: 0;
}

.admin-agent-status-dot.is-connected {
  background: var(--success);
}

/* Inline agent name + status dot. Keeps the dot adjacent to the name
   instead of floating it to the far right edge of the card, which made
   the connection state feel disconnected from what it referred to. */
.admin-agent-name-line {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.agent-edit-form {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.admin-notes-wrap {
  position: relative;
}

.notes-area {
  width: 100%;
  min-height: 180px;
  resize: vertical;
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-sunken);
  color: var(--text-primary);
  font-family: var(--font-display);
  font-size: 1rem;
  line-height: 1.6;
  box-shadow: inset 0 1px 0 var(--admin-detail-highlight);
  transition:
    border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

/* No `outline: none` here — see the .form-input note above. At (0,2,0) it
   outranks `input:focus-visible` (0,1,1) and deletes the keyboard ring; the
   global `*:focus { outline: none }` already suppresses the mouse case and
   correctly loses to focus-visible. */
/* The bespoke 3px 14%-alpha glow went with it: it was a second, softer
   focus language competing with the app's one accent ring, and it fired on
   mouse focus too. The border shift is the hover/focus affordance; the ring
   is the keyboard one. */
.notes-area:focus {
  border-color: var(--accent);
  box-shadow: inset 0 1px 0 var(--admin-detail-highlight);
}

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

.admin-notes-save {
  display: block;
  margin-top: var(--space-2);
  text-align: right;
  color: var(--text-muted);
  font-size: var(--text-caption);
  opacity: 0.78;
  transition: opacity 180ms var(--ease-out), color 180ms var(--ease-out);
}

.admin-notes-save[data-state="saving"] {
  color: var(--text-secondary);
}

.admin-notes-save[data-state="error"] {
  color: var(--pill-danger-fg);
}

.simplistic-ui-status[hidden] {
  display: none;
}

.admin-user-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  background: var(--accent-muted);
  color: var(--accent-strong);
  font-size: var(--text-caption);
  font-weight: 700;
  flex: 0 0 auto;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}

.admin-user-last-login {
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

/* Mobile-only inline timestamp that lives next to the role text. Desktop
   keeps using the side-cluster timestamp; this copy hides above 768px so
   the role line doesn't double up on the timestamp. */
.admin-user-last-login-inline {
  font-variant-numeric: tabular-nums;
}
@media (min-width: 769px) {
  .admin-user-last-login-inline {
    display: none;
  }
}

.admin-user-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.admin-user-inline-confirm {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.admin-user-password-input {
  flex: 1;
}

/* .admin-detail-empty-note and .admin-detail-empty-state retired (U3): the
   client detail's empty sections render through .empty-state--line and
   .empty-state.empty-state--compact. */

.admin-detail-setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  padding: var(--space-5) 0;
}

.admin-detail-setting-copy {
  min-width: 0;
}

.admin-detail-setting-title {
  color: var(--text-primary);
  font-size: var(--text-body);
  font-weight: 600;
  line-height: 1.3;
}

.admin-detail-setting-copy p {
  margin: var(--space-1) 0 0;
  max-width: 620px;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.admin-integration-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: 56px;
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.admin-integration-icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  background: var(--accent-muted);
  color: var(--accent);
  flex: 0 0 auto;
}

.admin-integration-icon i,
.admin-integration-icon svg {
  width: 16px;
  height: 16px;
}

.admin-integration-copy {
  display: grid;
  gap: var(--space-1);
  flex: 1;
  min-width: 0;
}

.admin-integration-status {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.admin-integration-status[data-kind="connected"] {
  color: var(--pill-success-fg);
}

.admin-integration-status[data-kind="error"] {
  color: var(--pill-danger-fg);
}

.admin-integration-chevron {
  width: 16px;
  height: 16px;
  color: var(--text-muted);
  transition: transform 200ms var(--ease-out);
}

.calendar-config-card.is-expanded .admin-integration-chevron {
  transform: rotate(180deg);
}

.admin-integration-panel {
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  transition:
    max-height 200ms var(--ease-out),
    opacity 160ms var(--ease-out),
    margin-top 200ms var(--ease-out);
}

.calendar-config-card.is-expanded .admin-integration-panel {
  max-height: 1200px;
  margin-top: var(--space-5);
  opacity: 1;
}

.admin-detail-section--danger {
  margin-top: var(--space-10);
  padding-top: var(--space-8);
  border-top: 1px solid var(--border);
}

.danger-zone-card-v2 {
  border-color: var(--admin-danger-border);
  background: linear-gradient(180deg, rgba(var(--danger-rgb), 0.045), rgba(var(--danger-rgb), 0.015));
}

.danger-zone-action {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding: var(--space-5) 0;
  border-bottom: 1px solid var(--admin-danger-border);
}

.danger-zone-action:first-child {
  padding-top: 0;
}

.danger-zone-action:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.danger-zone-action-text {
  min-width: 0;
  flex: 1;
}

.danger-zone-action-text p {
  margin: var(--space-1) 0 0;
  max-width: 640px;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.danger-zone-btn {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* --admin-danger-border-strong is scoped to .admin-client-detail, so
   outside that subtree the whole `border` shorthand went invalid at
   computed-value time and this outline button rendered with NO outline.
   The fallback repeats the scoped definition verbatim, so nothing inside
   .admin-client-detail changes. (Found 2026-08-07 while reconciling the
   later .btn overrides against the refreshed base.) */
.btn-danger-outline {
  border: 1px solid var(--admin-danger-border-strong, color-mix(in srgb, var(--danger) 55%, var(--border)));
  background: transparent;
  color: var(--pill-danger-fg);
}

.btn-danger-outline:hover:not(:disabled) {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--pill-danger-fg);
}

.modal-compact {
  max-width: 400px;
}

.modal-compact-wide {
  max-width: 420px;
}

.modal-copy-spaced {
  margin-bottom: var(--space-4);
}

.modal-copy-relaxed {
  line-height: 1.6;
}

.form-group-flush {
  margin-bottom: 0;
}

.form-error-spaced {
  margin-top: var(--space-2);
}

.danger-title {
  color: var(--pill-danger-fg);
}

.feedback-pagination {
  margin-top: var(--space-3);
}

.feedback-client-group {
  min-width: 164px;
}

.feedback-client-select {
  min-width: 164px;
}

.modal-compact {
  max-width: 400px;
}

.modal-compact-wide {
  max-width: 420px;
}

.modal-copy-spaced {
  margin-bottom: var(--space-4);
}

.modal-copy-relaxed {
  line-height: 1.6;
}

.form-group-flush {
  margin-bottom: 0;
}

.form-error-spaced {
  margin-top: var(--space-2);
}

.reset-pw-btn {
  margin-top: var(--space-1);
  padding: 2px 8px;
  font-size: var(--text-caption);
}

.reset-pw-btn i,
.reset-pw-btn svg {
  width: 12px;
  height: 12px;
}

.danger-title {
  color: var(--pill-danger-fg);
}

@media (max-width: 767px) {
  .admin-list-actions {
    width: 100%;
  }

  .admin-list-actions .btn {
    flex: 1;
    justify-content: center;
  }

  .admin-client-detail {
    padding-bottom: var(--space-8);
  }

  .admin-detail-back {
    margin-bottom: var(--space-5);
  }

  .admin-detail-hero {
    display: grid;
    gap: var(--space-4);
    margin-bottom: var(--space-8);
  }

  .admin-detail-hero-main {
    gap: var(--space-4);
  }

  .admin-detail-avatar,
  .admin-detail-avatar-skeleton {
    width: 64px;
    height: 64px;
  }

  .admin-detail-title {
    font-size: 1.5rem;
    line-height: 1.12;
  }

  .admin-detail-meta {
    gap: var(--space-2);
    margin-top: var(--space-3);
  }

  .admin-detail-hero-actions {
    justify-self: stretch;
    margin-top: 0;
  }

  .admin-detail-icon-btn {
    width: 44px;
    height: 44px;
  }

  .admin-detail-section {
    margin-top: var(--space-7);
  }

  .admin-detail-section-header {
    align-items: flex-start;
  }

  .admin-detail-section-card {
    padding: var(--space-5);
  }

  .admin-detail-row {
    align-items: flex-start;
  }

  /* On mobile the timestamp moves out of the side and gets rendered inline
     after the role ("Admin · 17 min ago"), which puts it in a column where
     it can't visually collide with a long email. Hide the side copy here. */
  .user-row-v2 .admin-user-last-login {
    display: none;
  }

  /* Long emails were overflowing the main column on narrow viewports and
     bleeding into the side, making the timestamp look like it was sitting
     on top of the email. Truncate to a single line with ellipsis. */
  .admin-user-email {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .admin-detail-row-side {
    align-self: center;
  }

  .admin-detail-row-action,
  .admin-user-actions {
    opacity: 1;
  }

  /* Mobile: keep the chip as a single ellipsizing line and let it expand
     to fill the available width. Tap to copy still works; the title
     attribute carries the full id for accessibility. */
  .admin-detail-id-chip {
    display: block;
    max-width: 100%;
  }
  .admin-detail-row-side {
    gap: var(--space-2);
    align-self: center;
  }
  /* Stack the last-login under the icons on tight screens so the row reads
     as: avatar | email + role | actions. The timestamp lives inside the
     row-side container with a small visual separator. */
  .admin-user-last-login {
    font-size: var(--text-caption);
    white-space: nowrap;
  }
  .admin-detail-row {
    gap: var(--space-3);
    padding: var(--space-4) 0;
  }
  /* Agent card mobile - more padding for the row's outer card so the
     content isn't pressed against the border. */
  .agent-card .admin-detail-row-main {
    /* Allow more space between the lines when the chip + phone stack. */
    gap: 6px;
  }

  .admin-detail-setting-row,
  .danger-zone-action {
    align-items: stretch;
    flex-direction: column;
    gap: var(--space-4);
  }

  .danger-zone-btn {
    width: 100%;
    justify-content: center;
    min-height: 44px;
  }

  .notes-area {
    min-height: 160px;
  }

  .admin-integration-row {
    min-height: 64px;
  }

  .admin-user-inline-confirm {
    align-items: stretch;
    flex-direction: column;
  }

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

  .feedback-client-group,
  .feedback-client-select {
    width: 100%;
    min-width: 0;
  }
}

/* ── Long content protection ────────────── */
.call-card-caller,
.sidebar-user-name,
.sidebar-logo-text,
.feedback-client,
.kpi-label-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Ray's 3-second glance: hero KPI label stands out on mobile */
@media (max-width: 767px) {
  .kpi-card.kpi-hero .kpi-label {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-secondary);
  }
}

/* Ray's mobile: pagination buttons full-width */
@media (max-width: 767px) {
  .pagination {
    flex-direction: column;
    gap: var(--space-3);
    align-items: stretch;
    text-align: center;
  }
  .pagination-buttons {
    display: flex;
    gap: var(--space-2);
  }
  .pagination-buttons .btn {
    flex: 1;
    justify-content: center;
  }
}

/* "New since last visit" indicator dot */
/* A LEADING GUTTER, not a trailing tag (audit E5). The dot used to sit
   AFTER the timestamp, so its x-position moved with the length of the date
   string — measured 7.8px of jog between rows, which is exactly enough to
   stop the column reading as a column. It is now in a fixed 14px gutter at
   the row's leading edge: every dot on the same vertical line, and the eye
   can run down it. */
/* The gutter is a property of the COLUMN, not of the cells that happen to be
   carrying a dot. The previous pass carved it out of .td-date alone — 16px of
   cell padding cut to 14 — which bought the dot 2px and cost the table its
   first column: every timestamp sat 2px left of the "TIME" header above it
   and 2px left of the day heads beside it, on read rows as much as unread
   ones. And 14px was never enough anyway: 4 + 6 + 4 left the dot four pixels
   off the card's own edge and four off the digits, so it read as a bullet
   glued to the wall rather than as a mark belonging to the row.

   28px, shared by the header cell and the day heads so the column stays one
   column: 10px of air, an 8px dot, 10px of air. Mail's proportions at this
   table's scale — the dot is comfortably inside the padding, clear of the
   border, and clear of the digits, and the eye can still run straight down
   the column of them. :has() keeps the indent on the calls tables; every
   other .data-table in the app has no .td-date and is untouched. */
.td-date {
  position: relative;
  padding-left: 28px !important;
  white-space: nowrap;
}
/* THE SAME GUTTER, PLUS AN OPTICAL CORRECTION (owner report 2026-08-09:
   "can you perfectly align the like 'yesterday' with the actual times like
   '3:40 PM'").
   The 28px above put the column header, the day heads and the timestamps on
   an identical left edge — measured 0.00px apart at the BOX level — and they
   still did not line up, because the three are set in three different faces.
   The header is Inter, the day heads are Inter Tight, the times are JetBrains
   Mono, and a monospace glyph is centred inside a fixed advance, so its ink
   starts a real distance right of its own box. Measured in Chrome at the 13px
   these cells render, across all twelve hour values: the digits' ink sits
   0.75–1.13px right of the box (mean 0.97). The sans capitals sit almost on
   it — 0.00px for the Y of YESTERDAY, 0.13 for TODAY's T, 0.38 for the
   header's T. So every label hung ~0.7px left of the column of digits it was
   labelling, which is exactly the misalignment that was reported.
   0.7px IS that difference of measured means — a side bearing being
   cancelled, not a nudge someone eyeballed. Remove it and the day heads go
   back to hanging left of their own column. It lands all four labels within
   0.15px of the digits, which is tighter than the mono column's own 0.38px
   glyph-to-glyph spread, so there is nothing left to win here.
   The alternative — setting the day heads in the mono face, which removes the
   mismatch at its source — was measured and rejected: monospace centring
   makes the per-letter bearing spread WORSE for letters than for digits (an M
   sits flush, an I or a 1 sits ~1px in), so it trades one systematic offset
   for a per-label lottery, and it turns a section divider into a code label. */
.data-table:has(.td-date) {
  --time-col-optical-comp: 0.7px;
}
.data-table:has(.td-date) th:first-child,
.data-table:has(.td-date) .day-group-head {
  padding-left: calc(28px + var(--time-col-optical-comp));
}
/* A sortable header is a <button> filling the cell, and the button carries
   its own 16px of padding — so the gutter landed on the CELL and the label
   started 16px further in than the day heads and the timestamps under it
   (measured: ink at 345.4 against 329.1 and 329.9 on /admin-calls, an
   unmissable step, where the client table's plain <th> was already flush).
   The gutter has to be spent INSIDE the button and given up by the cell:
   moving it the other way would leave 28px of header that looks clickable —
   th.sortable sets cursor:pointer — but sits outside the only element with
   the click handler on it. */
.data-table:has(.td-date) th:first-child.sortable { padding-left: 0; }
.data-table:has(.td-date) th:first-child .th-sort {
  padding-left: calc(28px + var(--time-col-optical-comp));
}
/* 8px, not 6: at 6 the dot was smaller than the punctuation in the timestamp
   beside it and read as a speck. Vertical centring is against the cell, which
   is also where the timestamp centres, so the two share a centre line whether
   the row is one line tall or two. Nothing animates it in — an indicator that
   is on screen from first paint on every unread row is not a place to spend
   motion, and the arrival shine already covers the one row that earns it. */
.new-dot {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}
/* Reading a call is not a state that should blink out of existence. */
.new-dot,
.call-row-new-dot,
.call-card-new-dot {
  transition: opacity 400ms var(--ease-out), transform 400ms var(--ease-out);
}
/* 0.8, not 0.4: a mark that collapses to two fifths of itself on the way out
   is the same shrink-to-nothing the greeting icon used to do on the way in.
   The opacity leg is what retires the dot; the scale only has to keep it from
   looking pinned in place while it goes. */
.new-dot.is-clearing,
.call-row-new-dot.is-clearing,
.call-card-new-dot.is-clearing {
  opacity: 0;
  transform: translateY(-50%) scale(0.8);
}
.call-row-new-dot.is-clearing,
.call-card-new-dot.is-clearing {
  transform: scale(0.8);
}
@media (prefers-reduced-motion: reduce) {
  .new-dot,
  .call-row-new-dot,
  .call-card-new-dot { transition: opacity 400ms linear; }
  .new-dot.is-clearing { transform: translateY(-50%); }
  .call-row-new-dot.is-clearing,
  .call-card-new-dot.is-clearing { transform: none; }
}

.call-card {
  position: relative;
}

/* ── Empty state, inside the card it replaces (audit D10/D11) ─────────
   The list's empty state dropped its container and floated on the bare
   page, so "no results" read as "the page failed to render a table" — and
   its icon was 24px inside a 1280px region. It keeps the surface the rows
   were on, and the icon is sized to the space it is holding. */
#calls-container > .empty-state,
#admin-calls-container > .empty-state {
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-md);
  padding: var(--space-10) var(--space-6);
}
#calls-container > .empty-state .empty-state-icon svg,
#calls-container > .empty-state .empty-state-icon i,
#admin-calls-container > .empty-state .empty-state-icon svg,
#admin-calls-container > .empty-state .empty-state-icon i {
  width: 44px;
  height: 44px;
  stroke-width: 1.5;
}

/* ── Filter Chips (independent toggles) ──── */
.filter-chips {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}

/* Chips sit shoulder-to-shoulder with .btn in every toolbar, so they share
   its corner (8px) and, since 2026-08-07, its 36px height — the 2px
   mismatch was small enough to read as a mistake rather than a variant.
   They keep the FILLED selected state on purpose: a chip is an
   independent on/off toggle, not a one-of-N segment, and a filter that is
   on should be unmissable. (iOS makes the same split — neutral thumb in a
   segmented control, tinted fill on a filter chip.) */
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--text-sm);
  font-weight: 500;
  padding: 6px var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text-secondary);
  cursor: pointer;
  transition-property: color, background, border-color, box-shadow, scale;
  transition-duration: var(--duration-normal);
  transition-timing-function: var(--ease-in-out);
  -webkit-tap-highlight-color: transparent;
  min-height: 36px;
  /* Chips carry counts that update live — fixed-width figures stop the
     label shifting under the pointer. */
  font-variant-numeric: tabular-nums;
}

.filter-chip:hover {
  border-color: var(--border-hover);
  color: var(--text-primary);
}

.filter-chip.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-inverse);
  font-weight: 600;
  /* Same cap lighting as the filled buttons — an active chip is a filled
     control and should read like one. */
  box-shadow: var(--shadow-btn-fill);
}

/* v55. tactile press feedback on filter chips.
   2026-08-07: on pointer-down, and at the family's 0.97. */
.filter-chip:active {
  scale: 0.97;
  transition-duration: var(--motion-press);
}

.filter-chip:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg-page), 0 0 0 4px var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .filter-chip:active { scale: 1; }
}

.filter-chip.active:hover {
  background: var(--accent-hover);
}

.filter-clear {
  font-size: var(--text-caption);
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px 8px;
  border: none;
  background: none;
  transition: color var(--duration-fast) ease;
}

.filter-clear:hover {
  color: var(--text-primary);
}

@media (max-width: 767px) {
  .filter-chips {
    width: 100%;
  }
  .filter-chip {
    flex: 1;
    justify-content: center;
    min-height: 44px;  /* HIG tap-target floor; was 38px */
  }
  /* The client calls row is two chips since "To review" was removed
     (2026-08-09), so flex:1 splits it exactly in half — which is the ask.
     Clear filters is not a chip and must not eat one of those halves when
     it appears, so it takes a full line of its own underneath. */
  .calls-toolbar .filter-chips .filter-clear {
    flex: 1 0 100%;
    /* It is the way out of a zero-results state, which is exactly when the
       thumb is least accurate, and it was ~26px tall. Same 44px floor as the
       chips above it; the flex centring replaces text-align. */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 12px;
  }
}

/* ============================================
   v50 NEW FEATURES
   ============================================ */

/* ── Pull to Refresh ──────────────────────── */
.ptr-indicator {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translate(-50%, -60px);
  z-index: var(--z-overlay);
  opacity: 0;
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
  pointer-events: none;
}

.ptr-spinner {
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-card-solid, var(--bg-card));
  border: 1px solid var(--border);
  border-radius: 50%;
  box-shadow: var(--shadow-md);
  color: var(--text-muted);
  transition: color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}

.ptr-ready .ptr-spinner {
  color: var(--accent);
  border-color: var(--accent);
}

.ptr-refreshing .ptr-spinner {
  animation: ptr-spin 0.7s linear infinite;
}

@keyframes ptr-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* ── Follow-up Indicator ──────────────────────────────────────────────
   VF-5 (owner 2026-07-03): the amber left-stripes are gone. On table rows
   the amber Follow-up pill + amber status ring carry the state entirely.
   On mobile cards an even, quiet amber tint replaces the stripe+gradient;
   viewed follow-ups recede to the plain card surface. */
.call-card--follow-up {
  background: rgba(var(--warning-rgb), 0.05);
}
.call-card--follow-up.call-card--follow-up-viewed {
  background: var(--bg-card);
}

.pill-follow-up { --chip-hue: var(--warning); color: var(--pill-warning-fg); }
/* Size the swapped lucide svg in CSS — the markup's inline style does not
   reliably survive the <i> -> <svg> swap (see lucide-icon-sizing note), which
   left the icon invisible/oversized on some loads. Flex-center handles the
   vertical alignment; no nudges. */
.pill-follow-up i,
.pill-follow-up svg { width: 11px; height: 11px; flex-shrink: 0; }
.pill-follow-up--muted { opacity: 0.45; }

/* Tool/booking failure (2026-08-07) — AGENCY ADMIN ONLY, never rendered on a
   client surface. Same shape as the follow-up tag one tier louder: danger
   tokens carry both themes, and the svg is sized here for the same reason
   (the inline style does not survive the lucide <i> -> <svg> swap).

   The heavier edge is not decoration. .pill.sentiment-negative is also
   danger-hued, and the two can land in the same pills row on one call — a
   booking that failed for a caller who was unhappy about it. Once the chip
   recipe derived every hairline from --chip-edge, the 1px border that used
   to tell them apart became the same 30% both were already getting, and
   "the tool failed" started looking exactly like "the caller was annoyed".
   Those are not the same fact and the operator acts on them differently, so
   failure keeps a defined edge while sentiment keeps the derived one. The
   hue is untouched — both are genuinely danger; only the weight separates
   them. */
.pill-failure {
  --chip-hue: var(--danger);
  --chip-edge: 62%;
  color: var(--pill-danger-fg);
}
.pill-failure i,
.pill-failure svg { width: 11px; height: 11px; flex-shrink: 0; }

/* Desktop table alignment. tabular-numeric cells never wrap; Client cell
   truncates with ellipsis so long org names don't push the row onto a
   second line. The title attribute on the cell surfaces the full name
   on hover for admin users scanning long lists. */
.data-table .mono { white-space: nowrap; }
.data-table .td-client {
  max-width: 260px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The dashed "Earlier" separator that lived here was removed 2026-08-08 with
   its render paths in js/call-table.js — see the note there. Short version:
   it predates day grouping, it was spliced in at a flat index so it landed
   INSIDE a day group rather than between two, and the per-row unread dot
   already says the thing it was saying. */

/* ── Analytics KPI Row ─────────────────────── */
.analytics-kpi-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

/* Admin variant. 4 KPI cards stack vertically on mobile so the numbers
   stay readable. Desktop/tablet: inherits the 2-col base (wraps to 2x2). */
@media (max-width: 767px) {
  .analytics-kpi-row.is-stacked-mobile {
    grid-template-columns: 1fr;
  }
}

/* Full-width row below analytics-kpi-row. houses the Appointments Booked
   hero on admin-analytics. Standalone container keeps the hero's grid-cell
   sizing clean without affecting neighbouring KPI cards. */
.analytics-hero-row {
  display: grid;
  grid-template-columns: 1fr;
  margin-bottom: var(--space-6);
}

/* Analytics hero. same glow effects as overview hero but compact sizing */
.analytics-kpi-hero.kpi-hero {
  grid-column: auto;
  padding: var(--space-4);
  min-height: auto;
}
.analytics-kpi-hero.kpi-hero .kpi-value {
  font-size: 2.35rem;
}
@media (max-width: 767px) {
  .analytics-kpi-hero.kpi-hero {
    padding: var(--space-3) var(--space-4);
  }
  .analytics-kpi-hero.kpi-hero .kpi-value {
    font-size: 1.8rem;
  }
  .analytics-kpi-row .kpi-card {
    min-height: auto;
    padding: var(--space-3) var(--space-4);
  }
  .analytics-kpi-row .kpi-card .kpi-value {
    font-size: 1.6rem;
  }
  /* Visual-centroid compression on mobile: shrink the period-selector + KPI
     band so the first chart lives closer to the fold. Tier 1 audit fix.
     NOTE: the old `.chart-grid .chart-full:first-of-type { padding-top: 0 }`
     rule matched the Booking rate card (chart-card.chart-full), killing its
     own internal top padding. Removed. the kpi-row margin below already
     handles the centroid compression without touching card internals. */
  .analytics-kpi-row {
    margin-bottom: var(--space-3);
    gap: var(--space-3);
  }
  .chart-grid > .chart-full > .chart-section-heading {
    font-size: 16px;
    margin-top: var(--space-2);
  }
}

/* ── KPI Subtitle (billing cycle label) ────── */
.kpi-subtitle {
  font-size: var(--text-overline);
  color: var(--text-muted);
  margin-top: calc(-1 * var(--space-1));
  margin-bottom: var(--space-1);
}

/* ── Billing cycle label (client dashboard) ── */
.billing-cycle-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-weight: 500;
}

/* ============================================
   NOTIFICATION SETTINGS MODAL
   ============================================ */

#notif-settings-modal {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.notif-settings-sheet {
  background: var(--glass-surface-bg);
  border: var(--glass-surface-border);
  backdrop-filter: var(--glass-surface-filter);
  -webkit-backdrop-filter: var(--glass-surface-filter);
  border-radius: var(--radius-xl);
  width: 100%;
  max-width: 400px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: var(--glass-surface-shadow);
  animation: notifSlideUp var(--motion-dialog-enter) var(--ease-out) forwards;
}

@keyframes notifSlideUp {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

.notif-settings-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 24px 16px;
  border-bottom: 1px solid var(--border);
}

.notif-settings-header h3 {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
}

/* .notif-close-btn retired to .btn.btn-ghost.btn-icon; class kept in markup
   as the JS bind hook (sidebar.js focus + click handlers). */

.notif-settings-body {
  padding: 20px 24px 24px;
}

.notif-settings-loading {
  display: flex;
  justify-content: center;
  padding: 32px 0;
}

/* Toggle row */
.notif-toggle-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
}

.notif-toggle-title {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 4px;
}

.notif-toggle-desc {
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.4;
}

/* Toggle switch */
.notif-switch {
  position: relative;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
  margin-top: 2px;
}

.notif-switch input { opacity: 0; width: 0; height: 0; }

.notif-slider {
  position: absolute;
  inset: 0;
  background: var(--bg-elevated);
  border-radius: 12px;
  cursor: pointer;
  transition: background var(--duration-normal) ease;
}

.notif-slider::before {
  content: '';
  position: absolute;
  width: 18px;
  height: 18px;
  left: 3px;
  top: 3px;
  background: #fff;
  border-radius: 50%;
  transition: transform var(--duration-normal) ease;
}

.notif-switch input:checked + .notif-slider {
  background: var(--accent);
}

.notif-switch input:checked + .notif-slider::before {
  transform: translateX(20px);
}

/* ── Calendar Integration section (admin-clients.html) ──────────────
   Reuses .notif-switch for the toggle. The fields block uses a
   max-height transition that respects prefers-reduced-motion via the
   global rule. Styling matches the existing card / form conventions. */
.calendar-config-card { margin-bottom: 0; }

.calendar-config-toggle-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) 0;
}

.calendar-config-toggle-info { flex: 1; min-width: 0; }

/* v61 fix: credential fields always visible inside an expanded integration
   panel. Previously display:none hid them unless the "Enable calendar view"
   toggle was on, making it impossible to paste creds or run Test Connection
   before enabling. The Enable toggle now strictly controls whether the
   client sees a Calendar tab. it no longer gates the admin's ability to
   inspect or edit the connection. The legacy `.visible` class is kept as a
   no-op so existing JS keeps working. */
.calendar-config-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.calendar-config-fields.visible { display: flex; }

.calendar-config-secret-row {
  display: flex;
  gap: var(--space-2);
  align-items: stretch;
}
.calendar-config-secret-row .form-input { flex: 1; }

.calendar-config-reveal {
  padding: 0 var(--space-3);
  min-width: 40px;
}

.calendar-config-test-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.calendar-config-test-status {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 20px;
}
.calendar-config-test-status.success { color: var(--pill-success-fg); }
.calendar-config-test-status.error   { color: var(--pill-danger-fg); }
.calendar-config-test-status i,
.calendar-config-test-status svg { width: 14px; height: 14px; }

.calendar-config-sync-status {
  font-size: var(--text-caption);
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.calendar-config-sync-status .status-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--text-muted);
}
.calendar-config-sync-status.success .status-dot { background: var(--success); }
.calendar-config-sync-status.error   .status-dot { background: var(--danger); }
.calendar-config-sync-status.pending .status-dot { background: var(--warning); }

/* v57.1. Simplistic UI toggle card. Minimal row layout: title + short
   description on left, toggle on right. Status line below, only visible
   when flag is ON (persistent pulsing dot) or when an error occurred. */
.simplistic-ui-card {
  padding: var(--space-4) var(--space-5);
}
.simplistic-ui-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}
.simplistic-ui-info {
  min-width: 0;
  flex: 1;
}
.simplistic-ui-title {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  line-height: 1.25;
}
.simplistic-ui-desc {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: 2px;
  line-height: 1.4;
}
.simplistic-ui-status {
  margin-top: var(--space-3);
  font-size: var(--text-caption);
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.simplistic-ui-status .status-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--success);
  box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.55);
  animation: simplisticPulse 1.8s ease-in-out infinite;
  flex-shrink: 0;
}
@keyframes simplisticPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.55); }
  50%      { box-shadow: 0 0 0 5px rgba(52, 211, 153, 0);   }
}
@media (prefers-reduced-motion: reduce) {
  .simplistic-ui-status .status-dot { animation: none; }
}
.simplistic-ui-status[data-kind="error"] .status-dot {
  background: var(--danger);
  animation: none;
  box-shadow: none;
}
.simplistic-ui-status[data-kind="error"] .simplistic-ui-status-text { color: var(--pill-danger-fg); }

.calendar-config-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

/* Device state */
.notif-device-section {
  margin-bottom: 20px;
}

.notif-device-state {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-sm);
  padding: 10px 14px;
  border-radius: var(--radius-md);
}

.notif-device-state i,
.notif-device-state svg { width: 16px; height: 16px; flex-shrink: 0; }

.notif-state-ok {
  color: var(--pill-success-fg);
  background: var(--success-muted);
}

.notif-state-warn {
  color: var(--pill-warning-fg);
  background: var(--warning-muted);
}

.notif-link-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 16px;
  font-size: var(--text-body);
  font-weight: 600;
  color: #fff;
  background: var(--gradient-brand-primary);
  border: none;
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: box-shadow var(--duration-normal) ease;
}

.notif-link-btn:hover {
  box-shadow: 0 0 24px rgba(99, 102, 241, 0.3);
}

.notif-link-btn i,
.notif-link-btn svg { width: 18px; height: 18px; }

/* Agency section */
.notif-section {
  margin-bottom: 20px;
}

.notif-section-label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 10px;
}

.notif-radio {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-body);
  color: var(--text-secondary);
  padding: 6px 0;
  cursor: pointer;
}

.notif-client-list {
  margin-top: 10px;
  max-height: 200px;
  overflow-y: auto;
  padding: 8px 12px;
  background: var(--bg-sunken);
  border-radius: var(--radius-md);
}

.notif-client-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  padding: 6px 0;
  cursor: pointer;
}

/* Actions */
.notif-actions {
  padding-top: 4px;
}

.notif-test-btn {
  width: 100%;
  padding: 10px 16px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: border-color var(--duration-normal) ease, color var(--duration-normal) ease;
}

.notif-test-btn:hover {
  border-color: var(--border-hover);
  color: var(--text-primary);
}

/* Sidebar notification button (matches theme-toggle) */
.sidebar-notif-settings {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  background: none;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: color var(--duration-normal) ease, background var(--duration-normal) ease;
}

.sidebar-notif-settings:hover {
  color: var(--text-primary);
  background: rgba(var(--accent-rgb), 0.06);
}

.sidebar-notif-settings i,
.sidebar-notif-settings svg { width: 16px; height: 16px; }

/* ── Notification discovery prompt (dashboard top) ──────────────── */
/* Specificity fix: `.notif-prompt-card` (0,1,0) below has `display: flex`,
   which ties with the browser-default `[hidden] { display: none }`. In a
   tie, author CSS wins → the `hidden` HTML attribute was a no-op and the
   banner rendered on every page load, even when the IIFE's dismiss gates
   had returned early. This selector has specificity (0,2,0). beats the
   base rule. so `hidden` actually hides. */
.notif-prompt-card[hidden] { display: none; }
.notif-prompt-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-4);
  background: var(--bg-card);
  border: 1px solid rgba(var(--accent-rgb), 0.16);
  border-radius: var(--radius-lg);
  box-shadow: var(--ring-shadow);
  overflow: hidden;
  animation: notifPromptIn 0.4s var(--ease-out) both;
  /* Belt-and-suspenders with data-no-ptr: tell the browser this card's
     touches are taps, not pan gestures. Prevents the pull-to-refresh
     handler from cancelling the click after a few pixels of finger drift. */
  touch-action: manipulation;
}
.notif-prompt-card .btn {
  touch-action: manipulation;
}

.notif-prompt-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.06) 0%, transparent 55%);
  pointer-events: none;
  border-radius: inherit;
}

.notif-prompt-card > * { position: relative; z-index: 1; }

.notif-prompt-icon {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(var(--accent-rgb), 0.12);
  border: 1px solid rgba(var(--accent-rgb), 0.18);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
}
.notif-prompt-icon i,
.notif-prompt-icon svg { width: 18px; height: 18px; }

.notif-prompt-body { flex: 1; min-width: 0; }
.notif-prompt-title {
  font-family: var(--font-display);
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
.notif-prompt-desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-top: 2px;
}

.notif-prompt-actions {
  display: flex;
  gap: var(--space-2);
  flex-shrink: 0;
}

/* VF-8 (owner 2026-07-03): the banner offers exactly two affordances —
   "Turn on" and "Not now". The X close button was removed; both dismiss
   paths were already the same 7-day snooze, so "Not now" carries it. */

/* ── Realtime arrival choreography (v56) ──────────────────────────────────
   Two-stage sequence when a live call lands via the realtime subscription:
   (1) `.is-arriving` plays a 300ms enter. slide from -12px translateY,
       opacity 0→1, scale 0.96→1, filter blur(2px)→0. One-shot keyframe
       (correct use. this runs once on prepend, not on interactive state).
   (2) `.is-fresh` overlaps/follows and fades an accent tint for ~2s.
   Together: the row feels like it "arrives" rather than just materializes.
   Respects prefers-reduced-motion. */
.is-arriving {
  animation: callArrive 300ms cubic-bezier(0.2, 0, 0, 1) forwards;
}
@keyframes callArrive {
  from {
    opacity: 0;
    transform: translateY(-12px) scale(0.96);
    filter: blur(2px);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
    filter: blur(0);
  }
}

/* ── Fresh-highlight for realtime-inserted rows / cards ────────────
   Applied by js/realtime.js on newly-prepended rows and cards. Fades
   over ~2s using the existing --ease-out token. */
.is-fresh {
  animation: freshRowFade 2s var(--ease-out) forwards;
}
.call-card.is-fresh {
  animation: freshCardFade 2s var(--ease-out) forwards;
}

@keyframes freshRowFade {
  from { background-color: rgba(var(--accent-rgb), 0.10); }
  to   { background-color: transparent; }
}
@keyframes freshCardFade {
  from { box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.25), 0 0 0 rgba(0,0,0,0); background-color: rgba(var(--accent-rgb), 0.06); }
  to   { box-shadow: none; background-color: transparent; }
}

/* Allow .is-arriving + .is-fresh to stack: .is-arriving owns the transform
   chain during its 300ms, .is-fresh kicks in for the background tint after.
   Both animations are on different properties so they can run in parallel.
   The animation shorthand in .is-fresh gets replaced if .is-arriving is
   present because both set `animation`; prefer setting .is-fresh AFTER
   .is-arriving ends in JS to avoid the override. See realtime.js. */

@media (prefers-reduced-motion: reduce) {
  .is-fresh, .call-card.is-fresh, .is-arriving { animation: none; }
}

/* ── Caller issue preview (desktop table, under caller name) ──── */
.caller-issue-preview {
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin-top: 3px;
  max-width: 340px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.3;
}

.notif-prompt-card.is-leaving {
  animation: notifPromptOut 0.2s var(--ease-out) forwards;
  pointer-events: none;
}

/* Transform-only: an opacity-0 from-state on a mount animation strands the
   element invisible whenever the animation is running with a frozen clock
   (a document that has not had a rendering opportunity — background tab,
   headless/print renderer). Fill mode does NOT save it: the animation is
   active rather than pending, so `none` paints the from-state exactly as
   `both` does. Same class af4b741 removed from cardEnter. */
@keyframes notifPromptIn {
  from { transform: translateY(-8px); }
  to   { transform: translateY(0); }
}

@keyframes notifPromptOut {
  from {
    opacity: 1;
    transform: translateY(0);
    max-height: 140px;
    margin-bottom: var(--space-4);
    padding-top: var(--space-4);
    padding-bottom: var(--space-4);
  }
  to {
    opacity: 0;
    transform: translateY(-4px);
    max-height: 0;
    margin-bottom: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
  }
}

@media (max-width: 640px) {
  .notif-prompt-card {
    flex-wrap: wrap;
  }
  .notif-prompt-body { flex-basis: 100%; }
  .notif-prompt-actions {
    width: 100%;
  }
  .notif-prompt-actions .btn { flex: 1; }
}

/* ────────────────────────────────────────────────────────────────────────
   Download split-menu (admin-calls: CSV of calls + MD of transcripts)
   Visibility driven by .is-open on the wrapper, not [hidden] on the menu
  . keeps the positioning math out of attribute-vs-CSS precedence wars.
   ──────────────────────────────────────────────────────────────────────── */
.download-menu-wrap {
  position: relative;
  display: inline-flex;
}
.download-menu-wrap .download-caret {
  width: 14px;
  height: 14px;
  margin-left: 2px;
  opacity: 0.7;
  transition: transform 150ms var(--ease-out);
}
.download-menu-wrap.is-open .download-caret {
  transform: rotate(180deg);
}
.download-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: var(--z-dropdown);
  min-width: 300px;
  background: var(--bg-card-solid);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 4px;
  display: none;
}
.download-menu-wrap.is-open .download-menu {
  display: block;
  animation: downloadMenuIn var(--motion-menu) var(--ease-out);
}
@keyframes downloadMenuIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0);    }
}
.download-menu-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  padding: 10px 12px;
  background: none;
  border: none;
  border-radius: var(--radius-md);
  color: var(--text-primary);
  cursor: pointer;
  text-align: left;
  transition: background var(--motion-menu) var(--ease-out);
  font-family: inherit;
}
.download-menu-item + .download-menu-item {
  margin-top: 2px;
}
.download-menu-item:hover,
.download-menu-item:focus-visible {
  background: var(--bg-card-hover);
  outline: none;
}
.download-menu-item > i[data-lucide],
.download-menu-item > svg {
  width: 18px;
  height: 18px;
  color: var(--accent);
  flex-shrink: 0;
  margin-top: 1px;
}
.download-menu-item-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.download-menu-item-title {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  line-height: 1.3;
}
.download-menu-item-desc {
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.3;
}
@media (max-width: 767px) {
  .download-menu {
    min-width: calc(100vw - 48px);
    right: 0;
  }
}

/* ════════════════════════════════════════════════════════════════════════
   v59. Admin overview redesign
   Visual refinement only. no new sections, no new metrics. Pulls the
   admin overview up to the client overview's visual quality.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Greeting bar right-side date eyebrow ─────────────────────────────
   Right-aligned overline next to the greeting text. Fills the space the
   admin page previously left empty. Hidden on narrow mobile where the
   greeting itself wraps. */
.greeting-eyebrow-date {
  font-family: var(--font-display);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  text-transform: uppercase;
  margin-left: auto;
  white-space: nowrap;
  flex-shrink: 0;
}
@media (max-width: 767px) {
  .greeting-eyebrow-date { display: none; }
}

/* ── AD-5 · Admin overview greeting demoted (taste 4.1 A) ─────────────
   On the admin overview the greeting is not the information — the client
   cards are. Drop it from hero-h1 to a compact --text-secondary header row
   so the cards own the top visual slot. Client dashboard keeps the greeting
   as hero (it uses .greeting-bar without this modifier). */
.greeting-bar--compact {
  padding-bottom: var(--space-3);
}
.greeting-bar--compact h1 {
  font-family: var(--font-body);
  font-size: var(--text-h2);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
}
@media (max-width: 767px) {
  .greeting-bar--compact h1 { font-size: var(--text-h2); }
}

/* ── Admin section shell ───────────────────────────────────────────────
   Replacement for .section on admin.html: tighter top margin (space-6,
   matching client overview's rhythm. admin previously used space-8 which
   felt stretched). Every section header carries an eyebrow over the title
   so the page has consistent hierarchy instead of mixed h2/h3 treatments. */
.admin-section {
  margin-top: var(--space-6);
}
.admin-section:first-of-type {
  margin-top: var(--space-4);
}
.admin-section-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
/* Shared section eyebrow / kicker (S11 · 2026-07-04). The small-caps label
   that sits above a section title. Was rewritten per feature (admin overview,
   test-deploy hero, chart sections); this is the one canonical class. Type
   spec matches the primary admin eyebrow's computed style. */
.section-kicker {
  font-family: var(--font-display);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  text-transform: uppercase;
}
/* Composes .section-kicker for type; keeps only its layout context. */
.admin-section-eyebrow {
  flex-basis: 100%;
  margin-bottom: -2px;      /* tuck tight against the title below */
  line-height: 1;
}
.admin-section-head .section-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.375rem;      /* between h2 and h3. the right weight for a
                               command-center section, not a page title */
  letter-spacing: -0.01em;
  line-height: 1.2;
  color: var(--text-primary);
}
.admin-section-link {
  margin-left: auto;
  font-size: var(--text-sm);
  color: var(--text-muted);
  transition: color 150ms ease;
}
.admin-section-link:hover { color: var(--text-primary); }

/* Keep on-topic controls (segmented tabs) from stretching across the row.
   Pushes right into the same baseline as .admin-section-link. */
.admin-section-head .admin-feedback-tabs {
  margin-left: auto;
}

/* ── KPI card cluster (v59.4) ──────────────────────────────────────────
   Desktop tuning on top of v59.3's correct vertical-pair alignment.
   Mobile is preserved exactly (user said mobile is clean, don't regress).

   Summary:
     - Hero: min-height 104px (was 128), padding 16/20 (was 20/24)
     - Stat cards: min-height 88px (was 108), padding 12/16 (was 16/20)
     - Ring card: desktop restructured. ring is the LEFT anchor at 88×88;
       label sits on the right, vertically centered against the ring.
       Icon disc hidden on desktop. Mobile keeps its icon-disc + content-
       with-inline-ring layout.
     - Sparkline: min-height 100px, justify-content removed so the head +
       canvas stack tight at the top instead of spreading to corners.

   All desktop changes are either in the base rules (which apply to
   everyone but get overridden for mobile in the @media max-width: 767px
   block below) or in the explicit @media (min-width: 768px) scope. */

/* Stat + ring cards. 2-col grid baseline (mobile layout).
   Ring card's desktop override replaces "icon content" with "ring content". */
.kpi-grid-admin .kpi-card--stat,
.kpi-grid-admin .kpi-card--ring {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "icon content";
  align-items: center;
  gap: var(--space-3);
  min-height: 88px;
  padding: var(--space-3) var(--space-4);
}
.kpi-grid-admin .kpi-card--stat > .kpi-icon-disc,
.kpi-grid-admin .kpi-card--ring > .kpi-icon-disc { grid-area: icon; }
.kpi-grid-admin .kpi-card--stat > .kpi-content,
.kpi-grid-admin .kpi-card--ring > .kpi-content { grid-area: content; }

.kpi-grid-admin .kpi-card.kpi-hero {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-3);
  min-height: 104px;
  padding: var(--space-4) var(--space-5);
}

/* Sparkline card. flex-column, no space-between so head + canvas stack
   tight at the top (v59.3 bug: space-between pushed canvas to the bottom
   leaving visual dead zone between). gap: space-2 for breathing room. */
.kpi-grid-admin .kpi-card--sparkline {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
  min-height: 100px;
  padding: var(--space-3) var(--space-4);
}

/* Content column. tight vertical stack of label + value (or label + ring
   on the ring card's mobile layout). */
.kpi-content {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

/* Icon disc. left anchor. Accent-muted tint. */
.kpi-icon-disc {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-full);
  background: var(--accent-muted);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.kpi-icon-disc > i,
.kpi-icon-disc > svg {
  width: 20px;
  height: 20px;
}
.kpi-hero-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  background: rgba(var(--accent-rgb), 0.18);
  color: var(--accent);
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}
.kpi-hero-icon > i,
.kpi-hero-icon > svg {
  width: 18px;
  height: 18px;
}

/* Label. Muted, uppercase, tracked — the overline role, and casing is CSS's
   job, not the string's. It used to be sentence case here while the admin
   analytics deck hardcoded UPPERCASE literals in its markup and then scoped a
   text-transform to two ID'd rows to make its own two views agree, so the app
   shipped two KPI label styles and one page shipped both. That page-scoped
   rule is promoted here: one casing for every KPI label in the app. The
   tracking is repeated from .kpi-label because kpi-cards.js renders some
   labels as a bare .kpi-label-text with no .kpi-label around them. */
.kpi-label-text {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  line-height: 1.2;
  min-width: 0;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Value. mono, tabular-nums, left-aligned within content column. */
.kpi-grid-admin .kpi-card .kpi-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  line-height: 1;
  letter-spacing: -0.015em;
  color: var(--text-primary);
}
.kpi-grid-admin .kpi-card--stat .kpi-value {
  font-size: 2rem;
  font-weight: 700;
}
.kpi-grid-admin .kpi-card.kpi-hero .kpi-value {
  font-size: 3.25rem;
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.025em;
  position: relative;
  z-index: 1;
}

/* Ring anchor (desktop, 88px left-column). default HIDDEN; only shown via
   the @media (min-width: 768px) block below. */
.kpi-ring--anchor {
  display: none;
}
/* Ring inline (mobile, inside content column, smaller). visible by default. */
.kpi-ring--inline {
  width: 48px;
  height: 48px;
  position: relative;
  display: inline-block;
  flex-shrink: 0;
}
.kpi-ring--inline > svg,
.kpi-ring--anchor > svg { width: 100%; height: 100%; }
.kpi-ring--inline > span,
.kpi-ring--anchor > span { inset: 0; }

/* Sparkline internal layout */
.kpi-card--sparkline .kpi-spark-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.kpi-card--sparkline .kpi-spark-icon {
  width: 20px;
  height: 20px;
  color: var(--text-muted);
  flex-shrink: 0;
}
.kpi-card--sparkline .kpi-sparkline {
  width: 100%;
  height: 56px;
  margin: 0;
  flex-shrink: 0;
}
.kpi-card--sparkline .kpi-sparkline canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* ── Desktop (≥768px): ring-card layout switches; icon disc hidden ────── */
@media (min-width: 768px) {
  .kpi-card--ring {
    grid-template-areas: "ring content" !important;
  }
  .kpi-card--ring > .kpi-icon-disc { display: none; }
  .kpi-card--ring > .kpi-ring--anchor {
    display: inline-block;
    grid-area: ring;
    align-self: center;
    width: 88px;
    height: 88px;
    position: relative;
    flex-shrink: 0;
  }
  .kpi-card--ring .kpi-ring--inline { display: none; }
  /* Ring card needs ~112px to fit 88px ring + padding. Stat cards in the
     same row will stretch to match; that's fine. keeps the row uniform. */
  .kpi-grid-admin .kpi-card--ring {
    min-height: 112px;
  }
}

/* ── Mobile (≤767px): preserved from v59.3 exactly ─────────────────────── */
@media (max-width: 767px) {
  .kpi-grid-admin {
    grid-template-columns: 1fr;
  }
  .kpi-grid-admin > .kpi-card.kpi-hero:not(.analytics-kpi-hero) {
    grid-column: auto;
  }
  .kpi-grid-admin .kpi-card--stat,
  .kpi-grid-admin .kpi-card--ring,
  .kpi-grid-admin .kpi-card.kpi-hero {
    min-height: 96px;
    padding: var(--space-4);
    gap: var(--space-3);
  }
  .kpi-grid-admin .kpi-card.kpi-hero {
    min-height: 112px;
  }
  .kpi-grid-admin .kpi-card--stat .kpi-value { font-size: 1.875rem; }
  .kpi-grid-admin .kpi-card.kpi-hero .kpi-value { font-size: 2.75rem; }
  .kpi-icon-disc { width: 36px; height: 36px; }
  .kpi-icon-disc > i, .kpi-icon-disc > svg { width: 18px; height: 18px; }
  .kpi-hero-icon { width: 30px; height: 30px; }
  .kpi-hero-icon > i, .kpi-hero-icon > svg { width: 16px; height: 16px; }
  .kpi-grid-admin .kpi-card--sparkline {
    min-height: 96px;
    padding: var(--space-4);
  }
  .kpi-card--sparkline .kpi-sparkline { height: 44px; }
}

/* ── Client Overview grid (step 5 + step 8 CSS extraction) ─────────────
   Moved out of the inline <style> block in admin.html and onto the canonical
   4pt spacing scale (gap was 28px. off-grid). Carousel logic on mobile
   unchanged. */
.client-health-grid {
  display: grid;
  gap: var(--space-6);      /* was 28px (off-grid). 24px is on the 4pt scale. */
  grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 1023px) {
  .client-health-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (max-width: 767px) {
  .client-health-grid {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    gap: var(--space-4);
    margin-left: calc(-1 * var(--space-4));
    margin-right: calc(-1 * var(--space-4));
    padding: 0 var(--space-4);
    scrollbar-width: none;
  }
  .client-health-grid > .card {
    min-width: calc(100vw - 32px);
    max-width: calc(100vw - 32px);
    flex-shrink: 0;
    scroll-snap-align: center;
  }
  .client-health-grid::-webkit-scrollbar { display: none; }
}
.carousel-dots {
  display: none;
  justify-content: center;
  gap: 6px;
  padding: var(--space-3) 0 0;
}
.carousel-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--border-hover);
  border: none;
  cursor: pointer;
  padding: 0;
  transition: background 200ms ease, transform 200ms ease;
}
.carousel-dot.active {
  background: var(--accent);
  transform: scale(1.3);
}
@media (max-width: 767px) {
  .carousel-dots { display: flex; }
}

/* ── Admin client card. lit treatment + tiered internal typography ───
   Matches the visual weight of .kpi-hero and .admin-recent-card so the three
   lit surfaces read as one visual family across the page. */
.admin-client-card {
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.admin-client-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}
.admin-client-card-identity {
  min-width: 0;
  overflow: hidden;
}
.admin-client-card-name {
  font-family: var(--font-display);
  font-size: 0.9375rem;           /* 15px. display-weight primary */
  font-weight: 700;
  color: var(--text-primary);
  letter-spacing: -0.005em;
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.admin-client-card-meta {
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.admin-client-card-stats {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.admin-client-card-stats strong {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--text-primary);
  font-size: var(--text-body);
}
.admin-client-card-sep {
  color: var(--text-muted);
  opacity: 0.5;
}
.admin-client-card-rate {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-1);
}
.admin-client-card-rate-label {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-weight: 500;
}
.admin-client-card-rate-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}
.admin-client-card-bar {
  margin-top: 2px;
  margin-bottom: var(--space-1);
}
.admin-client-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
}
.admin-client-card-last {
  font-size: var(--text-caption);
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: var(--space-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Recent calls wrapping card (step 6) ──────────────────────────────
   Caps the table with a lit surface + subtle internal padding so the
   section reads as "here's a curated slice" instead of "here's the table".
   The underlying renderCalls() markup is unchanged; this is pure wrapping.
   L-2 (2026-05-16): bumped padding from --space-2 (8px) to --space-5 (20px)
   so this card matches the dominant 20px card-padding token used by
   .card, .kpi-card, .admin-client-card. Inner row padding below is
   untouched so row density stays the same. */
.admin-recent-card {
  padding: var(--space-5);
}
.admin-recent-card > #recent-calls > .table-container {
  box-shadow: none;
  border: none;
  background: transparent;
  border-radius: 0;
}
/* Nudge table row vertical padding so rows don't press against the card
   edges. Affects only admin overview's Recent calls (not the full admin
   calls page which uses the standalone .data-table). */
.admin-recent-card .data-table th,
.admin-recent-card .data-table td {
  padding-left: var(--space-3);
  padding-right: var(--space-3);
}
/* Separator inset follows the cell padding it belongs to. */
.admin-recent-card .data-table { --table-sep-inset: var(--space-3); }

/* ── Feedback section (step 7) ────────────────────────────────────────
   Tabs migrated from bespoke .feedback-tab to the canonical .seg-btn
   inside a .segmented container. Filter dropdown + Download button moved
   to a secondary row below the section header so the header stays clean. */
.admin-feedback-tabs {
  flex-shrink: 0;
}
.admin-feedback-filterbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}
.admin-feedback-select {
  min-width: 220px;
  flex: 0 1 260px;
}
@media (max-width: 767px) {
  .admin-feedback-select {
    flex: 1 1 auto;
    min-width: 0;
  }
  .admin-feedback-filterbar {
    gap: var(--space-2);
  }
}

/* Feedback cards carry their rating through the star row alone — the
   rating-colored left edge was removed in the VF-5 side-stripe sweep
   (owner 2026-07-03). data-rating stays on the markup as metadata. */

/* Caller column. with the issue-preview subtitle removed, the cell holds
   only the phone number (and an optional muted phone line under a name).
   Centering reads tighter in the trimmed column width. `white-space: nowrap`
   prevents a narrow admin column (extra Client column eats width) from
   wrapping a formatted phone number mid-digit. No min-width. the column
   auto-sizes to content so Chrome zoom doesn't force horizontal scroll
   earlier than necessary. */
/* LEFT, like every other column (audit B2). Caller was the one centred
   column in the table, so its ragged edges cut across the left rag every
   neighbouring column reads on — the eye had nothing to run down. */
.data-table .th-caller,
.data-table .td-caller {
  text-align: left;
  white-space: nowrap;
}
.data-table .td-caller .font-medium,
.data-table .td-caller .text-xs {
  white-space: nowrap;
}

/* ── Direction inside the Caller cell (/admin-calls, 2026-08-16) ────────
   The admin calls table gave up its Direction column so SCORE could fit on a
   1280 laptop (see _callColumnModel in js/call-table.js); the glyph moves in
   front of the caller, which is where the mobile card has always drawn it.
   Muted, because it is the least load-bearing thing in the cell — the name is
   what the eye is here for — and one size under the name so the two do not
   compete. Lucide REPLACES the <i> with an <svg> at runtime, so both selectors
   have to carry the size. */
.data-table .td-caller-dir {
  display: inline-block;
  vertical-align: -2px;
  margin-right: var(--space-2);
  color: var(--text-muted);
}
.data-table .td-caller-dir i,
.data-table .td-caller-dir svg {
  display: block;
  width: 13px;
  height: 13px;
}
/* A fixed column can be narrower than the name in it, and `.data-table--fixed`
   puts the ellipsis on the CELL — which does nothing here, because what
   overflows is the nowrap <div> inside it, so an over-long name was cut
   mid-glyph. Both classes together are the /admin-calls table alone; the
   client table sizes this column at 28-34% and is left as it is. */
.data-table--admin.data-table--fixed .td-caller .font-medium,
.data-table--admin.data-table--fixed .td-caller .text-xs {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ═══════════════════════════════════════════════════════════════
   Motion Contract. Theme 1 (v63-polished)
   Use semantic tokens, not raw durations. One source of truth.

   - Hover / press state: `--motion-hover` (100ms) + `--ease-out`
   - Tactile press scale: `--motion-press` (80ms) + `--ease-out`
   - Popover / dropdown enter: `--motion-popover` (180ms) +
     `--ease-spring-soft` (gentle overshoot)
   - Modal / sheet enter: `--motion-dialog-enter` (220ms) +
     `--ease-spring-out` (full overshoot)
   - Modal / sheet exit: `--motion-dialog-exit` (180ms) + `--ease-in`
   - Layout shift (panel resize, accordion): `--motion-layout` (200ms) +
     `--ease-out`
   - Page transition: View Transitions API (`--motion-page` 240ms)

   Reduced-motion users get a 1ms cross-fade on every keyframe via the
   master override below. no need to gate per-component.
   ═══════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    /* Zeroing the delay too — a `backwards`-filled stagger otherwise holds
       its element at the from-state (opacity 0) for the full delay. */
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Realtime arrival pulse (Theme 3) ──────────────────────────
   New rows / cards arriving via realtime get a 2-second highlight
   then settle. Class is added by realtime.js + removed via setTimeout. */
.is-newly-arrived {
  animation: newlyArrivedPulse 1800ms var(--ease-spring-soft);
  animation-fill-mode: backwards;
  position: relative;
}
@keyframes newlyArrivedPulse {
  0%   { background-color: rgba(var(--accent-rgb), 0.18); }
  35%  { background-color: rgba(var(--accent-rgb), 0.10); }
  100% { background-color: transparent; }
}

/* ═══════════════════════════════════════════════════════════════
   Theme 5. Command palette + Shortcut overlay
   ═══════════════════════════════════════════════════════════════ */
.cmdk-overlay {
  position: fixed;
  inset: 0;
  background: var(--glass-backdrop-bg);
  backdrop-filter: var(--glass-backdrop-filter);
  -webkit-backdrop-filter: var(--glass-backdrop-filter);
  z-index: var(--z-command);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: max(64px, 12vh) var(--space-4) var(--space-4);
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms var(--ease-out);
}
.cmdk-overlay.is-open {
  opacity: 1;
  pointer-events: auto;
}
.cmdk-panel {
  width: 100%;
  max-width: 600px;
  max-height: 70vh;
  background: var(--glass-surface-bg);
  backdrop-filter: var(--glass-surface-filter);
  -webkit-backdrop-filter: var(--glass-surface-filter);
  border: var(--glass-surface-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--glass-surface-shadow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transform: translateY(-8px) scale(0.97);
  transition: transform var(--motion-popover) var(--ease-out);
}
.cmdk-overlay.is-open .cmdk-panel {
  transform: translateY(0) scale(1);
}
.cmdk-search-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}
.cmdk-search-icon { width: 16px; height: 16px; color: var(--text-muted); flex-shrink: 0; }
.cmdk-input {
  flex: 1;
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text-primary);
  font-size: 0.9375rem;
  font-family: inherit;
  min-height: 28px;
}
.cmdk-input::placeholder { color: var(--text-muted); }
.cmdk-esc {
  font-size: 0.6875rem;
  font-family: var(--font-mono);
  color: var(--text-muted);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
}
.cmdk-list {
  list-style: none;
  margin: 0;
  padding: var(--space-2);
  overflow-y: auto;
  flex: 1 1 auto;
}
.cmdk-heading {
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: var(--space-3) var(--space-3) var(--space-1);
}
.cmdk-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--text-primary);
}
.cmdk-item.is-active {
  background: rgba(var(--accent-rgb), 0.10);
  color: var(--text-primary);
}
.cmdk-item-icon { width: 16px; height: 16px; color: var(--text-muted); flex-shrink: 0; }
.cmdk-item.is-active .cmdk-item-icon { color: var(--accent); }
/* R7 — the prod promote is the only palette entry that reaches every
   client. Its icon carries the danger hue in both states (the active rule
   above would otherwise recolour it accent), and its kind label says what
   it touches instead of "action". */
.cmdk-item--danger .cmdk-item-icon,
.cmdk-item--danger.is-active .cmdk-item-icon { color: var(--danger); }
.cmdk-item-label { flex: 1; }
.cmdk-item-kind {
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-transform: lowercase;
}
/* An agent row's right-hand label is its client's name (O1), a proper noun
   that the lowercase kind treatment would mangle. */
.cmdk-item-kind--name { text-transform: none; }
.cmdk-empty {
  list-style: none;
  padding: var(--space-5);
  text-align: center;
  color: var(--text-muted);
  font-size: var(--text-sm);
}
.cmdk-hint {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--border);
  font-size: var(--text-caption);
  color: var(--text-muted);
  flex-shrink: 0;
}
.cmdk-hint kbd {
  font-family: var(--font-mono);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  padding: 1px 4px;
  margin-right: 3px;
}

/* mobile: full-width sheet from top */
@media (max-width: 640px) {
  .cmdk-overlay { padding: 0; align-items: stretch; }
  .cmdk-panel {
    max-width: 100%;
    max-height: 100dvh;
    border-radius: 0;
    transform: translateY(8%) scale(1);
  }
  .cmdk-overlay.is-open .cmdk-panel { transform: translateY(0) scale(1); }
}

/* ── Shortcuts overlay ──────────────────────────── */
.shortcuts-overlay {
  position: fixed;
  inset: 0;
  background: var(--glass-backdrop-bg);
  backdrop-filter: var(--glass-backdrop-filter);
  -webkit-backdrop-filter: var(--glass-backdrop-filter);
  z-index: var(--z-command);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms var(--ease-out);
}
.shortcuts-overlay.is-open { opacity: 1; pointer-events: auto; }
.shortcuts-panel {
  width: 100%;
  max-width: 480px;
  background: var(--glass-surface-bg);
  border: var(--glass-surface-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--glass-surface-shadow);
  backdrop-filter: var(--glass-surface-filter);
  -webkit-backdrop-filter: var(--glass-surface-filter);
  transform: scale(0.96);
  transition: transform var(--motion-dialog-enter) var(--ease-spring-out);
}
.shortcuts-overlay.is-open .shortcuts-panel { transform: scale(1); }
.shortcuts-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4);
  border-bottom: 1px solid var(--border);
}
.shortcuts-header h3 {
  margin: 0;
  font-size: var(--text-h3);
  font-family: var(--font-display);
  font-weight: 600;
}
.shortcuts-list {
  list-style: none;
  margin: 0;
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
}
.shortcuts-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.shortcuts-list li:last-child { border-bottom: none; }
.shortcuts-keys { display: flex; align-items: center; gap: var(--space-1); }
.shortcuts-keys kbd {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  padding: 2px 6px;
  color: var(--text-primary);
}
.shortcuts-or {
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin: 0 var(--space-1);
}
.shortcuts-desc { color: var(--text-secondary); flex: 1; text-align: right; }

/* j/k row nav highlight */
.is-row-selected {
  background: rgba(var(--accent-rgb), 0.08) !important;
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ═══════════════════════════════════════════════════════════════
   Theme 4. Density toggle (calls + admin client list)
   `.density-compact` shrinks row paddings ~30% so more rows fit.
   ═══════════════════════════════════════════════════════════════ */
.data-table.density-compact td,
.data-table.density-compact th {
  padding-top: 8px;
  padding-bottom: 8px;
  font-size: 0.8125rem;
}
.data-table.density-compact .td-caller,
.data-table.density-compact .font-medium {
  font-size: 0.875rem;
}
.density-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 2px;
}
.density-toggle button {
  background: transparent;
  border: 0;
  padding: 4px var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: var(--text-caption);
  cursor: pointer;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.density-toggle button:hover { color: var(--text-primary); }
.density-toggle button.is-active {
  background: var(--bg-card-solid);
  color: var(--text-primary);
  box-shadow: 0 0 0 1px var(--border);
}
.density-toggle button i,
.density-toggle button svg { width: 14px; height: 14px; }

/* ═══════════════════════════════════════════════════════════════
   Theme 10. Performance: content-visibility on long lists
   The browser skips render of below-the-fold rows / cards / chart
   tiles, saving frames during scroll. `contain-intrinsic-size` keeps
   scroll-bar geometry stable.
   ═══════════════════════════════════════════════════════════════ */
.calls-list,
.call-card-list,
.client-grid,
.admin-client-grid,
.chart-grid {
  content-visibility: auto;
  contain-intrinsic-size: 1px 600px;
}

/* Theme 4. calls-page meta row (count + density toggle) */
.calls-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  flex-wrap: wrap;
}
.calls-count-line {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 640px) {
  .calls-meta-row { padding: var(--space-2) 0; }
  /* Hide density toggle on phones. table is single-column oriented anyway */
  .calls-meta-row .density-toggle { display: none; }
}

/* Theme 4. greeting status line on dashboard */
.greeting-status {
  margin: 4px 0 0;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
}
.greeting-status[hidden] { display: none; }

/* ── Prompt tab ────────────────────────────────────────────────────── */
.prompt-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.prompt-token-count {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  padding: 2px var(--space-2);
  border-radius: 6px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
}
.prompt-token-count.is-warning {
  color: #b45309;          /* amber-700 */
  background: #fef3c7;     /* amber-100 */
  border-color: #fcd34d;   /* amber-300 */
}
[data-theme="dark"] .prompt-token-count.is-warning {
  color: #fbbf24;          /* amber-400 */
  background: rgba(251, 191, 36, 0.12);
  border-color: rgba(251, 191, 36, 0.35);
}
.prompt-token-warning {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  padding: var(--space-3);
  border-radius: 8px;
  background: #fef3c7;
  border: 1px solid #fcd34d;
  color: #78350f;
  font-size: var(--text-sm);
}
[data-theme="dark"] .prompt-token-warning {
  background: rgba(251, 191, 36, 0.10);
  border-color: rgba(251, 191, 36, 0.30);
  color: #fcd34d;
}
.prompt-token-warning i,
.prompt-token-warning svg { width: 16px; height: 16px; flex: 0 0 auto; }

/* A SYSTEM PROMPT IS A DOCUMENT, SO GIVE IT A MEASURE.
   The field ran the full width of the content column — 189 characters per
   line at the audited width, with 123,000 characters in it. Nobody can
   track a line that long; the eye loses the return sweep. 96ch is generous
   for monospace prose and still shows indented structure without wrapping.
   The stats and hint under it share the measure so the block reads as one
   object rather than a narrow field with a wide caption. */
.prompt-textarea {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.6;
  max-width: 96ch;
  min-height: 700px;
  resize: vertical;
  white-space: pre-wrap;
  tab-size: 2;
}
/* Qualified with .agent-edit-tab-content so this outranks the 72ch prose
   measure that tab-content puts on every other hint: the block under the
   prompt is a caption ON the document and has to share the document's
   measure, or it reads as a separate narrow column beside a wide one. */
.prompt-editor .prompt-footer-stats,
.agent-edit-tab-content .prompt-editor > .form-hint,
.prompt-editor > .form-hint { max-width: 96ch; }

/* Full-screen editing. The prompt is the only field here that is really a
   document; everything else on the tab is chrome around it. Expanded, the
   measure is unchanged — what grows is the height, and the quiet. */
.prompt-editor.is-expanded {
  position: fixed;
  inset: 0;
  z-index: var(--z-panel);
  margin: 0;
  border: 0;
  border-radius: 0;
  background: var(--bg-page);
  padding: var(--space-6) var(--space-8);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.prompt-editor.is-expanded .agent-edit-section-head { margin-bottom: 0; }
.prompt-editor.is-expanded .prompt-textarea {
  flex: 1 1 auto;
  min-height: 0;
  resize: none;
}
/* Keep the expanded editor centred on wide screens instead of hugging the
   left edge of a 2560px display. */
.prompt-editor.is-expanded > * {
  width: 100%;
  max-width: 96ch;
  margin-left: auto;
  margin-right: auto;
}
.prompt-editor.is-expanded .agent-edit-section-head { max-width: 96ch; }
body.prompt-editor-open { overflow: hidden; }

.prompt-vars-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: var(--space-3);
}
.prompt-vars-table th,
.prompt-vars-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  text-align: left;
  font-size: var(--text-sm);
  vertical-align: top;
}
.prompt-vars-table th {
  color: var(--text-muted);
  font-weight: 500;
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.prompt-vars-table tbody tr:last-child td { border-bottom: 0; }
.prompt-vars-table td:first-child { width: 240px; }
.prompt-vars-table code {
  font-family: var(--font-mono);
  font-size: 12.5px;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  color: var(--text-primary);
}

/* ── Advanced tab locked banner ────────────────────────────────────── */
.advanced-locked-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
  border-radius: 12px;
  background: #fef3c7;
  border: 1px solid #fcd34d;
  color: #78350f;
}

/* Advanced tab inputs are HTML-`disabled` while the per-agent override
   gate is closed. Surface the locked state visually so operators don't
   try to toggle and wonder why nothing happens (audit 2026-05-15 QW-12).
   Scoped to the Advanced panel via the tab's container ID so other
   places that use `disabled` (e.g. Phone Numbers Krisp toggle pre-
   provision) keep their own treatments. */
#tab-panel-advanced input:disabled,
#tab-panel-advanced select:disabled,
#tab-panel-advanced textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
#tab-panel-advanced .toggle-label:has(input:disabled),
#tab-panel-advanced .form-group:has(input:disabled) .form-label,
#tab-panel-advanced .form-group:has(select:disabled) .form-label {
  cursor: not-allowed;
  color: var(--text-muted);
}
#tab-panel-advanced .form-group:has(input:disabled) .form-hint,
#tab-panel-advanced .form-group:has(select:disabled) .form-hint {
  opacity: 0.7;
}
.advanced-locked-banner > i,
.advanced-locked-banner > svg {
  width: 18px; height: 18px;
  flex: 0 0 auto;
  margin-top: 2px;
}
.advanced-locked-banner strong {
  display: block;
  font-weight: 600;
  margin-bottom: 4px;
}
.advanced-locked-banner p {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.45;
}
[data-theme="dark"] .advanced-locked-banner {
  background: rgba(251, 191, 36, 0.10);
  border-color: rgba(251, 191, 36, 0.30);
  color: #fcd34d;
}

/* Visually mute disabled inputs in agent-edit sections so users see
   they're locked, without dimming the section title or hint text. */
.agent-edit-section input:disabled,
.agent-edit-section select:disabled,
.agent-edit-section textarea:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
.agent-edit-section .toggle-label:has(input:disabled),
.agent-edit-section .toggle-label:has(input:disabled) span {
  cursor: not-allowed;
}

/* ── Price estimator chip ───────────────────────────────────────────
   Sits in the agent-edit-title-row next to the version badge. Hover
   reveals a structured breakdown tooltip. Color-coded by total cost.
*/
.price-estimator {
  position: relative;
  display: inline-flex;
}
.price-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 10px;
  border-radius: 999px;
  font-size: var(--text-caption);
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--bg-card-subtle);
  color: var(--text-secondary);
  user-select: none;
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.price-chip strong {
  font-family: var(--font-mono);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.price-chip i,
.price-chip svg {
  width: 12px;
  height: 12px;
  opacity: 0.55;
  transition: transform 0.15s ease;
}
.price-estimator.is-open .price-chip i,
.price-estimator.is-open .price-chip svg { transform: rotate(180deg); }
.price-chip:hover { color: var(--text-primary); border-color: var(--text-muted); }

/* Color tiers. subtle, not loud. Semantic tokens carry both themes: the
   --pill-*-fg text tiers clear AA in light and dark, so no per-theme overrides. */
.price-chip--cheap     { background: rgba(var(--success-rgb), 0.10); border-color: rgba(var(--success-rgb), 0.35); }
.price-chip--cheap strong { color: var(--pill-success-fg); }
.price-chip--moderate  { background: var(--bg-card-subtle); border-color: var(--border); }
.price-chip--expensive { background: rgba(var(--warning-rgb), 0.12); border-color: rgba(var(--warning-rgb), 0.40); }
.price-chip--expensive strong { color: var(--pill-warning-fg); }
.price-chip--neutral   { background: var(--bg-card-subtle); }

/* Popover. Hover previews it; a click on the chip pins it open (.is-open,
   toggled in JS with Esc + outside-click dismiss). No :focus-within pinning —
   that kept it open over the tab bar whenever the chip held focus. */
.price-tooltip {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  width: 320px;
  padding: var(--space-3) var(--space-4);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  font-size: var(--text-caption);
  color: var(--text-primary);
  z-index: var(--z-dropdown);
  opacity: 0;
  visibility: hidden;
  /* Grows out of the chip that opened it, not out of thin air — the panel
     hangs off the chip's leading edge, so that corner is the origin. A
     translate alone reads as a panel sliding past; the scale is what makes
     the chip look like the thing it came from. */
  transform-origin: top left;
  transform: translateY(-4px) scale(0.96);
  transition: opacity 140ms var(--ease-out),
              transform 140ms var(--ease-out),
              visibility 0s linear 140ms;
  pointer-events: none;  /* Don't let the tooltip itself block hover */
}
.price-estimator:hover .price-tooltip,
.price-estimator.is-open .price-tooltip,
.price-chip:hover + .price-tooltip {
  opacity: 1;
  visibility: visible;
  transform: translateY(0) scale(1);
  transition: opacity 140ms var(--ease-out),
              transform 140ms var(--ease-out),
              visibility 0s linear 0s;
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  .price-tooltip,
  .price-estimator:hover .price-tooltip,
  .price-estimator.is-open .price-tooltip,
  .price-chip:hover + .price-tooltip { transform: none; }
}
[data-theme="dark"] .price-tooltip {
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}

.price-tooltip-inner > * + * { margin-top: 6px; }
.price-tooltip-title {
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--text-primary);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}

/* Default tooltip rows. clean "label: value" pairs, no per-row math */
.price-row-clean {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--text-secondary);
  line-height: 1.6;
}
.price-row-clean > span:last-child {
  color: var(--text-primary);
  font-weight: 500;
}
.price-row-clean--unavailable {
  color: var(--text-muted);
  font-style: italic;
}
.price-row-clean--unavailable > span:last-child { color: var(--text-muted); }
.price-row-clean--meta {
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.price-row-clean--meta > span:last-child { color: var(--text-muted); font-weight: 400; }
.price-row-clean--fallback {
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px dashed var(--border);
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.price-row-clean--fallback > span:last-child { color: var(--text-muted); font-weight: 400; }

/* Total row. emphasized, separated by border */
.price-total-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.price-notes {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px dashed var(--border);
  font-size: var(--text-caption);
  color: var(--warning, #b45309);
}
[data-theme="dark"] .price-notes { color: #fbbf24; }
.price-notes > div + div { margin-top: 2px; }

/* Math disclosure. collapsed by default, click "Show calculation" to expand */
.price-math-details {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px dashed var(--border);
}
.price-math-details > summary {
  cursor: pointer;
  font-size: var(--text-caption);
  color: var(--text-muted);
  user-select: none;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.price-math-details > summary::-webkit-details-marker { display: none; }
.price-math-details > summary::before {
  content: "▸";
  font-size: 9px;
  transition: transform 0.15s ease;
  display: inline-block;
}
.price-math-details[open] > summary::before { transform: rotate(90deg); }
.price-math-details > summary:hover { color: var(--text-secondary); }

.price-math-body {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.price-math-group { padding-bottom: 4px; }
.price-math-group + .price-math-group {
  padding-top: 4px;
  border-top: 1px solid var(--border);
}
.price-math-head {
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 2px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.price-math-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.5;
}
.price-math-row > span:last-child {
  text-align: right;
  color: var(--text-secondary);
}
.price-footnote {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
  font-size: 10px;
  color: var(--text-muted);
  font-style: italic;
  line-height: 1.5;
}

/* Sanity check (comment-only). for the Phase 0 seed Eva config:
   ~7,000 cached prompt tokens × 3 turns/min × 0.99 = 20,790 cached tokens
     × $0.125/M = $0.0026
   ~370 uncached tokens (300 history + 70 prompt-cache misses) × 3 = 1,110
     × $1.25/M = $0.0014
   ~600 output tokens × $10/M = $0.0060
   LLM total ≈ $0.010/min
   STT (Deepgram) = $0.0043/min
   TTS (Inworld @ 400 chars × $0.035/1k) = $0.014/min
   Twilio = $0.0085/min
   Grand total ≈ $0.036/min  (revised April 2026. Inworld rate drop +
                              corrected agent speech rate from 700→400 chars/min)
*/

/* ── Platform settings card (admin-agents.html) ─────────────────────
   Collapsible card above the agents table. Currently houses one
   toggle (LiveKit Inference); designed to grow as more global
   settings land. Uses native <details>/<summary> for accessibility
   without a JS dependency. */
.platform-settings-card {
  margin: 0 0 var(--space-4);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}
.platform-settings-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  user-select: none;
  list-style: none;
  font-weight: 500;
  color: var(--text-secondary);
  transition: background 0.15s ease;
}
.platform-settings-summary::-webkit-details-marker { display: none; }
.platform-settings-summary:hover { background: var(--bg-card-subtle); color: var(--text-primary); }
.platform-settings-summary > i,
.platform-settings-summary > svg {
  width: 16px;
  height: 16px;
  opacity: 0.7;
}
.platform-settings-summary > span:first-of-type {
  flex: 0 0 auto;
}
.platform-settings-summary-meta {
  flex: 1;
  text-align: right;
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.platform-settings-card[open] .platform-settings-summary {
  border-bottom: 1px solid var(--border);
  background: var(--bg-card-subtle);
}

.platform-settings-body {
  padding: var(--space-4);
}
.platform-settings-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
}
.platform-settings-row + .platform-settings-row {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.platform-settings-row-text {
  flex: 1;
}
.platform-settings-row-label {
  font-weight: 500;
  font-size: var(--text-sm);
  color: var(--text-primary);
  margin-bottom: 2px;
}
.platform-settings-row-hint {
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.5;
  max-width: 540px;
}

/* Toggle switch (label wraps the input + visual track) */
.platform-settings-toggle {
  position: relative;
  flex: 0 0 44px;
  height: 24px;
  margin-top: 2px;
  cursor: pointer;
}
.platform-settings-toggle > input {
  opacity: 0;
  position: absolute;
  inset: 0;
  cursor: pointer;
}
.platform-settings-toggle > input:disabled { cursor: wait; }
.platform-settings-switch {
  position: absolute;
  inset: 0;
  background: var(--border);
  border-radius: 999px;
  transition: background 0.15s ease;
}
.platform-settings-switch::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  transition: transform 0.15s ease;
}
.platform-settings-toggle > input:checked + .platform-settings-switch {
  background: var(--accent, #8B93E8);
}
.platform-settings-toggle > input:checked + .platform-settings-switch::after {
  transform: translateX(20px);
}
.platform-settings-toggle > input:disabled + .platform-settings-switch {
  opacity: 0.6;
}

/* Mode badge in price tooltip header. shows whether the estimate is
   based on LiveKit Inference rates or direct-provider rates. */
/* .price-mode-badge retired — the price tooltip now renders the mode as
   plain metadata text (.price-mode-meta), not a status badge. */

/* ── Page header actions cluster ──────────────────────────────────
   When the page header has multiple buttons, wrap them in
   .page-header-actions so they sit together on the right side
   (the parent .page-header uses justify-content: space-between). */
.page-header-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* ── Phone Numbers two-pane management surface ────────────────────
   Opened from admin-agents.html as a centered modal-style overlay.
   Wider than the standard .panel-sheet drawer (440px) since the
   two-pane layout needs horizontal room: ~280px left rail + the
   right detail pane. Reuses .panel-overlay backdrop / animations.

   When an item is selected: left rail stays visible, right pane
   loads the detail view. Empty-state right pane shows the "pick
   a number" copy.
*/
.phone-numbers-overlay {
  /* Override the default panel-overlay flex centering. we want
     full bleed for our shell container at ~95vw / ~88vh. */
  padding: 24px;
}

.phone-numbers-shell {
  position: relative;
  display: grid;
  grid-template-columns: 280px 1fr;
  width: 100%;
  max-width: 1200px;
  height: 88vh;
  max-height: 880px;
  background: var(--bg-card-solid);
  border: 1px solid var(--border);
  border-radius: 12px;
  /* --shadow-xl carries its own inset top highlight since the dark-elevation
     retune (0248ca1); stacking a second one here rendered it at ~0.10 and
     made this one panel's lit edge brighter than every other elevated
     surface in the app. */
  box-shadow: var(--shadow-xl);
  overflow: hidden;
  /* Lift in animation. borrowed from .notif-settings-sheet pattern */
  animation: phone-numbers-shell-in var(--motion-layout, 0.2s) var(--ease-out, ease-out);
}
@keyframes phone-numbers-shell-in {
  from { transform: translateY(8px) scale(0.99); opacity: 0; }
  to   { transform: translateY(0)    scale(1);    opacity: 1; }
}

.phone-numbers-overlay.is-closing .phone-numbers-shell {
  animation: phone-numbers-shell-out var(--motion-layout, 0.2s) var(--ease-in, ease-in) forwards;
}
@keyframes phone-numbers-shell-out {
  from { transform: translateY(0)    scale(1);    opacity: 1; }
  to   { transform: translateY(8px)  scale(0.99); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .phone-numbers-shell,
  .phone-numbers-overlay.is-closing .phone-numbers-shell {
    animation: none;
  }
}

.phone-numbers-close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 2;
  background: transparent;
  border: 0;
  padding: 6px;
  border-radius: 8px;
  cursor: pointer;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease, color 0.15s ease;
}
.phone-numbers-close:hover {
  background: var(--bg-card-subtle);
  color: var(--text-primary);
}
.phone-numbers-close i,
.phone-numbers-close svg { width: 18px; height: 18px; }

/* Left rail. list of numbers */
.pn-left-rail {
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border);
  background: var(--bg-card-subtle);
  min-height: 0;   /* allow inner scroll */
}
.pn-rail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-4) var(--space-3);
  gap: var(--space-2);
}
.pn-rail-title {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
.pn-rail-title i,
.pn-rail-title svg {
  width: 18px; height: 18px;
  color: var(--text-secondary);
}
.pn-rail-add-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--border);
  background: var(--bg-card);
  color: var(--text-secondary);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.pn-rail-add-btn:not(:disabled):hover {
  background: var(--accent, #8B93E8);
  color: #fff;
  border-color: var(--accent, #8B93E8);
}
.pn-rail-add-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.pn-rail-add-btn i,
.pn-rail-add-btn svg { width: 16px; height: 16px; }

.pn-rail-search {
  position: relative;
  padding: 0 var(--space-4) var(--space-3);
}
.pn-rail-search-icon {
  position: absolute;
  left: calc(var(--space-4) + 10px);
  top: 50%;
  transform: translateY(calc(-50% - var(--space-2) / 2));
  width: 14px; height: 14px;
  color: var(--text-muted);
  pointer-events: none;
}
.pn-rail-search-input {
  width: 100%;
  padding: 8px 10px 8px 32px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text-primary);
  font-size: var(--text-sm);
  font-family: var(--font-body);
  transition: border-color 0.15s ease;
}
/* No `outline: none` here — see the .form-input note above. At (0,2,0) it
   outranks `input:focus-visible` (0,1,1) and deletes the keyboard ring; the
   global `*:focus { outline: none }` already suppresses the mouse case and
   correctly loses to focus-visible. */
.pn-rail-search-input:focus {
  border-color: var(--text-secondary);
}
.pn-rail-search-input:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.pn-rail-list {
  flex: 1;
  overflow-y: auto;
  padding: 0 var(--space-2) var(--space-3);
}
.pn-rail-empty {
  padding: var(--space-4);
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-style: italic;
  text-align: center;
}
.pn-rail-empty p { margin: 0; }

/* Right pane. detail */
.pn-right-pane {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--bg-card-solid);
  overflow-y: auto;
}

/* .pn-empty-state retired — the phone-numbers no-selection state renders
   through the canonical .empty-state family. */

/* Mobile: stack the panes */
@media (max-width: 760px) {
  .phone-numbers-overlay { padding: 12px; }
  .phone-numbers-shell {
    grid-template-columns: 1fr;
    height: 92vh;
  }
  .pn-left-rail {
    max-height: 40vh;
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Phone Numbers. rail list items ─────────────────────────────── */
.pn-rail-item {
  display: flex;
  align-items: center;
  width: 100%;
  padding: 10px 12px;
  margin: 0 0 2px;
  border: 0;
  background: transparent;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--text-primary);
  transition: background 0.12s ease;
}
.pn-rail-item:hover {
  background: var(--bg-card);
}
.pn-rail-item.is-selected {
  background: var(--bg-card);
  border: 1px solid var(--border);
  /* Keep padding consistent. border eats 2px */
  padding: 9px 11px;
  font-weight: 500;
}
.pn-rail-item-nickname {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Loading skeletons in the rail */
.pn-rail-loading {
  padding: 4px;
}
/* Size-only modifier; shimmer + mount-delay come from the canonical
   `.skeleton` (S5). radius-md (8px) matches the loaded rail rows. */
.pn-rail-skeleton {
  height: 36px;
  margin: 0 8px 6px;
  /* Block placeholder — opts out of the pill default .skeleton now carries. */
  border-radius: var(--radius-md);
}

/* Error state in the rail */
.pn-rail-error { color: var(--danger, #dc2626); }
.pn-rail-error-detail {
  margin-top: 4px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-style: italic;
}

/* ── Phone Numbers. Add modal ────────────────────────────────────
   Layered above the phone-numbers panel. Z-index higher than the
   parent .panel-overlay so backdrop-click + Esc dismiss only this
   form, not the parent panel. */
.pn-add-overlay {
  z-index: var(--z-popover);
  /* Darker dim than the parent overlay so this modal pops visually */
  background: rgba(0, 0, 0, 0.45);
  padding: 24px;
}
.pn-add-modal {
  width: 100%;
  max-width: 540px;
  max-height: 88vh;
  overflow-y: auto;
  background: var(--bg-card-solid);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-xl);
  animation: pn-add-modal-in var(--motion-layout, 0.2s) var(--ease-out, ease-out);
}
@keyframes pn-add-modal-in {
  from { transform: translateY(8px) scale(0.99); opacity: 0; }
  to   { transform: translateY(0)    scale(1);    opacity: 1; }
}
.pn-add-overlay.is-closing .pn-add-modal {
  animation: pn-add-modal-out var(--motion-layout, 0.2s) var(--ease-in, ease-in) forwards;
}
@keyframes pn-add-modal-out {
  from { transform: translateY(0)    scale(1);    opacity: 1; }
  to   { transform: translateY(8px)  scale(0.99); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .pn-add-modal,
  .pn-add-overlay.is-closing .pn-add-modal {
    animation: none;
  }
}

.pn-add-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
}
.pn-add-header h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
/* .pn-add-close-btn retired to .btn.btn-ghost.btn-icon; class kept as the JS
   bind hook (getElementById) and for the shared ::after tap-target below. */

.pn-add-form {
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.pn-add-form .form-group {
  margin-bottom: 0;
}
.pn-add-required {
  color: var(--danger, #dc2626);
  margin-left: 2px;
}

.pn-add-error {
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--danger, #dc2626);
  min-height: 0;     /* collapse cleanly when empty */
  line-height: 1.4;
}
.pn-add-error:empty { display: none; }

.pn-add-form-error {
  margin: 0;
  padding: var(--space-3);
  border-radius: 8px;
  background: rgba(220, 38, 38, 0.08);
  border: 1px solid rgba(220, 38, 38, 0.25);
  color: var(--danger, #dc2626);
  font-size: var(--text-sm);
  line-height: 1.45;
}
.pn-add-form-error:empty { display: none; }

.pn-add-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
  margin-top: var(--space-2);
  /* Push above-the-line so the border separates the actions visually */
  padding-top: var(--space-4);
}
.pn-add-actions .btn i,
.pn-add-actions .btn svg { width: 14px; height: 14px; }
.pn-add-actions .btn span { font-weight: 500; }

/* Spinner state for the submit button */
.pn-add-actions .btn:disabled {
  opacity: 0.7;
  cursor: wait;
}
/* The svg twin is what actually spins: Lucide has replaced the <i> by the
   time the button is disabled, so the attribute selector matched a node that
   no longer existed and the loader sat still. Plain `svg` rather than a
   name-matched twin because a disabled submit button contains exactly one
   icon, and it is always the loader (see phone-numbers.js). */
.pn-add-actions .btn:disabled i[data-lucide="loader"],
.pn-add-actions .btn:disabled svg {
  animation: spin 1s linear infinite;
}

/* Inline <code> tags inside form-hints */
.pn-add-form .form-hint code {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  padding: 1px 4px;
  border-radius: 3px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  color: var(--text-primary);
}

/* Mobile tweak. narrower modal, less padding */
@media (max-width: 480px) {
  .pn-add-overlay { padding: 12px; }
  .pn-add-modal { max-height: 92vh; }
  .pn-add-form { padding: var(--space-4); }
  .pn-add-header { padding: var(--space-3) var(--space-4); }
}

/* ── Phone Numbers. detail pane ──────────────────────────────── */
.pn-detail {
  padding: var(--space-5) var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* Header: nickname + edit pencil + ID + provider */
.pn-detail-header {
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--space-4);
}
.pn-detail-title-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.pn-detail-nickname {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.015em;
  line-height: 1.2;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pn-detail-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.pn-detail-id {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.pn-detail-id strong {
  color: var(--text-primary);
  font-weight: 500;
}
.pn-detail-meta-sep { color: var(--text-muted); }
.pn-detail-provider {
  font-family: var(--font-body);
  color: var(--text-muted);
}

/* Small icon-button on header / inline */
/* .pn-detail-icon-btn retired to .btn.btn-ghost.btn-icon; class kept as JS
   bind hook + for the shared ::after tap-target below. The copy variant keeps
   its tighter icon size. */
/* Triple-qualified on purpose: the element is .btn.btn-ghost.btn-icon
   .pn-detail-copy-btn, so this has to outrank the .btn.btn-icon 16px rule
   further down the file (equal specificity would lose on source order). */
.btn.btn-icon.pn-detail-copy-btn i,
.btn.btn-icon.pn-detail-copy-btn svg { width: 12px; height: 12px; }

/* Minimum 40x40 tap target on small icon buttons — visible size unchanged,
   hit area extended via a centered pseudo-element (make-interfaces-feel-better). */
.pn-detail-copy-btn,
.pn-detail-icon-btn,
.pn-add-close-btn,
.client-agents-drawer .drawer-close-btn { position: relative; }
.pn-detail-copy-btn::after,
.pn-detail-icon-btn::after,
.pn-add-close-btn::after,
.modal-deploy .modal-close::after,
.client-agents-drawer .drawer-close-btn::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  transform: translate(-50%, -50%);
}

/* Section card. same visual language as agent-edit-section */
.pn-section {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: var(--space-4) var(--space-5);
}
.pn-section-title {
  font-family: var(--font-display);
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
  letter-spacing: -0.005em;
}
.pn-section-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.pn-section-body .form-group { margin-bottom: 0; }

/* Read-only client display. looks like an input but isn't editable. */
.pn-readonly-field {
  padding: 8px 12px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text-primary);
  font-size: var(--text-sm);
  font-family: var(--font-body);
}
.pn-readonly-field--mono {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: 12px;
  line-height: 1.6;
  word-break: break-all;
}

/* Phase 2.5. Provisioning status badge in detail header */
/* Thin layout modifier over .badge — spacing + icon size only; the colour
   comes from the .badge-success/-warning/-danger variant emitted in JS. */
.pn-prov-badge { margin-left: 8px; }
.pn-prov-badge i,
.pn-prov-badge svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* Template-clone review banner. shown above tabs when an agent was just
   cloned from the template and hasn't been customized yet. */
.template-clone-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  margin: 0 0 16px 0;
  border-radius: 10px;
  background: rgba(234, 179, 8, 0.08);
  border: 1px solid rgba(234, 179, 8, 0.3);
  color: var(--text-primary);
}
.template-clone-banner-icon {
  flex-shrink: 0;
  color: rgb(234, 179, 8);
  display: flex;
  align-items: center;
}
.template-clone-banner-icon i,
.template-clone-banner-icon svg {
  width: 20px;
  height: 20px;
}
.template-clone-banner-text {
  flex: 1;
  font-size: var(--text-sm);
  line-height: 1.4;
}
.template-clone-banner-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

/* Phase 2.5. Provisioning banner in LiveKit Routing section */
.pn-prov-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 14px;
  border-radius: 10px;
  margin-bottom: 16px;
  border: 1px solid;
}
.pn-prov-banner-text {
  font-size: var(--text-sm);
  line-height: 1.4;
  flex: 1;
}
.pn-prov-banner--pending {
  background: rgba(234, 179, 8, 0.08);
  border-color: rgba(234, 179, 8, 0.25);
  color: var(--text-primary);
}
.pn-prov-banner--failed {
  background: rgba(239, 68, 68, 0.08);
  border-color: rgba(239, 68, 68, 0.3);
  color: var(--text-primary);
}
.pn-prov-banner button {
  flex-shrink: 0;
}

/* Visually mute disabled inputs in pn sections (matches agent-edit pattern) */
.pn-section input:disabled,
.pn-section select:disabled,
.pn-section textarea:disabled {
  opacity: 0.65;
  cursor: not-allowed;
}
.pn-section .toggle-label:has(input:disabled),
.pn-section .toggle-label:has(input:disabled) span {
  cursor: not-allowed;
}

/* Password input + show/hide button */
.pn-password-wrap {
  position: relative;
  display: block;
}
.pn-password-wrap .form-input {
  padding-right: 38px;
}
.pn-password-toggle {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 6px;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.pn-password-toggle:hover {
  color: var(--text-primary);
  background: var(--bg-card-subtle);
}
.pn-password-toggle i,
.pn-password-toggle svg { width: 14px; height: 14px; }

/* Recent Activity rows */
.pn-activity-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
  font-size: var(--text-sm);
  font-family: var(--font-body);
  padding: 6px 0;
  border-bottom: 1px dashed var(--border);
}
.pn-activity-row:last-child { border-bottom: 0; }
.pn-activity-label { color: var(--text-secondary); }
.pn-activity-value {
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.pn-activity-placeholder {
  color: var(--text-muted);
  font-family: var(--font-body);
  font-style: italic;
}

/* Footer: Release Number button (red, destructive) */
.pn-detail-footer {
  display: flex;
  justify-content: flex-end;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
/* This is a TINTED danger button, not a filled one, so it opts out of the
   filled variant's cap lighting (--shadow-btn-fill, added 2026-08-07) —
   otherwise a translucent surface carries a solid button's depth. */
.pn-detail-footer .btn-danger {
  background: rgba(220, 38, 38, 0.10);
  border: 1px solid rgba(220, 38, 38, 0.4);
  color: var(--danger, #dc2626);
  box-shadow: none;
}
/* Filling on hover means white ink, so it fills with the audited fill —
   var(--danger) here was 3.01:1 in dark. */
.pn-detail-footer .btn-danger:not(:disabled):hover {
  background: var(--danger-fill);
  color: #fff;
  border-color: var(--danger-fill);
}
.pn-detail-footer .btn-danger:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.pn-detail-footer .btn-danger i,
.pn-detail-footer .btn-danger svg { width: 14px; height: 14px; }

/* ── Phone Numbers. save bar (sticky-ish, sits before footer) ──── */
.pn-save-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: var(--text-caption);
}
.pn-save-status {
  flex: 1;
  color: var(--text-muted);
  font-family: var(--font-mono);
}
.pn-save-bar-error {
  flex: 2;
  color: var(--danger, #dc2626);
  font-size: 12px;
  line-height: 1.4;
}
.pn-save-bar-error:empty { display: none; }
.pn-save-bar-actions {
  display: inline-flex;
  gap: var(--space-2);
}
.pn-save-bar-actions .btn i,
.pn-save-bar-actions .btn svg { width: 14px; height: 14px; }

/* ── Phone Numbers. copy-button feedback ───────────────────────── */
.pn-detail-copy-btn.is-copied {
  color: var(--pill-success-fg);   /* brand success token (was hardcoded #16a34a) */
  border-color: rgba(var(--success-rgb), 0.4);
  background: rgba(var(--success-rgb), 0.10);
}

/* ── Phone Numbers. Test Webhook result panel ──────────────────── */
.pn-webhook-result {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border-radius: 8px;
  font-size: var(--text-sm);
  font-family: var(--font-mono);
  border: 1px solid var(--border);
  background: var(--bg-card-subtle);
  color: var(--text-secondary);
  line-height: 1.5;
}
.pn-webhook-result[hidden] { display: none; }
.pn-webhook-result code {
  padding: 1px 5px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 3px;
  font-size: var(--text-caption);
}
.pn-webhook-result-summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: 6px;
}
.pn-webhook-result-summary > strong {
  font-size: 18px;
  font-weight: 600;
}
.pn-webhook-result-time {
  color: var(--text-muted);
  font-size: var(--text-caption);
}
.pn-webhook-result-body {
  margin-top: 6px;
}
.pn-webhook-result-body > summary {
  cursor: pointer;
  font-size: var(--text-caption);
  color: var(--text-muted);
  user-select: none;
}
.pn-webhook-result-body[open] > summary { color: var(--text-secondary); }
.pn-webhook-result-body pre {
  margin: 6px 0 0;
  padding: 8px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: var(--text-caption);
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 160px;
  overflow-y: auto;
}

/* Color tiers. match HTTP status semantics */
.pn-webhook-result--ok {
  background: rgba(22, 163, 74, 0.10);
  border-color: rgba(22, 163, 74, 0.35);
  color: #15803d;
}
.pn-webhook-result--ok .pn-webhook-result-summary > strong { color: #15803d; }

.pn-webhook-result--warn {
  background: rgba(245, 158, 11, 0.10);
  border-color: rgba(245, 158, 11, 0.35);
  color: #b45309;
}
.pn-webhook-result--warn .pn-webhook-result-summary > strong { color: #b45309; }

.pn-webhook-result--err {
  background: rgba(220, 38, 38, 0.10);
  border-color: rgba(220, 38, 38, 0.35);
  color: #dc2626;
}
.pn-webhook-result--err .pn-webhook-result-summary > strong { color: #dc2626; }

[data-theme="dark"] .pn-webhook-result--ok   { color: #4ade80; }
[data-theme="dark"] .pn-webhook-result--ok .pn-webhook-result-summary > strong { color: #4ade80; }
[data-theme="dark"] .pn-webhook-result--warn { color: #fbbf24; }
[data-theme="dark"] .pn-webhook-result--warn .pn-webhook-result-summary > strong { color: #fbbf24; }
[data-theme="dark"] .pn-webhook-result--err  { color: #f87171; }
[data-theme="dark"] .pn-webhook-result--err .pn-webhook-result-summary > strong { color: #f87171; }

/* ── Phone Numbers. confirm / release modals ───────────────────── */
.pn-confirm-overlay,
.pn-release-overlay {
  z-index: calc(var(--z-popover) + 5);
  background: rgba(0, 0, 0, 0.55);
  padding: 24px;
}
.pn-confirm-modal {
  width: 100%;
  max-width: 460px;
  background: var(--bg-card-solid);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-xl);
  padding: var(--space-5);
  text-align: center;
  animation: pn-add-modal-in var(--motion-layout, 0.2s) var(--ease-out, ease-out);
}
.pn-confirm-overlay.is-closing .pn-confirm-modal,
.pn-release-overlay.is-closing .pn-confirm-modal {
  animation: pn-add-modal-out var(--motion-layout, 0.2s) var(--ease-in, ease-in) forwards;
}
.pn-release-modal { max-width: 540px; text-align: left; }

.pn-confirm-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: rgba(245, 158, 11, 0.15);
  color: #b45309;
  margin: 0 auto var(--space-3);
}
.pn-confirm-icon i,
.pn-confirm-icon svg { width: 22px; height: 22px; }
.pn-confirm-icon--danger {
  background: rgba(220, 38, 38, 0.12);
  color: var(--danger, #dc2626);
}
[data-theme="dark"] .pn-confirm-icon { color: #fbbf24; }
[data-theme="dark"] .pn-confirm-icon--danger { color: #f87171; }
.pn-release-modal .pn-confirm-icon { margin-left: 0; margin-right: 0; }

.pn-confirm-modal h3 {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
}
.pn-confirm-modal p {
  margin: 0 0 var(--space-4);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.5;
}
.pn-release-summary {
  margin: 0 0 var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-primary);
}
.pn-release-summary strong { font-weight: 600; }
.pn-release-bullets {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-5);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.6;
}
.pn-release-bullets li { margin-bottom: 4px; }
.pn-release-soft-note {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 12px;
  color: var(--text-muted);
  font-style: italic;
}

.pn-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  margin-top: var(--space-2);
}
.pn-confirm-actions .btn i,
.pn-confirm-actions .btn svg { width: 14px; height: 14px; margin-right: 4px; }

/* The release button shares .btn-danger styling from the detail footer */

/* ── Prompt tab footer stats line ───────────────────────────────── */
.prompt-footer-stats {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.prompt-footer-stat strong {
  color: var(--text-primary);
  font-weight: 600;
}
.prompt-footer-stat-sep { color: var(--text-muted); opacity: 0.5; }
.prompt-footer-stat#pr-footer-cache.is-cache-ineligible {
  color: #b45309;   /* amber */
  cursor: help;
}
.prompt-footer-stat#pr-footer-cache.is-cache-ineligible strong { color: #b45309; }
[data-theme="dark"] .prompt-footer-stat#pr-footer-cache.is-cache-ineligible,
[data-theme="dark"] .prompt-footer-stat#pr-footer-cache.is-cache-ineligible strong {
  color: #fbbf24;
}

/* ── Tab toolbar (links above first section, e.g. "Reset to template") ── */
.agent-edit-tab-tools {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--space-3);
}
/* .agent-edit-link-btn retired — the General-tab reset now uses the unified
   .btn.btn-ghost.btn-sm "Reset to defaults" affordance. */

/* ─────────────────────────────────────────────────────────────────────
   Phase 2 UI overhaul: shared agent-builder primitives
   ───────────────────────────────────────────────────────────────────── */

/* Info icon for label clarification */
.info-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  padding: 2px;
  margin-left: 4px;
  cursor: help;
  color: var(--text-muted);
  border-radius: 4px;
  vertical-align: middle;
  transition: color 0.15s ease, background 0.15s ease;
}
.info-icon:hover { color: var(--text-primary); background: var(--bg-card-subtle); }
.info-icon i,
.info-icon svg { width: 12px; height: 12px; }

/* Section header with title + action (add button) */
.section-title-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: var(--space-3);
}
.section-title-row .agent-edit-section-title { margin: 0; }

/* Subsection title used inside tabs (smaller than .agent-edit-section-title) */
.agent-edit-subsection-title {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0 0 var(--space-3) 0;
}

/* .empty-state-inline retired (U3): it is .empty-state--line, declared with
   the rest of the empty-state family next to .empty-state. */

/* Larger modal variant */
.modal.modal-lg { max-width: 720px; width: 90%; }

/* Compact toggle for inline use */
.toggle-label.compact {
  font-size: var(--text-caption);
  gap: 6px;
}

/* Icon-only button.
   NOTE (audited 2026-08-07, left as-is deliberately): the 32px width and
   height below never take effect — `min-height`/`min-width: 44px` on the
   .btn-icon base clamps them regardless of specificity, so these buttons
   measure 44x44 everywhere, which is the touch floor we want. The values
   are kept rather than deleted so this comment has something to point at;
   they are inert, not load-bearing. */
.btn.btn-icon {
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Icon-only buttons keep their authored 16px rather than inheriting the 15px
   label-button size — the glyph is the button's whole content here, and it
   sits in a 44px chassis. Dual form, and (0,2,1) outranks the .btn svg base. */
.btn.btn-icon i,
.btn.btn-icon svg { width: 16px; height: 16px; }

/* Monospace input variant */
.form-input.form-input-mono {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: var(--text-body);
}

/* A SECOND .form-textarea BASE USED TO LIVE HERE, and it was the same bug
   the .badge block carried until 2026-08-07: a full redeclaration sitting
   six thousand lines below the real one, silently winning every tie.

   It cost more than tidiness. `font-family: var(--font-body)` shadowed
   .prompt-textarea's `var(--font-mono)`, so the system prompt editor has
   never actually been monospace despite the rule that says it is — the
   agents audit measured a 189-character Inter line and was reading this.
   Its :focus rule also hard-coded `outline: 2px solid rgba(99,102,241,.4)`
   on plain :focus, which is a bespoke ring on mouse click for one element
   type in an app that has exactly one focus ring.

   Everything it declared that mattered is already in the base up in the
   Forms section (search ".form-input,\n.form-select,\n.form-textarea") —
   same padding, same radius, same fill — so removing it changes nothing
   except the two things it was breaking. */

/* Badges (extending existing badge system)

   These three hard-coded raw Tailwind blue / violet / yellow — three hues
   the app's palette does not contain, none of them theme-aware. Worse, they
   are double-class (0,2,0), so `.badge.badge-info` silently OUTRANKED the
   canonical `.badge-info` declared at the bottom of this file: anyone
   reaching for the documented variant would have got Tailwind blue and no
   hint why. Retokenized onto the same --chip-hue recipe every other chip in
   the app uses, so the fork now agrees with what it was shadowing.

   Nothing currently emits any of the three (the only badge state in live
   markup is .badge-warning). Left in place rather than deleted because they
   are pre-existing; defused because they were a trap, not just dead. */
.badge.badge-info { --chip-hue: var(--info); color: var(--pill-info-fg); }
/* "secondary" has no hue of its own in this palette — it is the quiet tier,
   which is what .badge-neutral already means. */
.badge.badge-secondary { color: var(--text-secondary); }
.badge.badge-warn { --chip-hue: var(--warning); color: var(--pill-warning-fg); }
.badge.badge-neutral {
  background: var(--bg-card-subtle);
  color: var(--text-muted);
  border: 1px solid var(--border);
}

/* ─────────────────────────────────────────────────────────────────────
   Tools tab
   ───────────────────────────────────────────────────────────────────── */

.tools-list { display: flex; flex-direction: column; gap: 12px; }

.tool-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
  background: var(--bg-card-subtle);
  transition: border-color 0.15s ease;
}
.tool-card:hover { border-color: var(--border-hover, var(--border)); }
.tool-card-header {
  display: flex;
  align-items: center;
  gap: 12px;
}
.tool-card-icon {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  background: var(--bg-card);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
}
.tool-card-icon i,
.tool-card-icon svg { width: 18px; height: 18px; }
.tool-card-meta { flex: 1; min-width: 0; }
.tool-card-name {
  font-weight: 600;
  color: var(--text-primary);
  font-size: var(--text-body);
  font-family: var(--font-mono, monospace);
}
.tool-card-sub {
  display: flex;
  gap: 12px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin-top: 2px;
}
.tool-card-method {
  font-family: var(--font-mono, monospace);
  font-weight: 600;
  color: var(--text-secondary);
}
.tool-card-url {
  font-family: var(--font-mono, monospace);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 320px;
}
.tool-card-actions { display: flex; gap: 4px; flex-shrink: 0; }
.tool-card-desc {
  margin: 8px 0 0 48px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.4;
}

/* Tool parameter rows */
.tool-params-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
.tool-param-row {
  display: grid;
  grid-template-columns: 140px 110px 1fr auto auto;
  gap: 8px;
  align-items: center;
}
.tool-param-row .form-input { padding: 6px 10px; font-size: var(--text-caption); }
/* Keeps room for the chevron the base rule paints — a flat 10px right pad
   ran the value straight under the mark. */
.tool-param-row .form-select { padding: 6px 26px 6px 10px; font-size: var(--text-caption); }

/* ─────────────────────────────────────────────────────────────────────
   Knowledge Base tab (placeholder)
   ───────────────────────────────────────────────────────────────────── */

.kb-coming-soon {
  display: flex;
  gap: 20px;
  padding: 32px;
  border-radius: 14px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
}
.kb-coming-soon-icon {
  width: 64px;
  height: 64px;
  border-radius: 14px;
  background: rgba(99, 102, 241, 0.12);
  color: rgb(129, 140, 248);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.kb-coming-soon-icon i,
.kb-coming-soon-icon svg { width: 32px; height: 32px; }
.kb-coming-soon-body { flex: 1; }
.kb-coming-soon-badge {
  display: inline-block;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(168, 85, 247, 0.12);
  color: rgb(192, 132, 252);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 8px;
}
.kb-coming-soon-body h3 { margin: 0 0 8px 0; font-size: var(--text-title); }
.kb-coming-soon-body p {
  margin: 0 0 16px 0;
  color: var(--text-secondary);
  line-height: 1.5;
}

/* ─────────────────────────────────────────────────────────────────────
   Versions tab
   ───────────────────────────────────────────────────────────────────── */

/* THE VERSIONS LIST IS A LIST, NOT FIFTY-THREE CARDS.
   Every row carried its own 1px border, 8px radius, subtle fill and 8px
   gap, inside an .agent-edit-section that already has a border, a radius
   and a fill. On an agent with real history that is fifty-three boxes
   drawn inside one box, and the frame stopped meaning anything.

   Same inset-grouped list the call rows use: no per-row chrome, a hairline
   between rows, and a hover surface that bleeds past the text so the row
   reads as one target. The section's own border does the framing. */
.version-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px var(--space-3);
  margin: 0 calc(var(--space-3) * -1);
  border-radius: var(--radius-sm);
  transition: background var(--motion-hover) var(--ease-out);
}
.version-row + .version-row,
.versions-older .version-row:first-child {
  box-shadow: inset 0 1px 0 var(--separator);
}
.version-row:hover {
  background: var(--bg-card-hover);
}
/* The hover fill and the hairline are the same object seen twice — when a
   row lights up, the rule above it belongs to the fill, not the gap. */
.version-row:hover,
.version-row:hover + .version-row {
  box-shadow: none;
}
.version-row-main {
  display: flex;
  align-items: center;
  gap: 12px;
}
.version-row-num {
  font-family: var(--font-mono, monospace);
  font-weight: 600;
  color: var(--text-primary);
  min-width: 40px;
}
.version-row-meta {
  font-size: var(--text-caption);
  color: var(--text-muted);
}

/* ─────────────────────────────────────────────────────────────────────
   Extraction tab
   ───────────────────────────────────────────────────────────────────── */

.extraction-bundle-card {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  background: var(--bg-card-subtle);
}
.extraction-bundle-card.is-disabled { opacity: 0.55; }
.extraction-bundle-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}
.extraction-bundle-title {
  margin: 0 0 4px 0;
  font-size: var(--text-body);
  font-weight: 600;
}

.extraction-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.extraction-card {
  display: flex;
  gap: 14px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-card-subtle);
}
.extraction-card-icon {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  background: var(--bg-card);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
}
.extraction-card-icon i,
.extraction-card-icon svg { width: 18px; height: 18px; }
.extraction-card-body { flex: 1; min-width: 0; }
.extraction-card-row1 {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.extraction-card-name {
  font-weight: 600;
  color: var(--text-primary);
}
.extraction-card-row2 {
  display: flex;
  gap: 10px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin-top: 2px;
}
.extraction-card-fieldname {
  font-family: var(--font-mono, monospace);
  background: var(--bg-card);
  padding: 1px 6px;
  border-radius: 4px;
  font-size: var(--text-caption);
}
.extraction-card-prompt {
  margin: 6px 0 0 0;
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.4;
}
.extraction-card-actions {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}

/* Extraction picker (Add modal) */
.extraction-picker-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.extraction-picker-item {
  width: 100%;
  text-align: left;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.extraction-picker-item:hover {
  border-color: rgb(99, 102, 241);
  background: var(--bg-card);
}
.extraction-picker-name {
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 4px;
}
.extraction-picker-prompt {
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.4;
}

/* Selector option editor (used in extraction edit modal) */
.ext-option-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 6px;
}
.ext-option-row .form-input { flex: 1; }

/* Phone numbers panel — config-missing banner.
   Shown above the search/list when window.TST_CONFIG is missing or
   contains the placeholder token. Mirrors the .pn-prov-banner pattern
   but lives at the panel level (above the search), not inside a section. */
.pn-config-banner {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 12px 14px;
  margin: 0 var(--space-4) var(--space-3) var(--space-4);
  border-radius: 10px;
  background: rgba(239, 68, 68, 0.08);
  border: 1px solid rgba(239, 68, 68, 0.3);
  color: var(--text-primary);
}
.pn-config-banner-icon {
  flex-shrink: 0;
  color: rgb(239, 68, 68);
  display: flex;
  align-items: center;
  margin-top: 2px;
}
.pn-config-banner-icon i,
.pn-config-banner-icon svg { width: 18px; height: 18px; }
.pn-config-banner-text {
  flex: 1;
  font-size: var(--text-caption);
  line-height: 1.4;
}
.pn-config-banner-text strong { display: block; margin-bottom: 4px; color: rgb(239, 68, 68); }

/* ─────────────────────────────────────────────────────────────────────
   Phase 2 polish: Tools tab compact rows + Add-tool dropdown menu
   (Phase A: agent builder polish batch 2026-05-05)
   ───────────────────────────────────────────────────────────────────── */

/* Compact tool row replaces the old .tool-card overview style. List
   shows only icon + name + edit + delete; full editor opens on click. */
.tool-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-card-subtle);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.tool-row:hover { border-color: var(--border-hover, var(--border)); background: var(--bg-card); }
.tool-row-icon {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--bg-card);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
}
.tool-row-icon i,
.tool-row-icon svg { width: 16px; height: 16px; }
.tool-row-name {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono, monospace);
  font-weight: 600;
  color: var(--text-primary);
  font-size: var(--text-body);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tool-row-actions { display: flex; gap: 4px; flex-shrink: 0; }

/* Call-control group divider: custom tools above, built-ins below.
   Hairline gradient + whisper label — separation without weight. */
.tools-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 6px 2px 2px;
}
.tools-divider-line {
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--border), transparent);
}
.tools-divider-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-secondary);
  opacity: 0.55;
  white-space: nowrap;
}

/* Add-tool dropdown menu. Anchors below the trigger button. */
.tools-add-wrap { position: relative; }
.tools-add-caret { width: 12px; height: 12px; margin-left: 4px; opacity: 0.7; }
.tools-add-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  min-width: 320px;
  z-index: var(--z-dropdown);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32), 0 2px 6px rgba(0, 0, 0, 0.16);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.tools-add-menu[hidden] { display: none; }
.tools-add-menu-item {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 10px 12px;
  border: 0;
  background: transparent;
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
  color: var(--text-primary);
  transition: background 0.15s ease;
}
.tools-add-menu-item:hover,
.tools-add-menu-item:focus-visible { background: var(--bg-card-subtle); outline: none; }
.tools-add-menu-icon {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--bg-card-subtle);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
  flex-shrink: 0;
}
.tools-add-menu-icon i,
.tools-add-menu-icon svg { width: 16px; height: 16px; }
.tools-add-menu-text { flex: 1; min-width: 0; }
.tools-add-menu-label {
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--text-primary);
  display: flex;
  align-items: center;
  gap: 6px;
}
.tools-add-menu-hint {
  display: block;
  margin-top: 2px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.3;
}
.tools-add-menu-pill {
  display: inline-block;
  padding: 1px 6px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  background: var(--bg-card-subtle);
  border-radius: 4px;
  border: 1px solid var(--border);
}

/* Built-in banner inside the tool editor modal. Tells the user this is
   a pre-made tool so the missing HTTP fields feel intentional. */
.tool-builtin-banner {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  margin-bottom: var(--space-4);
  background: rgba(99, 102, 241, 0.08);
  border: 1px solid rgba(99, 102, 241, 0.24);
  border-radius: 10px;
}
.tool-builtin-banner-icon {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: rgba(99, 102, 241, 0.18);
  color: rgb(129, 140, 248);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.tool-builtin-banner-icon i,
.tool-builtin-banner-icon svg { width: 16px; height: 16px; }
.tool-builtin-banner-text { flex: 1; min-width: 0; }
.tool-builtin-banner-title {
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--text-primary);
  margin-bottom: 2px;
}
.tool-builtin-banner-hint {
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.4;
}

/* Hint text below a per-tool toggle (typing sound, args-only). Sits
   indented under the toggle so it reads as “reason this is off/on.” */
.tool-toggle-hint {
  margin-left: 28px;
  margin-top: -4px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.4;
}

/* General tab: standalone Internal notes block at the top of the tab.
   Replaces the old "Identity" section wrapper. */
.agent-edit-internal-notes {
  margin-bottom: var(--space-5);
}

/* General tab: footer-position destructive action (Reset to template). */
.agent-edit-tab-footer {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}


/* ─────────────────────────────────────────────────────────────────────
   Phase B: 3-stop delivery slider for Inworld TTS-2
   ───────────────────────────────────────────────────────────────────── */
.form-range-stops {
  /* Visually identical to .form-range; selector exists so we can scope
     the stop-label row below it without affecting other sliders. */
}
.form-range-stop-labels {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  /* Tracks the slider's own cap — the labels are its scale, so they end
     where it ends rather than running the width of the column. */
  max-width: 320px;
}
.form-range-stop-labels span:first-child  { text-align: left; }
.form-range-stop-labels span:nth-child(2) { text-align: center; flex: 1; }
.form-range-stop-labels span:last-child   { text-align: right; }

/* ─────────────────────────────────────────────────────────────────────
   Phase C: 3-column form row (Method | URL | Timeout in tool editor).
   Replaces the dead `flex: 0 0 ...` inline hints that grid was ignoring.
   ───────────────────────────────────────────────────────────────────── */
.form-row.form-row-3col {
  grid-template-columns: 140px 1fr 130px;
}
.form-row.form-row-3col .form-group { min-width: 0; }

/* Consistent label height across a form-row so labels with info icons
   sit on the same baseline as labels without; fixes the Webhook URL
   alignment issue Roman flagged. */
.form-row .form-label {
  display: flex;
  align-items: center;
  min-height: 22px;
}

/* ─────────────────────────────────────────────────────────────────────
   Phase C2 visual polish (2026-05-05): typography + spacing + form
   states + empty states + collapsibles. Matches calendar.css quality
   conventions: -0.01em letter-spacing on titles, tabular-nums for
   numeric runs, 150ms transitions, explicit hover + disabled states.
   ───────────────────────────────────────────────────────────────────── */

/* ── Typography polish ─────────────────────────────────────────────── */
.agent-edit-section-title {
  letter-spacing: -0.01em;
  line-height: 1.3;
}
.agent-edit-tab-content .form-label-value {
  font-variant-numeric: tabular-nums;  /* No layout shift on slider live-labels */
}
.agent-edit-name {
  letter-spacing: -0.02em;
}

/* ── Spacing breathing room ────────────────────────────────────────── */
.agent-edit-section + .agent-edit-section {
  margin-top: 0;  /* The section already has margin-bottom; double-space avoided. */
}
.agent-edit-section {
  /* Slightly more vertical breathing room inside each card; matches
     calendar's section padding rhythm. */
  padding: var(--space-6) var(--space-6);
  margin-bottom: var(--space-5);
}
.agent-edit-section-title + .form-group { margin-top: 0; }

/* ── Form input states (hover, disabled, error) ────────────────────── */
/* `background` is a shorthand and it was resetting background-image to
   `none` — which meant every styled <select> in the app LOST its chevron
   for as long as the pointer was over it. Naming the one property that
   changes keeps the mark painted. */
.form-input:not(:focus):not(:disabled):hover,
.form-select:not(:focus):not(:disabled):hover,
.form-textarea:not(:focus):not(:disabled):hover {
  border-color: var(--border-hover);
  background-color: var(--bg-card);
}

.form-input:disabled,
.form-select:disabled,
.form-textarea:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  /* background-color, not the shorthand — see the hover rule above. A
     disabled select that drops its chevron and grows one back the moment
     it enables reads as a layout glitch (admin-calls loads its Client
     filter that way). */
  background-color: var(--bg-card-subtle);
}

.form-input.is-error,
.form-select.is-error,
.form-textarea.is-error {
  border-color: rgb(239, 68, 68);
}
.form-input.is-error:focus,
.form-select.is-error:focus,
.form-textarea.is-error:focus {
  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18);
}

/* Range hover/disabled moved onto the custom track + thumb in the Forms
   section. An opacity wash over the whole control dropped a disabled
   slider to ~1.7:1 and took its label with it; the parts carry their own
   disabled tokens now. */

/* ── Disabled buttons read clearly disabled (Versions tab Roll back) ── */
.btn[disabled],
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ── Empty states across tabs (consistent inline format) ──────────── */
/* .agent-edit-empty-state retired — unused (no render site emits it); the
   canonical .empty-state family covers agent-editor empty panels. */

/* ── Collapsible section wrapper for low-priority info ────────────── */
/* Used in tabs where some sections sit better collapsed by default
   (e.g. Voicemail handling, Languages & interruption). Native <details>
   styled to match agent-edit-section. */
.agent-edit-section.is-collapsible > .agent-edit-section-collapsible {
  border: 0;
}
/* Three slots: title | summary | chevron. It was flex + space-between, which
   only lands correctly when there are exactly two things in the row — with a
   summary value present, that rule pushed the value into the middle of the
   header, floating between the title it belongs to and the chevron it should
   sit beside. A grid states the intent instead: the title takes the slack,
   the summary and the chevron are adjacent at the trailing edge, and a header
   with no summary still puts its chevron in the same place. */
.agent-edit-section-collapsible > summary {
  list-style: none;
  cursor: pointer;
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-4);
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  user-select: none;
  transition: color 150ms ease;
}
.agent-edit-section-collapsible > summary::-webkit-details-marker { display: none; }
.agent-edit-section-collapsible > summary::after {
  content: '';
  width: 14px;
  height: 14px;
  /* viewBox, not just width/height: the polyline is authored in lucide's
     24-unit space, so without it the 14px canvas cropped everything past
     x=14 and the chevron rendered as a single diagonal stroke. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='14' height='14' fill='none' stroke='%239090a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  transition: transform 150ms ease;
}
.agent-edit-section-collapsible[open] > summary::after {
  transform: rotate(180deg);
}
.agent-edit-section-collapsible > summary:hover {
  color: var(--text-primary);
}
.agent-edit-section-collapsible:not([open]) > summary {
  border-bottom: 0;
  margin-bottom: 0;
  padding-bottom: 0;
}
.agent-edit-section-collapsible-summary-meta {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  letter-spacing: 0;
}

/* The blur that used to be redeclared here is now on the base rule, with
   a translucent background behind it so it actually renders. */
.autosave-chip {
  font-variant-numeric: tabular-nums;  /* "Saved 3m ago" no layout shift */
  letter-spacing: -0.005em;
}
.autosave-chip.is-saving { color: var(--text-secondary); }
.autosave-chip.is-error { color: rgb(239, 68, 68); }

/* ── Form-row: tighten label/input rhythm to match calendar density ── */
.form-group {
  margin-bottom: var(--space-5);  /* was var(--space-4) globally; +4px in agent-edit gives breathing room */
}
.agent-edit-tab-content .form-label {
  margin-bottom: var(--space-2);  /* was --space-1; matches calendar header */
  letter-spacing: -0.005em;
}

/* ── Section header consistency: even spacing for sections with right action ── */
.section-title-row .agent-edit-section-title {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
}
.section-title-row {
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}

/* ─────────────────────────────────────────────────────────────────────
   Tool editor: Behavior toggles laid out vertically (was crammed inline)
   ───────────────────────────────────────────────────────────────────── */
.behavior-toggles {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
.behavior-row { display: block; }
.behavior-row .toggle-label { display: inline-flex; }

/* ─────────────────────────────────────────────────────────────────────
   Info icon: smaller + tighter so they sit cleanly on the label baseline.
   Replaces the previous 12px size with 11px, padding tightened.
   ───────────────────────────────────────────────────────────────────── */
.info-icon {
  padding: 1px;
  margin-left: 6px;
  vertical-align: -1px;
}
.info-icon i,
.info-icon svg { width: 11px; height: 11px; }

/* Force any label that contains an info-icon to align children on the
   center axis — fixes the misalignment from icons sitting above text. */
.form-label:has(.info-icon),
label:has(> .info-icon) {
  display: inline-flex;
  align-items: center;
}

/* ─────────────────────────────────────────────────────────────────────
   Info-icon tooltip popover (2026-05-05): hover or click to show.
   Sits above other content; auto-positions above or below the trigger.
   ───────────────────────────────────────────────────────────────────── */
.info-tooltip {
  position: absolute;
  z-index: var(--z-menu);
  max-width: 320px;
  padding: 10px 12px;
  font-size: var(--text-caption, 0.75rem);
  line-height: 1.45;
  color: var(--text-primary);
  background: var(--bg-elevated, #2A2A2E);
  border: 1px solid var(--border, rgba(255, 255, 255, 0.12));
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.36), 0 1px 2px rgba(0, 0, 0, 0.18);
  pointer-events: none;
  letter-spacing: -0.005em;
  /* Start invisible so positioning happens before display. */
  opacity: 0.98;
  transition: opacity 120ms ease;
}
.info-tooltip[hidden] { display: none; }
.info-tooltip.is-sticky { pointer-events: auto; }

/* ─────────────────────────────────────────────────────────────────────
   Phase 1 (workspaces/roles 2026-05-05): role badges + role groups
   on the client detail (workspace) view.
   ───────────────────────────────────────────────────────────────────── */

/* Role badge: role-coloured pill (icon + label). The box is the canonical
   .badge (emitted alongside in admin-agents.js); this is a THIN modifier —
   a hairline role border, tracking, and icon size. The --inbound/--outbound/
   --confirmation variants below supply the per-role colour, and also serve as
   colour tokens on non-badge icon tiles (test-deploy hero, new-agent picker). */
.role-badge {
  border: 1px solid transparent;
  letter-spacing: 0.02em;
}
.role-badge i,
.role-badge svg { width: 11px; height: 11px; }

.role-badge--inbound {
  color: rgb(165, 180, 252);
  background: rgba(99, 102, 241, 0.14);
  border-color: rgba(99, 102, 241, 0.30);
}
.role-badge--outbound {
  color: rgb(252, 211, 77);
  background: rgba(217, 119, 6, 0.16);
  border-color: rgba(217, 119, 6, 0.32);
}
.role-badge--confirmation {
  color: rgb(110, 231, 183);
  background: rgba(16, 185, 129, 0.14);
  border-color: rgba(16, 185, 129, 0.30);
}

/* Role group container — wraps each role's header + agent list */
.role-group { margin-bottom: var(--space-5); }
.role-group:last-child { margin-bottom: 0; }
.role-group-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
  padding: 0 var(--space-1);
}
.role-group-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.role-group-title h3 {
  margin: 0;
  font-size: var(--text-body);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text-primary);
}
.role-group-icon {
  flex-shrink: 0;
  padding: 2px 6px;  /* Slightly tighter than a regular role-badge label */
}
.role-group-icon i,
.role-group-icon svg { width: 12px; height: 12px; }
.role-group-count {
  font-size: var(--text-caption);
  font-weight: 500;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.role-group-body {
  /* Inherits .admin-detail-section-card; nothing specific needed. */
}
.role-empty-note {
  padding: var(--space-3) var(--space-4);
  text-align: center;
  opacity: 0.65;
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
}
.admin-detail-link-btn {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  cursor: pointer;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.admin-detail-link-btn:hover { color: var(--text-primary); }

/* Agent cards: name link + version meta row */
.admin-agent-name-line {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.admin-agent-name-link {
  color: var(--text-primary);
  text-decoration: none;
  letter-spacing: -0.01em;
}
.admin-agent-name-link:hover {
  color: var(--text-primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.admin-agent-meta-line {
  margin-top: 4px;
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.admin-agent-legacy-phone {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--text-muted);
  cursor: pointer;
  text-decoration: underline dashed;
  text-underline-offset: 2px;
}
.admin-agent-legacy-phone:hover { color: var(--text-secondary); }

/* Phone number row */
.phone-row .admin-phone-number-line {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.admin-phone-number {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  font-family: var(--font-mono);
  color: var(--text-primary);
  letter-spacing: 0.01em;
  cursor: pointer;
}
.admin-phone-number:hover { color: var(--text-primary); text-decoration: underline; text-underline-offset: 2px; }

/* ─────────────────────────────────────────────────────────────────────
   Phase 1: agent edit breadcrumbs (Clients / X / Agent name)
   Only shown inside workspace context; cross-client pages stay flat.
   ───────────────────────────────────────────────────────────────────── */
.agent-edit-breadcrumbs {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 var(--space-4);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
/* The trail is the ONLY way out of the editor now that the duplicate
   "← Back to Agents" button is gone (A2), so its links have to behave like
   navigation rather than like a caption. A 13px crumb is ~18px tall, well
   under the 40px an escape hatch deserves, so each link grows its hit area
   with a pseudo-element instead of growing the type — the trail's visual
   weight is correct where it is, its target was not. Vertical only: crumbs
   sit 8px apart and overlapping targets are worse than small ones. */
.agent-edit-breadcrumb-link {
  position: relative;
  color: var(--text-secondary);
  text-decoration: none;
  letter-spacing: -0.005em;
  transition: color var(--motion-hover) var(--ease-out);
}
.agent-edit-breadcrumb-link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 40px;
  transform: translateY(-50%);
}
.agent-edit-breadcrumb-link:hover { color: var(--text-primary); }
.agent-edit-breadcrumb-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}
.agent-edit-breadcrumb-sep {
  color: var(--text-muted);
  user-select: none;
}
.agent-edit-breadcrumb-current {
  color: var(--text-primary);
  font-weight: 500;
  letter-spacing: -0.005em;
}
.agent-edit-meta-link {
  color: inherit;
  text-decoration: none;
}
.agent-edit-meta-link:hover { text-decoration: underline; text-underline-offset: 2px; }

/* ─────────────────────────────────────────────────────────────────────
   Phase 2: role selection cards in the new agent creation modal.
   3-card grid with hover + selected states. Each card uses the role
   badge color treatment for the icon chip.
   ───────────────────────────────────────────────────────────────────── */
.new-agent-role-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
@media (min-width: 720px) {
  .new-agent-role-grid { grid-template-columns: repeat(3, 1fr); }
}
.new-agent-role-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--text-primary);
  transition: border-color 150ms ease, background 150ms ease;
}
.new-agent-role-card:hover {
  border-color: var(--border-hover);
  background: var(--bg-card);
}
.new-agent-role-card.is-selected {
  border-color: var(--border-focus, #818CF8);
  background: var(--bg-card);
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12);
}
.new-agent-role-icon {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.new-agent-role-icon i,
.new-agent-role-icon svg { width: 14px; height: 14px; }
.new-agent-role-card-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.new-agent-role-card-label {
  font-weight: 600;
  letter-spacing: -0.005em;
  font-size: var(--text-sm);
}
.new-agent-role-card-desc {
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.35;
}

/* ── New Agent modal: segmented toggle + branch notes (2026-05-15) ──
   Pinned near the top of the New Agent modal. Switches between
   "Duplicate existing" and "Start blank" branches. Default state is
   Duplicate (handled in JS via .is-active class). */
.new-agent-preset-subtitle {
  margin: 0 0 var(--space-3);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.4;
}
.new-agent-toggle {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-1);
  padding: var(--space-1);
  background: var(--bg-elevated);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
}
.new-agent-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 10px var(--space-3);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-weight: 500;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.new-agent-toggle-btn:hover { color: var(--text-primary); }
.new-agent-toggle-btn.is-active {
  background: var(--bg-card-solid, var(--bg-card));
  border-color: var(--border);
  color: var(--text-primary);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.new-agent-toggle-btn i[data-lucide],
.new-agent-toggle-btn svg { width: 14px; height: 14px; }

/* Zero-agents auto-flip banner — shown when target org has no usable
   sources to duplicate from and the modal flips itself to Blank. */
.new-agent-zero-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--info-muted);
  border: 1px solid rgba(var(--info-rgb), 0.22);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: var(--text-sm);
  line-height: 1.4;
}
.new-agent-zero-note i[data-lucide],
.new-agent-zero-note svg {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  margin-top: 2px;
  color: var(--info);
}

/* Blank-path explanatory note — replaces the source dropdown when
   "Start blank" is active. */
.new-agent-blank-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--accent-muted);
  border: 1px solid rgba(var(--accent-rgb), 0.18);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: var(--text-sm);
  line-height: 1.4;
}
.new-agent-blank-note i[data-lucide],
.new-agent-blank-note svg {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  margin-top: 2px;
  color: var(--accent);
}

/* Cross-org warning — shown below the source dropdown when the
   selected source is from a different org than the target. */
.new-agent-cross-org-warning {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--warning-muted);
  border: 1px solid rgba(var(--warning-rgb), 0.28);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: var(--text-caption);
  line-height: 1.4;
}
.new-agent-cross-org-warning i[data-lucide],
.new-agent-cross-org-warning svg {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  margin-top: 2px;
  color: var(--warning);
}

/* ─────────────────────────────────────────────────────────────────────
   Phase 4 (workspaces/roles 2026-05-05): test deployment surfaces.
   - Sidebar "Currently testing" chip (always visible during active deploy)
   - Test admin page (current state + recent calls)
   - Versions tab: Test on staging button + on-test indicators
   - Test on staging modal
   ───────────────────────────────────────────────────────────────────── */

/* ── Sidebar chip ──────────────────────────────────────────────────── */
/* ─────────────────────────────────────────────────────────────────────
   Testing chip + popover (2026-05-24 rewrite, v2 visual restraint).
   "Testing" is informational, not warning. Accent-tinted, near-invisible
   chip; pulse dot does the only "live" signaling. Danger color reserved
   for the explicit confirm-to-stop moment only.
   ───────────────────────────────────────────────────────────────────── */
.tst-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 36px;
  padding: 0 12px;
  border-radius: 4px;
  background: rgba(var(--accent-rgb), 0.04);
  border: 1px solid rgba(var(--accent-rgb), 0.16);
  color: var(--text-secondary);
  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
  transition: background 150ms ease, border-color 150ms ease;
}
.tst-chip:hover {
  background: rgba(var(--accent-rgb), 0.08);
  border-color: rgba(var(--accent-rgb), 0.28);
}
.tst-chip.is-open {
  background: rgba(var(--accent-rgb), 0.10);
  border-color: rgba(var(--accent-rgb), 0.36);
}
.tst-chip-pulse {
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.4);
  animation: tst-chip-pulse 2s ease-out infinite;
}
@keyframes tst-chip-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.4); }
  70%  { box-shadow: 0 0 0 6px rgba(var(--accent-rgb), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); }
}
.tst-chip-label {
  letter-spacing: -0.005em;
}

.tst-popover {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: var(--z-dropdown);
  min-width: 240px;
  padding: 16px;
  border-radius: 8px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.tst-popover-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border-subtle);
}
.tst-popover-agent {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--text-primary);
}
.tst-popover-version {
  font-family: var(--font-mono);
  font-weight: 500;
  color: var(--text-muted);
}
.tst-popover-org {
  font-size: var(--text-caption);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tst-popover-confirm-text {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-secondary);
  padding-bottom: 4px;
}
.tst-popover-actions {
  display: flex;
  gap: 6px;
}
.tst-popover-btn {
  flex: 1;
  height: 30px;
  padding: 0 10px;
  border-radius: 4px;
  border: 1px solid var(--border-default);
  background: transparent;
  color: var(--text-primary);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.tst-popover-btn:hover {
  background: rgba(255,255,255,0.04);
  border-color: rgba(255,255,255,0.18);
}
.tst-popover-btn:disabled { opacity: 0.6; cursor: not-allowed; }
/* Danger styling — applied ONLY to the confirm-stop button in the
   confirm view, per design philosophy: danger reserved for the
   "you've explicitly stated intent" moment, not the routine action. */
.tst-popover-btn--danger {
  color: var(--pill-danger-fg);
  background: rgba(var(--danger-rgb), 0.10);
  border-color: rgba(var(--danger-rgb), 0.30);
}
.tst-popover-btn--danger:hover {
  background: rgba(var(--danger-rgb), 0.16);
  border-color: rgba(var(--danger-rgb), 0.45);
}

/* ── Test admin page: current deployment card ─────────────────────── */
.test-deploy-current {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-5);
}
.test-deploy-current-header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}
.test-deploy-current-pulse {
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: rgb(252, 211, 77);
  margin-top: 6px;
  box-shadow: 0 0 0 0 rgba(252, 211, 77, 0.7);
  animation: sidebar-test-chip-pulse 2s ease-out infinite;
}
.test-deploy-current-text { flex: 1; min-width: 0; }
.test-deploy-current-label {
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.test-deploy-current-target {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 6px;
  font-size: var(--text-h2);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--text-primary);
}
.test-deploy-current-target a {
  color: inherit;
  text-decoration: none;
}
.test-deploy-current-target a:hover { text-decoration: underline; text-underline-offset: 3px; }
.test-deploy-version-pill {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
}
.test-deploy-current-meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* Dial card */
.test-deploy-current-dial {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: 10px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
}
.test-deploy-dial-label {
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.test-deploy-dial-number {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-family: var(--font-mono);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: 0;
  cursor: pointer;
}
.test-deploy-dial-number:hover { color: var(--text-primary); text-decoration: underline; text-underline-offset: 3px; }

.test-deploy-current-actions {
  display: flex;
  gap: var(--space-2);
}

/* ── Versions tab: Test on staging button + on-test indicators ─────
   The on-test row is marked by its amber ON TEST badge alone; the 3px
   left-stripe was removed in the VF-5 sweep (owner 2026-07-03). */
.version-row-test-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: 8px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(217, 119, 6, 0.16);
  border: 1px solid rgba(217, 119, 6, 0.32);
  color: rgb(252, 211, 77);
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.version-row-test-badge i,
.version-row-test-badge svg { width: 10px; height: 10px; }
.version-row-state-text {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-style: italic;
}

/* Roll back materialises a fresh draft — a real mutation, and not one to
   leave standing on every archived row. It used to be opacity 0 until
   hover, which is the same as not being there: nothing told an operator
   the row could be rolled back at all. Half-visible at rest states the
   affordance; hover or keyboard focus commits it.
   (Promoted here from an injected <style> in js/admin-agent-edit.js, which
   only existed because this file belonged to another lane at the time.) */
.version-row .version-row-reveal {
  opacity: 0.5;
  transition: opacity var(--motion-hover) var(--ease-out);
}
.version-row:hover .version-row-reveal,
.version-row:focus-within .version-row-reveal { opacity: 1; }
@media (hover: none) {
  .version-row .version-row-reveal { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .version-row .version-row-reveal { transition: none; }
}
.versions-show-older { margin-top: var(--space-2); }
.version-test-btn.is-on-test {
  border-color: rgba(217, 119, 6, 0.40);
  color: rgb(252, 211, 77);
}

/* ── Test on staging modal ────────────────────────────────────────── */
.test-deploy-dial-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: 10px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  margin: var(--space-3) 0;
}
.test-deploy-dial-card-label {
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.test-deploy-dial-card-number {
  font-family: var(--font-mono);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
}
.test-deploy-replace-warning {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: 10px;
  background: rgba(217, 119, 6, 0.10);
  border: 1px solid rgba(217, 119, 6, 0.28);
  margin: var(--space-3) 0;
}
.test-deploy-replace-warning i,
.test-deploy-replace-warning svg {
  width: 16px;
  height: 16px;
  color: rgb(252, 211, 77);
  flex-shrink: 0;
  margin-top: 2px;
}
.test-deploy-replace-warning strong {
  color: var(--text-primary);
  font-size: var(--text-sm);
}

/* Stub state for outbound/confirmation test deploys */
.test-deploy-stub-message {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: 10px;
  background: var(--bg-card-subtle);
  border: 1px dashed var(--border);
}
.test-deploy-stub-icon {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: rgba(99, 102, 241, 0.18);
  color: rgb(129, 140, 248);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.test-deploy-stub-icon i,
.test-deploy-stub-icon svg { width: 16px; height: 16px; }

/* Card header for sub-sections */
.card-header {
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
}
.card-header h3 {
  margin: 0 0 4px;
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* 4px → --space-2 (2026-08-08, founder: the count line was tucked under the
   title). The h1 is 24px Inter Tight on a 1.15 leading, which leaves under
   2px of half-leading below it, so a 4px margin bought roughly 9px of optical
   gap against a 13px caption — close enough to read as the two lines having
   collided. One step up the spacing ladder rather than an invented 6: at
   --space-2 the pair still groups (the next block down is --space-8 away, so
   proximity is not in question) and the title stops sitting on its subtitle.

   global.css also declared .page-subtitle on the same 4px rhythm with zero
   consumers anywhere in the app; it has since been deleted, so this is the
   one subtitle rule. */
.page-header-sub {
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* ─────────────────────────────────────────────────────────────────────
   Phase 6: Sticky action bar at the bottom of the agent builder.
   Test on staging + Publish buttons always within reach; no scroll
   required to find them. Uses position: sticky which doesn't break
   page scroll behavior (vs position: fixed).
   ───────────────────────────────────────────────────────────────────── */
.agent-edit-actionbar.agent-edit-actionbar--sticky {
  position: sticky;
  bottom: 0;
  /* z-index lives on the base rule as --z-editor-actionbar; the 60 that
     used to be here was a second invented number for the same layer. */
  margin-left: calc(-1 * var(--space-6));
  margin-right: calc(-1 * var(--space-6));
  margin-top: var(--space-5);
  padding-left: var(--space-6);
  padding-right: var(--space-6);
}

/* ─────────────────────────────────────────────────────────────────────
   Polish round (2026-05-05): chip reposition, badge sizing, extractor
   tightening, version history balance, sticky-bar fixes.
   ───────────────────────────────────────────────────────────────────── */

/* Sidebar chip — sits INSIDE .sidebar-nav as the last item, right after
   the Agents link. Because the nav has `flex: 1`, putting the chip
   outside the nav (between nav and footer) caused flex to push the
   chip down against the footer. Inside the nav it sits naturally
   adjacent to the link list with the nav's natural 4px gap. */
.sidebar-test-chip-slot {
  margin-top: var(--space-3);
}
.sidebar-test-chip {
  padding: 8px 10px;
  gap: 8px;
  font-size: var(--text-caption);
}
.sidebar-test-chip-pulse {
  width: 6px;
  height: 6px;
  margin-top: 4px;
}
.sidebar-test-chip-target {
  gap: 5px;
}
.sidebar-test-chip-name {
  font-size: 12px;
}
.sidebar-test-chip-version {
  font-size: var(--text-caption);
}
.sidebar-test-chip-meta { font-size: 10px; }

/* Role badges — shrink icon + text proportions so the badge reads as
   a compact pill, not a header element. */
.role-badge {
  font-size: 10.5px;
  padding: 2px 7px;
  gap: 3px;
}
.role-badge i,
.role-badge svg { width: 9px; height: 9px; }

/* Version badge (Draft v5 / Published v4 / Archived) — same fix. */
.agent-version-badge {
  font-size: var(--text-caption);
  padding: 3px 9px;
  gap: 5px;
}
.agent-version-badge i,
.agent-version-badge svg { width: 10px; height: 10px; }

/* Info icons even smaller + more deliberate alignment. */
.info-icon {
  padding: 0;
  margin-left: 5px;
  width: 14px;
  height: 14px;
  vertical-align: middle;
}
.info-icon i,
.info-icon svg { width: 10px; height: 10px; }

/* Extractor cards — remove tier badges (handled in JS); tighten rows
   so each card is denser without losing scannability. */
.extraction-card {
  padding: 10px 12px;
  gap: 10px;
}
.extraction-card-icon {
  width: 28px;
  height: 28px;
}
.extraction-card-icon i,
.extraction-card-icon svg { width: 14px; height: 14px; }
.extraction-card-row1 {
  gap: 8px;
  align-items: center;
}
.extraction-card-name {
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.005em;
}
.extraction-card-row2 {
  gap: 8px;
  margin-top: 2px;
  align-items: center;
}
.extraction-card-fieldname {
  font-size: var(--text-caption);
  padding: 1px 6px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-secondary);
}
.extraction-card-type {
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.extraction-card-prompt { display: none; }  /* removed per request */
.extraction-card-actions {
  align-items: center;
}

/* Version history rows — better balance between number, badge, meta,
   and action buttons. Buttons on the active draft row sit right-aligned
   with consistent height. (Padding lives with the flattened base above;
   only the gap is tuned here.) */
.version-row {
  gap: 14px;
}
.version-row-num {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--text-sm);
  letter-spacing: -0.01em;
  min-width: 28px;
}
.version-row-meta {
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.version-row-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.version-row-actions .btn {
  height: 30px;
  padding: 0 10px;
  font-size: 12.5px;
  letter-spacing: -0.005em;
}
/* Deliberately smaller than the 15px base — micro actions inside a table row. */
.version-row-actions .btn i,
.version-row-actions .btn svg {
  width: 12px;
  height: 12px;
}

/* Change log — a read-only glance, so it stays quiet next to Test and
   Publish and only picks up weight on hover. It does NOT use
   .version-row-reveal: hiding it until hover is right for Roll back, which
   mutates, but wrong for something whose whole job is being noticed while
   scanning the list. */
.version-changelog-btn {
  width: 26px;
  height: 26px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 140ms ease, background 140ms ease, border-color 140ms ease;
}
.version-changelog-btn i,
.version-changelog-btn svg { width: 13px; height: 13px; }
.version-changelog-btn:hover {
  color: var(--text-primary);
  background: var(--bg-card-subtle);
  border-color: var(--border);
}
.version-changelog-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Change log modal */
.modal-changelog { max-width: 480px; border-radius: 16px; }
.modal-changelog .modal-header {
  padding: var(--space-6) var(--space-6) var(--space-3);
  border-bottom: none;
  align-items: flex-start;
}
.modal-changelog .modal-header h3 { font-size: 16px; margin: 0 0 4px; }
.modal-changelog .modal-body {
  padding: var(--space-3) var(--space-6) var(--space-5);
  max-height: 52vh;
  overflow-y: auto;
}
.modal-changelog .modal-footer {
  padding: var(--space-3) var(--space-6) var(--space-6);
  border-top: none;
}
.changelog-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.changelog-item {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  font-size: var(--text-body);
  color: var(--text-primary);
  line-height: 1.45;
}
.changelog-bullet {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
  margin-top: 8px;
}
.changelog-text { min-width: 0; word-break: break-word; }
.changelog-value {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-secondary);
}
/* The one line that can move without an operator having edited anything. */
.changelog-note {
  display: block;
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin-top: 2px;
}
.changelog-empty {
  margin: 0;
  font-size: var(--text-body);
  color: var(--text-secondary);
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .version-changelog-btn { transition: none; }
}
/* Test deployments page — tighten the spacing inside the current card */
.test-deploy-current { gap: var(--space-4); padding: var(--space-4); }
.test-deploy-current-target { font-size: var(--text-h3); gap: 8px; }
.test-deploy-version-pill { font-size: var(--text-caption); padding: 1px 7px; }
.test-deploy-current-dial {
  padding: var(--space-3) var(--space-4);
}
.test-deploy-current-pulse {
  width: 8px;
  height: 8px;
  margin-top: 5px;
}
.test-deploy-current-label { font-size: 10px; }

/* Sticky action bar — make Test on staging and Publish buttons fit
   better. Add icon size constraint so the lucide icons don't dominate. */
.actionbar-right .btn i,
.actionbar-right .btn svg {
  width: 12px;
  height: 12px;
}

/* ─────────────────────────────────────────────────────────────────────
   Agents page — client folder grid (replaces flat table 2026-05-06).
   Each folder = one client; click opens admin-clients?slug=X workspace
   detail where agents live grouped by role.
   ───────────────────────────────────────────────────────────────────── */
/* Loading placeholder, sized to the chip so the grid doesn't collapse from
   tall cards to short ones when data lands. Rendered via renderSkeletons'
   generic branch, which emits <div class="skeleton <type>">. */
.skeleton.agents-folder-skel {
  height: 52px;
  border-radius: 10px;
}
.agents-folder-grid {
  display: grid;
  /* BOUNDED track, packed left — not 1fr. Three workspaces made both obvious
     options bad: auto-fill + 1fr left ~900px of dead tracks on the right,
     while auto-fit + 1fr stretched each chip to ~390px of mostly-empty pill.
     A capped track keeps chips chip-sized at any client count, and the
     leftover space reads as air now that the full-width .card wrapper that
     used to frame (and highlight) the emptiness is gone. */
  grid-template-columns: repeat(auto-fill, minmax(200px, 260px));
  justify-content: start;
  gap: var(--space-3);
}
/* Compact chip — icon + name only, DELIBERATELY (Roman 2026-07-26: he likes
   the dense new design inside the drawer, not on the grid). This was briefly
   a tall stat card in the 2026-07-25 pass; reverted to the chip he prefers,
   but now as an explicit rule rather than a late override that display:none'd
   the extra markup. See _renderClientFolders — the markup is gone too, so
   there is nothing to hide. */
.agents-folder-card {
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
  text-decoration: none;
  color: var(--text-primary);
  transition: border-color 150ms ease, background 150ms ease;
  min-height: 52px;
}
.agents-folder-card:hover {
  border-color: var(--border-hover);
  background: var(--bg-card-hover);
}
.agents-folder-card-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
}
.agents-folder-icon {
  width: 28px;
  height: 28px;
  border-radius: 7px;
  /* Was hardcoded indigo (rgba(99,102,241,.12) on rgb(165,180,252)) — a dark-
     theme pair that left a pale lavender glyph on a near-white tile in light
     mode. Tokens track both themes. */
  background: var(--accent-muted);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
/* lucide REPLACES the <i> with an <svg>, so an `i`-only rule stops matching
   the moment icons hydrate and the glyph falls back to its intrinsic 24px —
   which would overflow the 28px tile entirely. Size both. */
.agents-folder-icon i,
.agents-folder-icon svg { width: 14px; height: 14px; }
.agents-folder-name {
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ── NO RENDER SITE (Roman 2026-07-26) ────────────────────────────────
   The stat / breakdown / production / meta lines below are not emitted by
   _renderClientFolders any more — he wants the grid to stay icon + name.
   Kept, not deleted: the folder model still computes all of it, and this is
   the second time the card's density has been flipped, so the cheapest path
   back is intact CSS. Nothing here is reachable today.
   ─────────────────────────────────────────────────────────────────────── */
.agents-folder-stat {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-top: var(--space-2);
}
.agents-folder-stat-num {
  font-family: var(--font-mono);
  font-size: var(--text-h1);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}
.agents-folder-stat-label {
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.agents-folder-breakdown {
  font-size: var(--text-caption);
  color: var(--text-secondary);
}
.agents-folder-meta {
  margin-top: auto;
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Agency Internal folder — sandbox workspace, pinned to top. The amber wash +
   amber icon carry the sandbox signal; the 3px stripe + edge gradient were
   removed in the VF-5 sweep (owner 2026-07-03).

   2026-07-25: amber alone read as an unexplained color difference against the
   accent-tinted client folders. The distinction is now carried by GLYPH first
   — this card renders `flask-conical`, clients render `folder` (see
   _renderClientFolders) — with the amber tile agreeing with the amber glyph
   instead of sitting on an accent tile, plus a title on the card. Color is
   reinforcement, never the only signal. */
/* A dashed border, because Agency Internal is not a client — it is the
   sandbox. Every other card in this grid represents a real org with real
   traffic, and a 4% amber wash was carrying that distinction alone: at that
   alpha it reads as "this card is slightly highlighted", not as "this one is
   a different KIND of thing". Dashed is the app's existing convention for a
   provisional or non-production container (the Kanban drop zone draws one),
   so the difference is stated in a language already on the page rather than
   in a second tint. The wash stays as reinforcement.

   The dash is the ONE-pixel border the base card already draws, restyled —
   not an added stroke — so the card's box model, radius and hover are
   untouched and it still sits on the grid's baseline exactly like its
   siblings. */
.agents-folder-card--internal {
  background: color-mix(in srgb, var(--warning) 4%, var(--bg-card));
  border-style: dashed;
  border-color: color-mix(in srgb, var(--warning) 34%, var(--border));
}
.agents-folder-card--internal:hover {
  border-color: color-mix(in srgb, var(--warning) 52%, var(--border));
}
/* (A `.agents-folder-card--internal .agents-folder-card-header` override used
   to re-declare display/align-items and tighten gap to 8px to fit the old
   INTERNAL pill. The pill is long gone and the base header already supplies
   display+align-items, so all it did was misalign the pinned card's icon by
   4px against every sibling chip. Removed 2026-07-26.) */
.agents-folder-card--internal .agents-folder-icon {
  background: var(--warning-muted);
  color: var(--pill-warning-fg);
}
.agents-folder-badge {
  /* Compact inline tag (design-sprint T4.3): sized so "Agency Internal"
     + tag fit the row card without truncating either. */
  flex-shrink: 0;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--pill-warning-fg);
  padding: 2px 5px;
  border: 1px solid color-mix(in srgb, var(--warning) 50%, transparent);
  border-radius: 999px;
  margin-left: auto;
  white-space: nowrap;
}
/* When the badge IS the folder identifier (Agency Internal), promote
   it to primary visual weight — larger + more letter-spacing, matching
   the perceived weight of a folder name without losing the badge cue. */
.agents-folder-badge--primary {
  font-size: 12px;
  padding: 4px 11px;
  letter-spacing: 0.05em;
}

/* Outside-Internal warning bands — admin-test-deployments + agent-edit modal */
.test-deploy-outside-warning,
.modal-warning-band {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 12px 14px;
  margin-bottom: 14px;
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
  background: color-mix(in srgb, var(--warning) 8%, var(--bg-elevated));
  border-radius: var(--radius-md);
  color: var(--text-primary);
  font-size: var(--text-body);
  line-height: 1.5;
}
.test-deploy-outside-warning i,
.test-deploy-outside-warning svg,
.modal-warning-band i,
.modal-warning-band svg {
  color: var(--warning);
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 2px;
}

/* KB picker org grouping — header above each group of KBs from one org. */
.kb-tab-picker-group + .kb-tab-picker-group {
  margin-top: 12px;
}
.kb-tab-picker-group-header {
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 6px 4px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}

/* Cross-org duplicate warning — fires inside the Duplicate modal when
   destination workspace differs from source. Visual consistency with
   the test-deploy outside-Internal warning. */
.dup-cross-org-warning {
  margin-top: 8px;
  margin-bottom: var(--space-4);
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
  background: color-mix(in srgb, var(--warning) 8%, var(--bg-elevated));
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  font-size: var(--text-body);
  line-height: 1.5;
}

/* ─────────────────────────────────────────────────────────────────────
   Description / form-hint spacing fixes (2026-05-06).
   The dashboard had inconsistent margins between hints and adjacent
   inputs/labels. Standardizing so the visual rhythm is predictable.
   ───────────────────────────────────────────────────────────────────── */
.agent-edit-tab-content .form-hint {
  margin-top: 6px;
  line-height: 1.45;
  letter-spacing: -0.005em;
}
.agent-edit-tab-content .form-hint:first-child { margin-top: 0; }
.agent-edit-tab-content .form-hint + .form-hint { margin-top: 4px; }

/* A7 — the prose gets a measure. Help text ran the full content column, so a
   two-line hint under a 200px select was one 150-character line the eye had
   to sweep back across. 72ch is the reading measure for this size; the system
   prompt keeps its own 96ch, because it is a document being edited rather
   than a caption being read (see .prompt-textarea).
   F5 — and every paragraph in here is help text, so none of them should end
   on an orphan. */
.agent-edit-tab-content .form-hint,
.agent-edit-tab-content .agent-edit-managed-copy p,
.agent-edit-tab-content .empty-state--line p,
.agent-edit-tab-content .agency-settings-subtitle {
  max-width: 72ch;
}
.agent-edit-tab-content p,
.agent-edit-managed-copy p,
.modal-copy-spaced {
  text-wrap: pretty;
}

/* The header badges row — name input + role + status + version + cost.
   Force consistent vertical alignment so the icons don't drift above
   the text. */
.agent-edit-title-row {
  align-items: center;
  gap: var(--space-3);
}
.agent-edit-title-row .badge,
.agent-edit-title-row .status-pill {
  flex-shrink: 0;
}

/* Test deployments page — compact the dial card horizontally so the
   number doesn't feel lost inside an oversized card. */
.test-deploy-current-dial {
  max-width: 480px;
}

/* ─────────────────────────────────────────────────────────────────────
   Polish round 2 (2026-05-06): redesigns from screenshot feedback.
   - Tiny minimalist sidebar chip (no amber card, no chunky icon)
   - Smaller folder cards (just icon + name)
   - Slimmer role group section icons (more breathing room)
   - Stripped-down new-agent modal (no subtext, lighter role cards)
   ───────────────────────────────────────────────────────────────────── */

/* === Sidebar "Currently testing" chip — minimalist redesign === */
.sidebar-test-chip-slot {
  margin: var(--space-2) 10px 0;
}
.sidebar-test-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  text-decoration: none;
  transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
  /* Width fits content — tiny pill, not a banner */
  width: fit-content;
  max-width: 100%;
}
.sidebar-test-chip:hover {
  color: var(--text-primary);
  border-color: var(--border-hover);
  background: var(--bg-card-subtle);
}
.sidebar-test-chip-pulse {
  flex-shrink: 0;
  width: 5px;
  height: 5px;
  margin: 0;
  border-radius: 50%;
  background: rgb(99, 102, 241);  /* indigo, matches dashboard accent */
  box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.6);
  animation: sidebar-test-chip-pulse 2s ease-out infinite;
}
@keyframes sidebar-test-chip-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.55); }
  70%  { box-shadow: 0 0 0 5px rgba(99, 102, 241, 0); }
  100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
}
/* Old `.sidebar-test-chip` ::before/::after injection hack removed
   2026-05-24 — replaced by .tst-chip rendered directly by sidebar.js. */

/* === Workspace role group section headers — slimmer icons === */
.role-group-icon {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.role-group-icon i,
.role-group-icon svg { width: 11px; height: 11px; }

/* The "minimalist (icon + name only)" folder-card override was RETIRED in the
   2026-07-25 agents design pass. It squashed every card to a ~44px chip and
   `display: none`-d the count, role breakdown and last-activity line — data
   the page already pays to fetch (fetchAgentVersionCounts + fetchAgentLastCalls
   run on every load). Three chips in a full-width grid is what made the page
   read 80% empty, and hiding operator signal inverts density > explanation.
   The canonical block above (~line 10565) is now the single source of truth. */

/* === New-agent modal — subtler role cards === */
.new-agent-role-grid {
  gap: var(--space-2);
}
.new-agent-role-card {
  padding: var(--space-3);
  gap: var(--space-2);
  align-items: center;  /* vertical center, not top */
}
.new-agent-role-icon {
  width: 26px;
  height: 26px;
  border-radius: 7px;
}
.new-agent-role-icon i,
.new-agent-role-icon svg { width: 12px; height: 12px; }
.new-agent-role-card-label {
  font-size: var(--text-sm);
  letter-spacing: -0.005em;
}
/* Hide the per-card description line — the role label is self-explanatory */
.new-agent-role-card-desc { display: none; }
/* Hide the helper paragraphs above/below the role grid */
.new-agent-role-helper-hint,
#new-agent-role + p.form-hint { display: none; }

/* ─────────────────────────────────────────────────────────────────────
   Polish round 3 (2026-05-06):
   - Sidebar chip removed entirely; indicator now lives in admin-agents
     page header next to "Phone Numbers" button.
   - Role group section header icons removed (HTML-side); fix vertical
     spacing now that the icon is gone.
   ───────────────────────────────────────────────────────────────────── */

/* Page-header testing chip slot — anchor for the .tst-popover drop. */
.agents-test-chip-slot {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.agents-test-chip-slot[hidden] { display: none; }

/* Role group title — no longer has icon block, reduce gap so the title
   sits flush left with the section card below it. */
.role-group-title {
  gap: 6px;
  padding-left: 2px;
}
.role-group-title h3 {
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.005em;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}
.role-group-count {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
}
/* Slightly more breathing room between section header and the agent
   card list — was looking compressed. */
.role-group-header { margin-bottom: 8px; }
.role-group { margin-bottom: var(--space-6); }

/* ─────────────────────────────────────────────────────────────────────
   Polish round 4 (2026-05-06):
   - Test deployments page: minimalist hero redesign (no inner dial frame,
     status pulse + bigger phone number, ghost "Clear" button).
   - Workspace agents section: remove duplicate eyebrow, drop count, align
     role group labels flush-left with section title, fix "+ Add inbound"
     stacking (was wrapping plus icon onto a separate line), and add more
     breathing room above the section.
   ───────────────────────────────────────────────────────────────────── */

/* Test deployments — surface containers (replace generic .card) */
.test-deploy-surface {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl, 14px);
}
.test-deploy-surface + .test-deploy-surface {
  margin-top: var(--space-5);
}
.test-deploy-surface--quiet {
  background: transparent;
  border-color: var(--border-subtle, var(--border));
}

/* Hero block (currently-deployed view) */
.test-deploy-hero {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-7);
}
.test-deploy-hero-status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  letter-spacing: 0.005em;
  align-self: flex-start;
}
.test-deploy-status-pulse {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgb(132, 204, 22);
  box-shadow: 0 0 0 0 rgba(132, 204, 22, 0.55);
  animation: sidebar-test-chip-pulse 2s ease-out infinite;
  flex-shrink: 0;
}
.test-deploy-status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-muted);
  opacity: 0.6;
  flex-shrink: 0;
}
.test-deploy-hero-status--idle { color: var(--text-muted); }

.test-deploy-hero-target {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0;
  font-size: var(--text-h2);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  line-height: 1.15;
}
.test-deploy-hero-target a {
  color: inherit;
  text-decoration: none;
}
.test-deploy-hero-target a:hover {
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-color: rgba(255, 255, 255, 0.25);
}

.test-deploy-hero-meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
}

.test-deploy-hero-dial {
  margin-top: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.test-deploy-hero-dial-label {
  font-size: var(--text-overline);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}
.test-deploy-hero-dial-number {
  align-self: flex-start;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  font-family: var(--font-mono);
  font-size: 32px;
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  cursor: pointer;
  line-height: 1.1;
  transition: color 150ms ease, text-decoration-color 150ms ease;
}
.test-deploy-hero-dial-number:hover {
  text-decoration: underline;
  text-underline-offset: 6px;
  text-decoration-color: rgba(129, 140, 248, 0.5);
}
.test-deploy-hero-dial-number--muted {
  color: var(--text-secondary);
}

.test-deploy-hero-clear {
  align-self: flex-start;
  margin-top: var(--space-3);
  color: var(--text-muted);
}
.test-deploy-hero-clear:hover {
  color: var(--text-primary);
}

/* Recent test calls — quieter list, no divider line */
.test-deploy-recent {
  padding: var(--space-6) var(--space-7);
}
.test-deploy-recent-title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--text-secondary);
  letter-spacing: -0.005em;
}
.test-deploy-recent-empty {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* Workspace agents section — fix alignment + + Add button stacking */
.workspace-agents-section {
  margin-top: var(--space-9);
}
.workspace-agents-section .role-group-header {
  padding: 0;
}
.workspace-agents-section .role-group-title {
  padding-left: 0;
}
/* Inline-flex the text actions so the lucide SVG sits next to the label
   instead of wrapping onto its own line. nowrap protects long labels. */
.admin-detail-text-action,
.role-group .add-agent-by-role-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  line-height: 1;
}
.admin-detail-text-action i,
.admin-detail-text-action svg,
.role-group .add-agent-by-role-btn i,
.role-group .add-agent-by-role-btn svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* ─────────────────────────────────────────────────────────────────────
   Production deployment modal (2026-07-26)
   One job, one surface: the staging→prod promote arm. Inherits the shell
   the Agency Settings modal used to own (that tuning UI was removed with
   its every-org save path); sized down to 480px since the body is a
   two-node flow plus one arming row.
   ───────────────────────────────────────────────────────────────────── */

.modal-deploy {
  max-width: 480px;
  border-radius: 16px;
  /* Spring-style overshoot on open. Subtle — doesn't feel bouncy, just
     alive. The base modalEnter is scale 0.95→1 ease-out; we override
     with a curve that overshoots to 1.02 then settles. */
  animation: deployModalEnter var(--motion-dialog-enter) var(--ease-spring-out);
}

@keyframes deployModalEnter {
  from {
    opacity: 0;
    transform: scale(0.93) translateY(12px);
    filter: blur(6px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
    filter: blur(0);
  }
}

.modal-deploy .modal-header {
  padding: var(--space-7) var(--space-7) var(--space-3);
  border-bottom: none;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  position: relative;
}

.modal-deploy .modal-header h3 {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}

.agency-settings-subtitle {
  font-size: var(--text-body);
  color: var(--text-muted);
  font-weight: 400;
  letter-spacing: 0;
  margin: 0;
}

.modal-deploy .modal-close {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  min-width: 32px;
  min-height: 32px;
  padding: 4px;
  border-radius: 8px;
}
.modal-deploy .modal-close svg,
.modal-deploy .modal-close i {
  width: 16px;
  height: 16px;
}

/* Body aligns to the header's --space-7 gutter; no footer (the promote
   button is content, not a dialog action), so the bottom pad closes it. */
.modal-deploy .modal-body {
  padding: var(--space-2) var(--space-7) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* ── Promote arm — Phase 4 ⑤ ────────────────────────────────────────────
   The body of .modal-deploy. No card chrome: the weight lives in the
   typed-PROMOTE arm + the warning line, not borders. (Was an accordion row
   inside Agency Settings until 2026-07-26; flattened when it became the
   modal's only content.) */
.agency-promote-hint {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.5;
  margin: 0;
}
.agency-promote-flow {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
.agency-promote-node { display: flex; flex-direction: column; gap: 3px; }
.agency-promote-node-k {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
}
.agency-promote-node-v {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.agency-promote-arrow { width: 14px; height: 14px; color: var(--text-muted); flex-shrink: 0; }
.agency-promote-arm { display: flex; gap: var(--space-2); }
.agency-promote-input {
  flex: 1;
  min-width: 0;
  height: 36px;
  padding: 0 var(--space-3);
  background: var(--bg-input);
  border: 1px solid var(--border-default);
  border-radius: 8px;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.10em;
}
.agency-promote-input::placeholder {
  font-family: var(--font-body);
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-muted);
}
/* No `outline: none` here — see the .form-input note above. At (0,2,0) it
   outranks `input:focus-visible` (0,1,1) and deletes the keyboard ring; the
   global `*:focus { outline: none }` already suppresses the mouse case and
   correctly loses to focus-visible. */
.agency-promote-input:focus { border-color: var(--border-hover); }
/* The promote button is the modal's secondary button (class="btn
   btn-secondary agency-promote-btn"): it inherits padding/radius/hover/active
   and the standard .btn:disabled (opacity 0.5). We only keep it from wrapping
   and let it read a touch more confidently once PROMOTE is typed. */
.agency-promote-btn { white-space: nowrap; }
.agency-promote-btn:not(:disabled) {
  border-color: var(--border-hover);
  color: var(--text-primary);
}
.agency-promote-status {
  font-size: 12px;
  line-height: 1.5;
  padding: var(--space-2) var(--space-3);
  border-radius: 8px;
}
.agency-promote-status[data-tone="pending"] { color: var(--text-secondary); background: var(--bg-card-subtle); }
.agency-promote-status[data-tone="success"] { color: #8FE0A8; background: rgba(90, 200, 130, 0.09); }
.agency-promote-status[data-tone="error"]   { color: #E6ABA4; background: rgba(220, 130, 120, 0.08); }
.agency-promote-status[data-tone="fatal"]   { color: #F1B79C; background: rgba(232, 130, 86, 0.11); border: 1px solid rgba(232, 130, 86, 0.32); font-weight: 600; }

/* Switch (toggle) — for recording_enabled */
.agency-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  user-select: none;
}
.agency-switch input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}
.agency-switch-track {
  position: relative;
  width: 36px;
  height: 20px;
  background: var(--border);
  border-radius: 999px;
  transition: background 200ms var(--ease-out);
}
.agency-switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,0.20);
  transition: transform 220ms var(--ease-spring-soft);
}
.agency-switch input:checked + .agency-switch-track {
  background: var(--accent);
}
.agency-switch input:checked + .agency-switch-track::after {
  transform: translateX(16px);
}
.agency-switch input:focus-visible + .agency-switch-track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.agency-switch-label {
  font-size: var(--text-body);
  color: var(--text-primary);
}

/* Agents page header actions.
   Deploy / Phone Numbers / Knowledge Bases are all .btn-secondary peers at
   full weight — hierarchy is carried by the single .btn-primary (New agent),
   not by dimming one of them. Sizing the svg (not the <i>, which lucide
   replaces) keeps every icon in the row matched. */
.agents-header-actions .btn i,
.agents-header-actions .btn svg {
  width: 16px;
  height: 16px;
}
/* .btn sets display:inline-flex, which outranks the UA [hidden] rule — the
   Deploy button is agency_admin-only and must actually disappear. */
.agents-header-actions .btn[hidden] { display: none; }

/* Agency-managed inline panel — used inside the agent builder where a
   per-agent control was migrated to organization_settings. Subtle, not
   loud. Reads as "this is intentionally read-only here, edit elsewhere". */
.agent-edit-managed-panel {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-5);
  border-radius: 12px;
  background: var(--bg-elevated);
  border: 1px dashed var(--border);
  margin-bottom: var(--space-5);
}
.agent-edit-managed-panel--inline {
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-body);
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.agent-edit-managed-icon {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: rgba(var(--accent-rgb), 0.1);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.agent-edit-managed-icon svg,
.agent-edit-managed-panel--inline svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--accent);
}
.agent-edit-managed-copy {
  flex: 1;
  min-width: 0;
}
.agent-edit-managed-copy h3 {
  margin: 0 0 4px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.005em;
}
.agent-edit-managed-copy p {
  margin: 0 0 var(--space-3);
  font-size: var(--text-body);
  color: var(--text-secondary);
  line-height: 1.5;
}
.agent-edit-managed-panel--inline .agent-edit-managed-copy {
  font-size: var(--text-body);
  color: var(--text-secondary);
  line-height: 1.5;
}
.agent-edit-managed-panel--inline .agent-edit-managed-copy strong {
  color: var(--text-primary);
  font-weight: 600;
}
.agent-edit-managed-panel--inline .agent-edit-managed-copy code {
  background: var(--bg-card);
  padding: 1px 6px;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-primary);
}
.agent-edit-managed-current {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.agent-edit-managed-label {
  display: block;
  font-size: var(--text-overline);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 6px;
  font-weight: 600;
}
.agent-edit-managed-values {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px var(--space-3);
  font-size: var(--text-body);
  margin: 0;
}
.agent-edit-managed-values dt {
  color: var(--text-muted);
  font-weight: 500;
}
.agent-edit-managed-values dd {
  margin: 0;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 12px;
}

/* Inline annotation appended to a managed value when an agency-level
   override is in effect (i.e., the displayed value comes from
   organization_settings, not from agent_versions.config). Subdued
   styling — the badge is supplemental context, not an alarm. Only
   rendered when org override differs from per-version fallback. */
.agent-edit-managed-source {
  font-family: var(--font-body);
  font-size: var(--text-caption);
  font-weight: 400;
  color: var(--text-muted);
  margin-left: 4px;
  white-space: nowrap;
}

/* ─────────────────────────────────────────────────────────────────────
   Test on staging modal — premium polish (2026-05-07)
   ───────────────────────────────────────────────────────────────────── */
.modal-test-deploy {
  max-width: 480px;
  border-radius: 16px;
}
.modal-test-deploy .modal-header {
  padding: var(--space-6) var(--space-6) var(--space-3);
  border-bottom: none;
  align-items: flex-start;
}
.modal-test-deploy .modal-header h3 {
  font-size: 16px;
  margin: 0 0 4px;
}
.modal-test-deploy .modal-body {
  padding: var(--space-3) var(--space-6) var(--space-5);
}
.modal-test-deploy .modal-footer {
  padding: var(--space-3) var(--space-6) var(--space-6);
  border-top: none;
}


/* ─────────────────────────────────────────────────────────────────────
   Test on staging — confirm + live states (redesign 2026-08-25)

   Two states, deliberately unalike. The confirm step is dense and quiet:
   one target row, one warning band only when something is actually
   displaced, and notes as muted lines. The live step is the payoff and is
   allowed to be the loudest thing in the flow, because it is the only
   screen the operator has to act on.
   ───────────────────────────────────────────────────────────────────── */

/* Confirm: what is going on the line. */
.test-deploy-target {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: 12px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
}
.test-deploy-target-icon {
  width: 36px;
  height: 36px;
  border-radius: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.test-deploy-target-icon i,
.test-deploy-target-icon svg { width: 17px; height: 17px; }
.test-deploy-target-meta { flex: 1; min-width: 0; }
.test-deploy-target-name {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.test-deploy-target-sub {
  font-size: var(--text-caption);
  color: var(--text-secondary);
  margin-top: 1px;
}

/* Notes are context, not consequences — a muted list, never a band. The
   "not in Agency Internal" line fires on every client agent, and as an
   amber band it taught the eye to skip the band shape, which is exactly
   the shape the replacement warning needs to keep working. */
.test-deploy-notes {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.test-deploy-notes li {
  position: relative;
  padding-left: 14px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.5;
}
.test-deploy-notes li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 7px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-muted);
  opacity: 0.6;
}

/* Live: the payoff. The dot rides in the modal title so state is stated
   once, in the strongest position, instead of on its own status row
   repeating what the title already says. */
.modal-test-deploy .modal-header h3 .test-deploy-live-dot {
  display: inline-block;
  vertical-align: middle;
  margin-right: 8px;
  margin-top: -2px;
}
.test-deploy-live-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgb(74, 222, 128);
  box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16);
  /* Completion feedback. The old success state had a spring-scaled check
     mark; this is the same beat, at the weight the new layout wants. */
  animation: testDeployDotIn 320ms var(--ease-spring-out);
}
@keyframes testDeployDotIn {
  from { opacity: 0; transform: scale(0.2); }
  to   { opacity: 1; transform: scale(1); }
}

/* The number, with both routes to a handset attached to it: copy, and a
   tel: link. It used to be plain text in a div, so the flow ended one step
   before the operator could do anything with it and they dialled from
   memory instead — which is where testing quietly stopped happening. */
.test-deploy-dial {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-4) var(--space-5);
  border-radius: 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
}
.test-deploy-dial-meta { flex: 1; min-width: 0; }
.test-deploy-dial-caption {
  display: block;
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.test-deploy-dial-num {
  display: block;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  /* Large display type reads too loose at default tracking. */
  font-size: 27px;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin-top: 3px;
  user-select: all;
}
.test-deploy-dial-copy,
.test-deploy-dial-go {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  transition: background 160ms ease, color 160ms ease, transform 120ms ease;
}
.test-deploy-dial-copy i,
.test-deploy-dial-copy svg,
.test-deploy-dial-go i,
.test-deploy-dial-go svg { width: 18px; height: 18px; }
.test-deploy-dial-copy {
  background: transparent;
  border-color: var(--border);
  color: var(--text-secondary);
  padding: 0;
}
.test-deploy-dial-copy:hover {
  color: var(--text-primary);
  background: var(--bg-card-subtle);
}
.test-deploy-dial-copy.is-copied {
  color: rgb(74, 222, 128);
  border-color: rgba(34, 197, 94, 0.35);
}
.test-deploy-dial-go {
  background: var(--accent);
  color: var(--text-inverse);
}
.test-deploy-dial-go:hover { background: var(--accent-hover); }
/* Feedback on press, not on release. */
.test-deploy-dial-copy:active,
.test-deploy-dial-go:active { transform: scale(0.94); }

@media (prefers-reduced-motion: reduce) {
  .test-deploy-live-dot { animation: none; }
  .test-deploy-dial-copy,
  .test-deploy-dial-go { transition: none; }
  .test-deploy-dial-copy:active,
  .test-deploy-dial-go:active { transform: none; }
}

/* ─────────────────────────────────────────────────────────────────────
   Extraction sync modal + actions row (2026-05-07)
   ───────────────────────────────────────────────────────────────────── */
.extraction-actions-row {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  flex-wrap: wrap;
}
.extraction-actions-row .btn-ghost {
  color: var(--text-secondary);
}
/* When the card is empty the add affordance is the empty state's own call to
   action, so it sits under the centred line rather than at the left edge of a
   row that has nothing in it. */
.empty-state--line + .extraction-actions-row { justify-content: center; }
.extraction-actions-row .btn-ghost:hover {
  color: var(--text-primary);
}

.modal-extractor-sync {
  max-width: 520px;
  border-radius: 14px;
}
.modal-extractor-sync .modal-body {
  padding: var(--space-3) var(--space-6) var(--space-5);
}
.extractor-sync-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 320px;
  overflow-y: auto;
  margin-top: var(--space-3);
  padding: var(--space-2);
  background: var(--bg-elevated);
  border-radius: 10px;
}
.extractor-sync-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 10px 12px;
  border-radius: 8px;
  cursor: pointer;
  transition: background var(--motion-hover) var(--ease-out);
}
.extractor-sync-row:hover {
  background: var(--bg-card-solid);
}
.extractor-sync-row input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 4px;
  border: 1.5px solid var(--text-muted);
  margin: 0;
  cursor: pointer;
  position: relative;
  flex-shrink: 0;
  transition: background var(--motion-hover) var(--ease-out),
              border-color var(--motion-hover) var(--ease-out);
}
.extractor-sync-row input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.extractor-sync-row input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 5px;
  height: 9px;
  border: solid white;
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}
.extractor-sync-row-name {
  flex: 1;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
}
.extractor-sync-row-role {
  font-size: var(--text-caption);
  padding: 2px 8px;
}

/* Tools tab — actions wrapper that holds Sync from… + Add tool side by side */
.tools-actions-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Single-extractor sync (2026-05-08) — list rendered as <ul>, no
   checkboxes (sync is automatic across all matching agents). Skipped
   targets dim and show an inline note. */
.modal-extractor-sync ul.extractor-sync-list {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: var(--space-2);
}
.modal-extractor-sync li.extractor-sync-row {
  cursor: default;
}
.extractor-sync-row-org {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-family: var(--font-mono);
}
.extractor-sync-row-skip {
  font-size: var(--text-caption);
  color: var(--warning, #d97706);
  font-style: italic;
}
.extractor-sync-row--skipped {
  opacity: 0.6;
}

/* Per-extractor sync — bucketed preview (WS2 Phase 1, 2026-05-11).
   Each bucket is a labeled section with its own header + list. Update
   bucket reads neutral; skip buckets read muted to make the "will
   update" the natural visual focus. */
.extractor-sync-bucket {
  margin-top: var(--space-4);
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-elevated);
  overflow: hidden;
}
.extractor-sync-bucket-head {
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  background: var(--bg-card-solid);
  border-bottom: 1px solid var(--border);
}
.extractor-sync-bucket-list {
  list-style: none;
  margin: 0;
  padding: var(--space-2);
}
.extractor-sync-bucket-list li {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 8px 12px;
  border-radius: 6px;
}
.extractor-sync-bucket-list li:hover {
  background: var(--bg-card-solid);
}
.extractor-sync-row-detail {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-family: var(--font-mono);
  flex-shrink: 0;
}

/* "Will update" bucket — neutral but the visual focus of the modal */
.extractor-sync-bucket.is-update .extractor-sync-bucket-head {
  color: var(--text-primary);
}

/* Skip buckets — muted so they don't compete with the update list. */
.extractor-sync-bucket.is-skip {
  opacity: 0.75;
}
.extractor-sync-bucket.is-skip .extractor-sync-row-name {
  color: var(--text-secondary);
}

/* Already-matches bucket lives inside <details> and starts collapsed. */
.extractor-sync-bucket.is-already-matches {
  padding: 0;
}
.extractor-sync-bucket.is-already-matches > summary {
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  background: var(--bg-card-solid);
  cursor: pointer;
  list-style: none;
}
.extractor-sync-bucket.is-already-matches > summary::-webkit-details-marker {
  display: none;
}
.extractor-sync-bucket.is-already-matches[open] > summary {
  border-bottom: 1px solid var(--border);
}

/* Tool sync modal — pad the body and give the description paragraph
   real breathing room before the cascade selects so the layout doesn't
   feel crammed. The default modal-body padding is tighter than what
   this longer-form modal needs. */
.modal-tool-sync .modal-body {
  padding: var(--space-4) var(--space-6) var(--space-5);
}
.modal-tool-sync .agency-settings-subtitle {
  margin: 0 0 var(--space-5);
  line-height: 1.55;
}
.modal-tool-sync .form-row {
  margin-top: 0;
  gap: var(--space-4);
}
.modal-tool-sync .form-group {
  margin-bottom: 0;
}

/* Tool sync modal — REPLACE preview with side-by-side add/remove lists */
.tool-sync-preview {
  margin-top: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
  background: var(--bg-elevated);
  border-radius: 10px;
}
.tool-sync-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.tool-sync-heading {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.tool-sync-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.tool-sync-list li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px 10px;
  background: var(--bg-card-solid);
  border-radius: 6px;
  font-size: var(--text-body);
}
.tool-sync-list code {
  font-family: var(--font-mono);
  color: var(--text-primary);
}
.tool-sync-list--remove li code {
  text-decoration: line-through;
  color: var(--text-muted);
}
.tool-sync-pill {
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(var(--accent-rgb), 0.12);
  color: var(--text-secondary);
  font-family: var(--font-mono);
}
/* .tool-sync-empty retired — the tool-sync picker empty renders through the
   canonical .empty-state--line. */

/* Tool sync modal — selective picker: a checklist of the source agent's
   tools. Operator checks the ones to merge into the current agent. */
.tool-sync-picker {
  margin-top: var(--space-4);
  padding: var(--space-3);
  background: var(--bg-elevated);
  border-radius: 10px;
}
.tool-sync-picker-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 0 2px var(--space-2);
  margin-bottom: var(--space-2);
  border-bottom: 1px solid var(--border-subtle);
}
.tool-sync-count {
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.tool-sync-checklist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 320px;
  overflow-y: auto;
}
.tool-sync-checkitem {
  display: flex;
}
.tool-sync-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: 7px 10px;
  background: var(--bg-card-solid);
  border-radius: 6px;
  font-size: var(--text-body);
  cursor: pointer;
}
.tool-sync-check:hover {
  background: var(--bg-card-hover, var(--bg-card-solid));
}
/* Geometry + colour now come from the shared custom control; this fork
   existed only to bump the native box to 15px and tint it. */
.tool-sync-check code {
  font-family: var(--font-mono);
  color: var(--text-primary);
}
.tool-sync-check--all {
  width: auto;
  padding: 0;
  background: none;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.tool-sync-check--all:hover {
  background: none;
}
.tool-sync-tag {
  margin-left: auto;
  font-size: 10px;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 999px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.tool-sync-tag--new {
  background: rgba(var(--success-rgb, 34, 197, 94), 0.14);
  color: var(--success, #22c55e);
}
.tool-sync-tag--overwrite {
  background: rgba(var(--warning-rgb, 234, 179, 8), 0.16);
  color: var(--warning, #eab308);
}

/* Versions tab — rollback button styling */
.version-rollback-btn {
  font-size: 12px;
  padding: 4px 10px;
  color: var(--text-secondary);
}
.version-rollback-btn:hover {
  color: var(--text-primary);
}
/* .btn-qualified so it still outranks the .btn svg base (this rule was already
   in svg form and therefore already live — without the bump the new base
   would have silently grown it 12px → 15px). */
.btn.version-rollback-btn svg {
  width: 12px;
  height: 12px;
}

/* ─────────────────────────────────────────────────────────────────────
   Badge variants (2026-05-07; base reconciled 2026-08-07)
   ─────────────────────────────────────────────────────────────────────
   This block used to open with a SECOND `.badge` base that redeclared
   every property the first one set — so it silently won for every badge
   the JS emits, and any attempt to tune chips at the top of the file was
   dead on arrival. It is gone: the one base lives with .tag / .pill near
   the top of this file (search "THE CHIP PRIMITIVE"), and what remains
   here is what was only ever declared here — the dot/text/tint tokens,
   the icon rule, the shape modifiers and the double-dash variants.

   Pattern:
     <span class="badge">label</span>                      neutral
     <span class="badge badge--accent badge--pill">…       subtle indigo, pill shape
   STATUS is not a badge. "Live v7", "indexed", "pass" and every other
   what-state-is-it chip is the .status-pill primitive (PROMOTED PRIMITIVES,
   below); the badge is for categories and counts. The role badge stays a
   badge because role is a category, not a state (U4). The Linear-style
   .badge--status dot pattern and .badge--muted were retired with it.
   ───────────────────────────────────────────────────────────────────── */

:root {
  /* The status dot is gone with .badge--status; this one hue survives as
     the .kb-tab-warn notice border. */
  --badge-dot-warning: rgb(245, 158, 11);   /* amber-500 */

  /* Filled-badge text colors (used on tinted backgrounds, lighter shade) */
  --badge-text-success: rgb(74, 222, 128);
  --badge-text-warning: rgb(251, 191, 36);
  --badge-text-danger:  rgb(248, 113, 113);
  --badge-text-info:    rgb(129, 140, 248);

  /* Tint backgrounds — kept low-opacity so they read as accent, not loud */
  --badge-bg-success: rgba(16, 185, 129, 0.10);
  --badge-bg-warning: rgba(245, 158, 11, 0.10);
  --badge-bg-danger:  rgba(239, 68, 68, 0.10);
  --badge-bg-info:    rgba(var(--accent-rgb), 0.10);
}

.badge i, .badge svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  opacity: 0.85;
}

/* Shape modifiers. --compact tightens horizontally only: the 24px height
   is the system, and a 20px chip beside a 24px chip was half of what the
   "two chips in one row differ 2px" finding was measuring. */
.badge--compact { padding: 0 6px; gap: 4px; }
.badge--pill { border-radius: 999px; padding: 0 12px; }

/* Color variants — used for FILLED badges, NOT status indicators. Fill and
   hairline are derived from --chip-hue by the primitive. */
.badge--accent  { --chip-hue: var(--accent);  color: var(--badge-text-info); }
.badge--success { --chip-hue: var(--success); color: var(--badge-text-success); }
.badge--warning { --chip-hue: var(--warning); color: var(--badge-text-warning); }
.badge--danger  { --chip-hue: var(--danger);  color: var(--badge-text-danger); }
.badge--accent i, .badge--success i, .badge--warning i, .badge--danger i,
.badge--accent svg, .badge--success svg, .badge--warning svg, .badge--danger svg {
  opacity: 1;
}

/* Role-icon coloring inside accent/pill badges — subtle indigo tint
   so all role badges share a visual language. Icon shape distinguishes role. */
.badge--accent i,
.badge--accent svg { color: var(--accent); opacity: 0.95; }

/* The admin-clients phone-type badge forces the unified accent styling on top
   of its role-badge--<key> colour hook. The role list badge (admin-agents),
   by contrast, is a plain .badge + role-badge--<key> and keeps the per-role
   colour. Hero icon tiles carry role-badge--<key> without .badge and are
   unaffected either way. */
.badge.badge--accent.role-badge--inbound,
.badge.badge--accent.role-badge--outbound,
.badge.badge--accent.role-badge--confirmation {
  background: var(--badge-bg-info);
  border-color: rgba(var(--accent-rgb), 0.22);
  color: var(--badge-text-info);
}

/* Faded modifier for "viewed/resolved" states (e.g., follow-up that's been
   addressed). Keeps the badge visible but de-emphasized. */
.badge--faded { opacity: 0.45; }

/* ============================================================
 * Knowledge Bases panel (admin-only, opened from admin-agents.html)
 * Phase 3 Gate A — list view, Create/Rename/Settings/Delete flows.
 * Doc detail view ships in Phase 3 Gate B.
 * ============================================================ */

/* Panel modal — wide, list-oriented (vs the narrow deploy modal) */
.modal-kb-panel {
  max-width: 720px;
  width: 90vw;
}

/* Smaller form modals reuse default modal sizing — these are slight tweaks */
.modal-kb-create   { max-width: 520px; width: 90vw; }
.modal-kb-settings { max-width: 520px; width: 90vw; }
.modal-kb-small    { max-width: 420px; width: 90vw; }
.modal-kb-confirm  { max-width: 480px; width: 90vw; }

/* Toolbar: search input + Create button */
.kb-panel-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.kb-panel-search {
  position: relative;
  flex: 1;
  min-width: 0;
}
.kb-panel-search .form-input {
  padding-left: 36px;
}
.kb-panel-search-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: var(--text-muted);
  pointer-events: none;
}

/* List container */
.kb-panel-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-height: 60vh;
  overflow-y: auto;
  padding-right: 2px; /* room for scrollbar without shifting cards */
}
.kb-panel-loading {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
}

/* Each KB card */
.kb-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border-subtle);
  border-radius: 12px;
  padding: var(--space-3) var(--space-4);
  transition: border-color 150ms ease, background 150ms ease;
}
.kb-card:hover {
  border-color: var(--border);
  background: var(--bg-card-hover);
}

.kb-card-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-bottom: var(--space-1);
}
.kb-card-name {
  flex: 1;
  margin: 0;
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--text-primary);
  /* Single-line truncation with ellipsis for long names */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.kb-card-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
  position: relative;
}
/* Manage button picks up a chevron-right icon for visual weight on the
   card's right edge. (KB-1) Without this, the card's right edge feels
   sparse with just a single text button. The chevron also reinforces
   the "drill into detail view" semantic. */
.kb-card-manage-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.kb-card-manage-btn i,
.kb-card-manage-btn svg {
  width: 14px;
  height: 14px;
}

.kb-card-stats {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: 2px;
}
.kb-card-time {
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin-bottom: 0;
}
.kb-card-description {
  margin: var(--space-2) 0 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-style: italic;
  /* Two-line clamp */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ⋯ dropdown menu (per-card) */
.menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: var(--z-menu);
  min-width: 160px;
  background: var(--bg-card, var(--bg-elevated));
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.menu.hidden {
  display: none;
}
.menu [role="menuitem"] {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 6px;
  background: transparent;
  border: none;
  color: var(--text-primary);
  text-align: left;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background 100ms ease;
}
.menu [role="menuitem"]:hover,
.menu [role="menuitem"]:focus-visible {
  background: var(--bg-control-hover);
  outline: none;
}
.menu [role="menuitem"] i,
.menu [role="menuitem"] svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--text-muted);
}
.menu [role="menuitem"].menu-item--danger {
  color: var(--danger);
}
.menu [role="menuitem"].menu-item--danger i,
.menu [role="menuitem"].menu-item--danger svg {
  color: currentColor;
}
.menu [role="menuitem"].menu-item--danger:hover {
  background: rgba(239, 68, 68, 0.08);
}

/* Advanced settings details/summary in Create modal */
.kb-advanced {
  margin-top: var(--space-2);
  border-top: 1px solid var(--border-subtle);
  padding-top: var(--space-3);
}
.kb-advanced-summary {
  display: flex;
  flex-direction: column;
  gap: 2px;
  cursor: pointer;
  font-weight: 500;
  font-size: var(--text-body);
  color: var(--text-primary);
  padding: 6px 0;
  list-style: none;
}
.kb-advanced-summary::-webkit-details-marker { display: none; }
.kb-advanced-summary::after {
  content: '';
  width: 12px;
  height: 12px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%2371717a' d='M4.5 6l3.5 4 3.5-4z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  position: absolute;
  right: 0;
  top: 12px;
  transition: transform 150ms ease;
}
.kb-advanced { position: relative; }
.kb-advanced[open] .kb-advanced-summary::after { transform: rotate(180deg); }
.kb-advanced-defaults {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-weight: 400;
}
.kb-advanced-body {
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Range value display chip (shown next to slider label) */
/* A value you cannot change should not be wearing an input's clothes.
   Bordered, filled and centred, these read as text fields — an operator
   clicks one, nothing happens, and the read-only hint eighty pixels below
   is the only thing that ever said so. Borderless fill, left-aligned, and
   the section head carries the Read-only badge instead. */
.form-value-display {
  display: inline-block;
  padding: 2px var(--space-2);
  margin: 0;
  background: var(--bg-card-subtle);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-caption);
  font-weight: 500;
  color: var(--text-secondary);
  text-align: left;
}

/* Required-field marker */
.form-required {
  color: rgb(239, 68, 68);
  font-weight: 600;
  margin-left: 2px;
}

/* Inline link-style button for retry / soft actions */
.btn-link {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent, var(--text-primary));
  text-decoration: underline;
  cursor: pointer;
  font-size: inherit;
  font-family: inherit;
}
.btn-link:hover { opacity: 0.85; }

/* Delete confirm copy */
.kb-confirm-body {
  margin: 0 0 var(--space-3) 0;
  font-size: var(--text-body);
  color: var(--text-primary);
  line-height: 1.5;
}
.kb-confirm-body:last-child { margin-bottom: 0; }
.kb-confirm-soft-note {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ============================================================
 * KB detail view — Phase 3 Gate B
 * Drilldown from KB list when "Manage" is clicked.
 * Header (description + stats), Upload/AddURL toolbar, documents table.
 * ============================================================ */

.kb-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.kb-detail-back {
  margin-bottom: var(--space-1);
}
.kb-detail-back .btn-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}
.kb-detail-back .btn-link:hover {
  color: var(--text-primary);
  text-decoration: underline;
}
.kb-detail-back .btn-link i,
.kb-detail-back .btn-link svg {
  width: 14px;
  height: 14px;
}

.kb-detail-header {
  border-bottom: 1px solid var(--border-subtle);
  padding-bottom: var(--space-3);
}
.kb-detail-name {
  margin: 0 0 var(--space-2) 0;
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
}
.kb-detail-description {
  margin: 0 0 var(--space-2) 0;
  font-size: var(--text-body);
  color: var(--text-secondary);
  font-style: italic;
  line-height: 1.5;
}
.kb-detail-stats {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: 4px;
}
.kb-detail-failed {
  color: var(--danger);
  font-weight: 500;
}
.kb-detail-config {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-family: var(--font-mono, ui-monospace, monospace);
}
.kb-detail-config code {
  background: var(--bg-elevated);
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: var(--text-caption);
}

.kb-detail-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.kb-detail-toolbar-spacer {
  flex: 1;
  min-width: var(--space-3);
}

/* Confirm modal — list of impacts and affected agents (2026-05-07) */
.kb-confirm-bullets {
  margin: var(--space-2) 0;
  padding-left: var(--space-5);
  color: var(--text-primary);
  font-size: var(--text-sm);
}
.kb-confirm-bullets li { margin: var(--space-1) 0; }
/* KB-2: agent-impact list polished — bullet-less, monospace agent
   names, tighter rhythm. Reads as a scannable name list rather than
   a generic prose enumeration. */
.kb-confirm-agent-list {
  margin: var(--space-2) 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  list-style: none;
  background: var(--bg-elevated);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-primary);
}
.kb-confirm-agent-list li {
  margin: 0;
  padding: 2px 0;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.kb-confirm-agent-list li::before {
  content: "";
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-muted);
  flex-shrink: 0;
}
.kb-confirm-muted {
  color: var(--text-muted);
  font-style: italic;
}

/* Documents table */
.kb-docs-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.kb-docs-table thead th {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  font-weight: 500;
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--border-subtle);
  background: transparent;
  white-space: nowrap;
}
.kb-docs-table tbody td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border-subtle);
  vertical-align: top;
}
.kb-docs-table tbody tr:last-child td {
  border-bottom: none;
}
.kb-docs-table tbody tr:hover td {
  background: var(--bg-elevated);
}

.kb-docs-col-num     { text-align: right; white-space: nowrap; }
.kb-docs-col-actions { width: 1%; white-space: nowrap; text-align: right; }

.kb-doc-name-cell {
  max-width: 0;
  width: 100%;
}
.kb-doc-name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 500;
  color: var(--text-primary);
  /* truncate long names */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.kb-doc-name span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kb-doc-source-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--text-muted);
}
.kb-doc-status-message {
  margin-top: 2px;
  font-size: var(--text-caption);
  color: var(--danger);
  font-style: italic;
  line-height: 1.4;
  /* clamp 2 lines for readability */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
}
.kb-doc-type {
  color: var(--text-muted);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-caption);
  white-space: nowrap;
}
.kb-doc-time {
  color: var(--text-muted);
  white-space: nowrap;
  font-size: 12px;
}
.kb-doc-actions {
  position: relative;
  display: inline-flex;
  align-items: center;
}

/* Processing state pulse — gentle attention without being annoying. The
   whole pill breathes: the status-pill has no dot to carry it. */
@keyframes kb-status-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}
.status-pill.kb-status-pulse {
  animation: kb-status-pulse 1.6s ease-in-out infinite;
}

/* ============================================================
 * Agent builder Knowledge Base tab — Phase 3 Gate C
 * Three sections: master enable, attached KBs picker, retrieval settings.
 * Disabled-state visual when master toggle is off.
 * ============================================================ */

/* Master toggle row */
.kb-tab-master {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.kb-tab-master .toggle-label {
  font-size: var(--text-body);
  font-weight: 500;
}

/* Disabled-state: visually mute child sections when master is off.
   Master toggle stays interactive; everything below is dimmed + non-clickable. */
.kb-tab-disabled {
  opacity: 0.45;
  pointer-events: none;
  user-select: none;
}

/* Strategy radio cards (stuff / retrieve) */
.kb-tab-strategy-options {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.kb-tab-strategy-option {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  background: var(--bg-card);
  transition: border-color 0.12s ease, background 0.12s ease;
}
.kb-tab-strategy-option:hover { border-color: var(--border-hover); }
.kb-tab-strategy-option.is-selected {
  border-color: var(--accent);
  background: var(--accent-muted);
}
.kb-tab-strategy-option input[type="radio"] { margin-top: 4px; flex: 0 0 auto; }
.kb-tab-strategy-option-body { display: flex; flex-direction: column; gap: 4px; }
.kb-tab-strategy-option-title {
  font-weight: 500;
  font-size: var(--text-body);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.kb-tab-strategy-option-desc {
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.45;
}
/* Tiny uppercase tag over the canonical .badge — thin modifier keeps the
   xs uppercase read; the .badge-accent / .badge-neutral variant carries the
   fill (muted-tint, not the old solid accent, per "status is information"). */
.kb-tab-strategy-pill {
  /* Was the phantom --text-xs, so the "xs uppercase read" this comment
     describes never happened — it inherited .badge's size. */
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Token meter — green/yellow/red bands beneath strategy */
.kb-tab-token-meter {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  font-size: var(--text-sm);
}
.kb-tab-token-meter strong { font-weight: 600; }
.kb-tab-token-meter-msg { color: var(--text-muted); margin-top: 2px; }
.kb-tab-token-meter--green {
  border-color: var(--pill-success-border);
  background: var(--pill-success-bg);
}
.kb-tab-token-meter--green > i,
.kb-tab-token-meter--green > svg { color: var(--pill-success-fg); }
.kb-tab-token-meter--yellow {
  border-color: var(--pill-warning-border);
  background: var(--pill-warning-bg);
}
.kb-tab-token-meter--yellow > i,
.kb-tab-token-meter--yellow > svg { color: var(--pill-warning-fg); }
.kb-tab-token-meter--red {
  border-color: var(--pill-danger-border);
  background: var(--pill-danger-bg);
}
.kb-tab-token-meter--red > i,
.kb-tab-token-meter--red > svg { color: var(--pill-danger-fg); }

/* Attached-KB cards (scannable: name + stats only) */
.kb-tab-attached-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.kb-tab-attached-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  padding: var(--space-3);
}
.kb-tab-attached-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: 4px;
}
.kb-tab-attached-name {
  flex: 1;
  font-weight: 500;
  font-size: var(--text-body);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.kb-tab-attached-stats {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: 2px;
}
.kb-tab-attached-time {
  font-size: var(--text-caption);
  color: var(--text-muted);
}

/* Inline notices (warning when enabled-no-attachments, info empty states) */
.kb-tab-warn {
  border-color: var(--badge-dot-warning, rgb(245, 158, 11));
}

/* Attach button row */
.kb-tab-attach-row {
  margin-top: var(--space-2);
}

/* Picker modal — list of selectable KB rows */
.kb-tab-picker-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-height: 50vh;
  overflow-y: auto;
  padding-right: 2px;
}
.kb-tab-picker-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--space-3);
  background: var(--bg-elevated);
  border: 1px solid var(--border-subtle);
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
  transition: border-color 100ms ease, background 100ms ease;
}
.kb-tab-picker-row:hover,
.kb-tab-picker-row:focus-visible {
  border-color: var(--border);
  background: var(--bg-control-hover);
  outline: none;
}
.kb-tab-picker-name {
  font-weight: 500;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kb-tab-picker-stats {
  font-size: var(--text-caption);
  color: var(--text-muted);
}

/* ── Agent edit: production star + danger zone (added 2026-05-11) ─── */
.ge-prod-star {
  width: 14px;
  height: 14px;
  margin-left: 6px;
  color: var(--text-muted);
  vertical-align: middle;
  transition: color 180ms var(--ease-out), fill 180ms var(--ease-out);
}
/* Same token as the drawer + folder-card production stars — one glyph, one
   hue, theme-aware (the #f5a623 literal was dark-theme-only). */
.ge-prod-star--active {
  color: var(--pill-warning-fg);
  fill: var(--pill-warning-fg);
}

/* Inline star next to the production toggle on the agent edit General tab.
   Sized to match the toggle track height (20px) so the row reads as one
   balanced unit. The parent .ge-production-row's gap handles spacing — no
   margin-left needed here. */
.ge-prod-row-star {
  width: 20px;
  height: 20px;
  margin-left: 0;
}

/* Production row: toggle + inline star side-by-side, vertically centered. */
.ge-production-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* ── Production + Danger zone polish (WS1 refinement, 2026-05-11) ─────
   Target the agent-edit sections by data-section attribute so we don't
   alter unrelated agent-edit-section cards. Goals: more breathing room
   between cards, softer red on the danger zone, tighter typography
   rhythm, more pronounced danger-button hover. Reference quality bar:
   GitHub settings danger zones + Stripe dashboard settings cards. */

.agent-edit-section[data-section="production"] {
  margin-top: var(--space-6);
  padding: var(--space-6);
}

.agent-edit-section[data-section="production"] .form-hint {
  margin-top: var(--space-2);
  line-height: 1.55;
}

.agent-edit-section[data-section="danger-zone"] {
  margin-top: var(--space-5);
  padding: var(--space-6);
}

/* Subtler red border on the danger card — drop from 30% to 22% of the
   danger color. Also lighten the gradient bg so the card reads as a soft
   warning, not a stop sign. */
.agent-edit-section[data-section="danger-zone"].danger-zone-card-v2 {
  border-color: color-mix(in srgb, var(--danger) 22%, var(--border));
  background: linear-gradient(180deg, rgba(var(--danger-rgb), 0.035), rgba(var(--danger-rgb), 0.010));
}

/* Tighter rhythm inside the single danger action: label → hint gap is now
   2px (was 4px) so the two lines read as one unit; gap to the right-aligned
   button stays at space-4 for clear separation. */
.agent-edit-section[data-section="danger-zone"] .danger-zone-action {
  gap: var(--space-4);
  /* Centred, the button drifted down past the two-line hint and stopped
     reading as the action for the label above it. */
  align-items: flex-start;
}
.agent-edit-section[data-section="danger-zone"] .danger-zone-action-text .font-medium {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.4;
}
.agent-edit-section[data-section="danger-zone"] .danger-zone-action-text p {
  margin-top: 2px;
  line-height: 1.5;
}

/* When the star renders inside a workspace agent row icon button, neutralize
   the inline-toggle sizing it inherits from the edit-page context. The button
   gives it the same 24×24 visual weight as its sibling lucide icons
   (external-link, copy), centered by the button's flex layout. */
.admin-detail-icon-btn .ge-prod-star {
  width: 24px;
  height: 24px;
  margin-left: 0;
  vertical-align: baseline;
}

/* Outline star uses the same muted tone as sibling icon-btn defaults so the
   row reads as one consistent action group, then transitions to text-primary
   on row-hover via the existing .admin-detail-icon-btn:hover rule. Active
   (filled gold) state is already handled by .ge-prod-star--active. */
.production-star-btn:not(:hover) .ge-prod-star:not(.ge-prod-star--active) {
  color: var(--text-muted);
}
.form-hint--warn {
  color: var(--pill-warning-fg);
}
.modal-impact-list {
  margin: var(--space-3) 0;
  padding-left: var(--space-4);
  line-height: 1.6;
}
.modal-impact-list li {
  margin-bottom: 4px;
}
/* `.text-muted` USED to be redeclared here, and it is why 38 em-dash
   placeholders on the calls table rendered at full #F0F0F3 instead of the
   muted tier — the audit's "the class isn't winning". It was winning: it
   set `color: var(--text-muted)` and `font-size: var(--text-sm)`,
   and NEITHER TOKEN EXISTS anywhere in the app (the canonical names are
   --text-muted and --text-sm). An invalid var() reference is invalid at
   computed-value time, so both properties fell back to `inherit` — the
   cell's own primary text colour. Same specificity as the real utility in
   global.css and 12k lines later, so it won and then erased itself.
   Deleted; the canonical utility now applies.

   NOTE for a later wave, found while diagnosing this: --color-text-muted
   is one of ~14 tokens from a namespace that was never defined
   (--color-surface, --color-border, --text-small, --text-base, --bg-subtle,
   --font-sans …), used in roughly 60 declarations that are all silently
   inert. Deliberately NOT switched on here — defining them would turn on
   60 untested declarations across pages this wave cannot verify. */

/* ═══════════════════════════════════════════════════════════════════════
   Client agents drawer (WS-NAV, 2026-05-11)
   Right-side sliding panel on the admin-agents page. Houses the agent
   management surface that previously lived on admin-clients (workspace
   page). Reuses the existing .panel-overlay + .panel-sheet primitives;
   these rules style only the drawer's INTERNAL chrome (header, role
   sections, agent rows, indicators) so the panel-sheet itself behaves
   identically to the call-detail panel (440px, slide-in animation, mobile
   full-screen fallback).
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Scrim per role (E5) ─────────────────────────────────────────────────
   A BROWSE drawer and a destructive-publish confirm were rendering behind
   the identical scrim: --glass-backdrop-bg at 35%/20% black plus a 12px
   blur, because .panel-overlay and .modal-overlay share one recipe. So the
   heaviest treatment the app owns — dim the page, blur it out of legibility
   — was being spent on "have a look at this client's agents", which is the
   most reversible thing on the page.

   Apple's rule is the one that decides it: DIM TO FOCUS, SEPARATE TO KEEP
   FLOW. A modal task interrupts, so it dims and pushes the page back. A
   parallel, non-blocking panel must not break the flow — it separates with
   translucency and elevation instead, and the page stays readable behind it,
   because the operator's next move is often "close this and open the next
   one". Only the browse drawer is re-graded; every .modal-overlay in the
   agents area keeps the full modal scrim, which is now a distinction that
   means something.

   The blur goes entirely rather than being reduced: a half-blur is the one
   value that reads as a rendering fault rather than as a material. */
.panel-overlay.client-agents-drawer {
  background: rgba(0, 0, 0, 0.14);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
[data-theme="light"] .panel-overlay.client-agents-drawer {
  background: rgba(0, 0, 0, 0.08);
}
/* Losing the scrim's weight means the sheet has to carry its own separation,
   which is what a non-modal panel does anyway. */
.client-agents-drawer .panel-sheet {
  box-shadow: var(--shadow-xl);
}
@media (prefers-contrast: more) {
  .panel-overlay.client-agents-drawer { background: rgba(0, 0, 0, 0.32); }
}

/* Content-sized drawer. As a full-height rail, a typical workspace (three role
   sections + the bench expander) left ~300px of empty sheet below the content,
   so the drawer's visual weight was set by the viewport instead of by what's
   in it. Now it hugs: top-right anchored, floats off both edges, capped so tall
   workspaces scroll inside .drawer-body rather than growing past the viewport.
   min-height keeps the skeleton→content swap from popping.
   The mobile full-bleed variant (.panel-overlay.panel-mobile .panel-sheet —
   three classes) outranks this two-class rule, so phones are untouched. */
.client-agents-drawer .panel-sheet {
  display: flex;
  flex-direction: column;
  padding: 0;
  top: var(--space-4);
  right: var(--space-4);
  bottom: auto;
  height: auto;
  min-height: 420px;
  max-height: calc(100dvh - var(--space-4) * 2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;  /* clip the sticky header to the radius; body scrolls */
  /* The shared panelIn/panelOut slide by exactly 100% of the sheet's width,
     which would now leave a 16px sliver on screen at the start of the travel
     because the sheet rests 16px off the edge. Scoped keyframes add the inset
     back. (The exit rule below matches .panel-overlay.is-closing
     .panel-sheet's specificity and wins on source order.) */
  animation-name: clientDrawerIn;
}
.client-agents-drawer.is-closing .panel-sheet {
  animation: clientDrawerOut var(--motion-layout) var(--ease-in) forwards;
}
@keyframes clientDrawerIn {
  from { transform: translateX(calc(100% + var(--space-4))); }
  to   { transform: translateX(0); }
}
@keyframes clientDrawerOut {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(100% + var(--space-4))); }
}

.client-agents-drawer .drawer-header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-5) var(--space-4);
  background: var(--bg-card-solid);
  border-bottom: 1px solid var(--border);
}

.client-agents-drawer .drawer-header-main {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
}

.client-agents-drawer .drawer-header-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  background: var(--bg-elevated);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
  flex-shrink: 0;
}

/* `i` alone stops matching once lucide swaps in the <svg> — every icon below
   was rendering at lucide's intrinsic 24px (a 24px arrow beside 11px text in
   the workspace link, a 24px glyph in a 32px duplicate button). */
.client-agents-drawer .drawer-header-icon i,
.client-agents-drawer .drawer-header-icon svg {
  width: 18px;
  height: 18px;
}

.client-agents-drawer .drawer-header-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.client-agents-drawer .drawer-header-name {
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.client-agents-drawer .drawer-header-workspace-link {
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.client-agents-drawer .drawer-header-workspace-link:hover {
  color: var(--text-primary);
}

.client-agents-drawer .drawer-header-workspace-link i,
.client-agents-drawer .drawer-header-workspace-link svg {
  width: 12px;
  height: 12px;
}

/* .drawer-close-btn retired to .btn.btn-ghost.btn-icon; class kept as JS bind
   hook + for the shared ::after tap-target and flex-shrink below. */
.client-agents-drawer .drawer-close-btn { flex-shrink: 0; }

.client-agents-drawer .drawer-body {
  flex: 1;
  min-height: 0;  /* lets the flex child actually scroll at max-height */
  overflow-y: auto;
  padding: var(--space-4) var(--space-5) var(--space-5);
}

.client-agents-drawer .drawer-skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4) 0;
}

.client-agents-drawer .drawer-role-section {
  margin-bottom: var(--space-5);
}

.client-agents-drawer .drawer-role-section:last-child {
  margin-bottom: 0;
}

/* 12px, not 8. The "+ Add" button's 44px expander is centred on a ~22px
   control, so it reaches 11px below the header — past an 8px margin and into
   the first agent row, where it stole the top edge of that row's own target.
   12 clears it with a pixel to spare, and the extra air reads as intentional
   section spacing rather than a fix. */
.client-agents-drawer .drawer-role-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.client-agents-drawer .drawer-role-title {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}

.client-agents-drawer .drawer-role-title h3 {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}

.client-agents-drawer .drawer-role-count {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
}

.client-agents-drawer .drawer-role-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  /* Sunk below the sheet, not floated on it. At --bg-card the body sat
     eight units off --bg-sheet and the border was doing all the work; the
     subtle step reads as a well the rows sit inside. */
  background: var(--bg-card-subtle);
  padding: var(--space-1);
}

/* Empty role groups keep the SAME frame as filled ones: one quiet
   .empty-state--line inside the bordered body. Supersedes AD-11's separate
   collapsed line, which solved the "tall empty box" problem by introducing a
   second component: three sibling sections then rendered in two different
   shapes, and the drawer read as three unrelated treatments stacked. */

/* Add action — one treatment in every section header. Bare text button, so it
   needs an invisible 44px hit area (make-interfaces-feel-better) and a hover
   surface; the negative margin keeps the label optically flush with the
   section body's right edge despite the new padding. */
.client-agents-drawer .drawer-add-agent-btn {
  position: relative;
  padding: var(--space-1) var(--space-2);
  margin-right: calc(var(--space-2) * -1);
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--ease-out);
}
.client-agents-drawer .drawer-add-agent-btn:hover {
  background: var(--accent-muted);
}
.client-agents-drawer .drawer-add-agent-btn::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  min-width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

/* min-height, because the row had none and its floor was therefore whichever
   child happened to be tallest — the 32px duplicate button on most rows, but
   the two-line text block on any row whose name line grew (a star, a `legacy`
   badge). A list whose row height depends on its contents reads as ragged.
   44px is the coarse-pointer floor the rest of the app already uses, and it
   clears the 32px button and the two-line block alike.
   position: relative anchors the stretched link below. */
.client-agents-drawer .drawer-agent-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--ease-out);
}

.client-agents-drawer .drawer-agent-row:hover {
  background: var(--bg-elevated);
}

/* The whole row lit up on hover while only the inner <a> navigated, so the
   row's padding, its gap and the strip beside the duplicate button were all
   promising a click they could not honour. The hover zone was right and the
   hit area was wrong: the link stretches to fill the row, and the duplicate
   button is lifted above it so it keeps its own target. */
.client-agents-drawer .drawer-agent-main::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
.client-agents-drawer .drawer-duplicate-btn {
  position: relative;
  z-index: 1;
}

.client-agents-drawer .drawer-agent-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-decoration: none;
  color: inherit;
  padding: 2px 0;
}

.client-agents-drawer .drawer-agent-name-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.client-agents-drawer .drawer-agent-name {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Production star — the drawer row's ONLY glyph since the active/draft dot
   came out (Roman 2026-07-26: it was green on every row, so it discriminated
   nothing). It sits inline right after the agent name rather than in a
   right-aligned rail: a lone star pinned to the far edge read as unanchored
   next to the duplicate button, and a rail would reserve empty width on every
   non-production row. Inline + flex-shrink:0 means unstarred rows reserve
   nothing at all.
   The old #f5a623 literal was theme-blind: --pill-warning-fg is #E8B257 on
   dark (~10:1) and #B45309 on light (~5.9:1 on white). */
.client-agents-drawer .drawer-agent-production-star,
.agents-folder-production-star {
  color: var(--pill-warning-fg);
  fill: var(--pill-warning-fg);
}
/* The star is a SPAN wrapping the glyph now (it is the only node that can
   carry a working `title` — see js/admin-agents.js), so the size has to reach
   the glyph inside it rather than stopping at the box. */
.client-agents-drawer .drawer-agent-production-star {
  display: inline-flex;
  align-items: center;
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}
.client-agents-drawer .drawer-agent-production-star > i,
.client-agents-drawer .drawer-agent-production-star > svg {
  width: 12px;
  height: 12px;
}

.client-agents-drawer .drawer-agent-meta-line {
  font-size: var(--text-caption);
  color: var(--text-muted);
}

.client-agents-drawer .drawer-duplicate-btn {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
}

.client-agents-drawer .drawer-duplicate-btn i,
.client-agents-drawer .drawer-duplicate-btn svg {
  width: 16px;
  height: 16px;
}

/* ── Callsim Bench section (collapsed <details>) ──────────────────────
   Not .advanced-expander: that's the agent builder's boxed "advanced
   settings" chrome, and inside the drawer its raw ▶ marker + 13px
   sentence-case label in a bordered box read as a debug leftover under the
   11px uppercase role headers. This matches the role sections instead —
   same uppercase label, same mono count — and gets a real chevron.
   The chevron is styled BY CLASS: lucide replaces <i data-lucide> with an
   <svg> that carries the <i>'s classes, so `.drawer-bench-summary svg` would
   be both wrong-by-timing and over-broad (it would catch nested row icons). */
.client-agents-drawer .drawer-bench {
  margin-top: var(--space-5);
  border-top: 1px solid var(--border);
  padding-top: var(--space-3);
}
.client-agents-drawer .drawer-bench-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-2);
  margin: 0 calc(var(--space-2) * -1);
  border-radius: var(--radius-sm);
  cursor: pointer;
  user-select: none;
  list-style: none;  /* kills the default ▶ marker (Firefox) */
  transition: background var(--duration-fast) var(--ease-out);
}
.client-agents-drawer .drawer-bench-summary::-webkit-details-marker {
  display: none;  /* …and in Safari/Chrome */
}
.client-agents-drawer .drawer-bench-summary:hover {
  background: var(--bg-elevated);
}
.client-agents-drawer .drawer-bench-summary:hover .drawer-bench-label,
.client-agents-drawer .drawer-bench[open] .drawer-bench-label {
  color: var(--text-primary);
}
.client-agents-drawer .drawer-bench-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.client-agents-drawer .drawer-bench-chevron {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--text-muted);
  /* Global reduced-motion reset zeroes transition-duration, so open/close
     snaps instead of sweeping — no local override needed. */
  transition: transform var(--duration-fast) var(--ease-out);
}
.client-agents-drawer .drawer-bench[open] .drawer-bench-chevron {
  transform: rotate(90deg);
}
.client-agents-drawer .drawer-bench-label {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  transition: color var(--duration-fast) var(--ease-out);
}
.client-agents-drawer .drawer-bench-body {
  padding-top: var(--space-3);
}
/* The bench's own role sections are nested one level in — the last one
   shouldn't add trailing space above the drawer's bottom padding. */
.client-agents-drawer .drawer-bench-body .drawer-role-section:last-child {
  margin-bottom: 0;
}

/* Folder card: production agent indicator (added 2026-05-11, first rendered
   2026-07-25 — the CSS shipped without a render site). Parent supplies the
   gap; the inner <span> owns the truncation since this is now a flex row. */
.agents-folder-production {
  display: flex;
  align-items: center;
  gap: 4px;
  /* --text-xs is a phantom token (never defined in global.css), so this line
     silently inherited ~13px and out-sized the 11px breakdown/meta lines
     beside it. --text-caption is the real 11px step. */
  font-size: var(--text-caption);
  color: var(--text-secondary);
  min-width: 0;
}
.agents-folder-production span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Color/fill come from the shared production-star rule above (token-based,
   theme-aware); this only carries the folder card's sizing. */
.agents-folder-production-star {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

/* Folder card: was an <a> but is now a <button> for drawer interaction.
   Reset button defaults so visual is identical to the previous anchor.
   `display` MUST stay flex — this element-qualified selector outranks
   .agents-folder-card, so the `display: block` that used to live here killed
   the chip's flex row (icon and name fell onto separate lines the moment the
   card had more than one child). Don't "simplify" it back to block. */
button.agents-folder-card {
  display: flex;
  text-align: left;
  width: 100%;
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  border: 1px solid var(--border);
  cursor: pointer;
}

/* ── Backchanneling panel + modal ────────────────────────────────── */
/* Voice-tab inline section + dedicated clip-management modal. See
   js/backchannel-panel.js. Status pill colors mirror the inactive /
   active / mismatch state machine defined in the dashboard spec. */
.bc-preview-tag {
  font-size: var(--text-overline);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  background: var(--bg-card-subtle);
  padding: 2px 6px;
  border-radius: 4px;
  font-weight: 500;
  vertical-align: middle;
  margin-left: 6px;
}

.bc-status-wrap { display: inline-flex; align-items: center; gap: var(--space-2); }
/* Box + fill come from the canonical .badge / .badge-<variant> (emitted in
   greeting/micro-opener/backchannel panels). Thin modifier: the slightly
   larger 12px read, and italics for the transient loading/error states. */
.bc-status-pill { font-size: 12px; }
.bc-status-pill.bc-status-loading,
.bc-status-pill.bc-status-error {
  font-style: italic;
}

.bc-modal-summary { margin-bottom: var(--space-5); }
.bc-modal-status-row {
  display: flex; flex-direction: column; gap: var(--space-3);
}
.bc-voice-mismatch-banner {
  background: rgba(239, 68, 68, 0.08);
  border: 1px solid rgba(239, 68, 68, 0.25);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-body);
  color: var(--text-primary);
}
/* Same shape as the mismatch banner, informational rather than alarming:
   nothing is wrong here, there is one switch left to flip and it lives on
   another tab. */
.bc-ready-banner {
  background: color-mix(in srgb, var(--info) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--info) 25%, transparent);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-body);
  color: var(--text-primary);
}
/* .bc-empty-state retired — backchannel "no clips" renders through the
   canonical .empty-state (empty-state--compact + empty-state-actions). */
.bc-modal-actions {
  display: flex; justify-content: flex-end;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.bc-type-section {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
}
.bc-type-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.bc-type-header h4 {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}
.bc-type-count {
  font-weight: 400;
  color: var(--text-muted);
  font-size: 12px;
}
.bc-type-hint {
  font-size: 12px;
  color: var(--text-muted);
}

.bc-clip-list { list-style: none; margin: 0 0 var(--space-3); padding: 0; }
.bc-clip-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border-subtle, var(--border));
  gap: var(--space-3);
}
.bc-clip-row:last-child { border-bottom: none; }
.bc-clip-info {
  display: flex; align-items: center; gap: var(--space-3); min-width: 0;
}
.bc-clip-text {
  font-weight: 500;
  font-size: 14px;
  word-break: break-word;
}
.bc-clip-empty {
  font-style: italic;
  color: var(--text-muted);
  font-size: var(--text-body);
  padding: var(--space-2) 0;
}
.bc-clip-status {
  font-size: var(--text-caption);
  font-weight: 500;
  padding: 1px 8px;
  border-radius: 9999px;
}
.bc-status-approved {
  background: rgba(16,185,129,0.12);
  color: rgb(16,185,129);
}
.bc-status-pending {
  background: rgba(245,158,11,0.12);
  color: rgb(245,158,11);
}
.bc-status-rejected {
  background: rgba(107,114,128,0.16);
  color: var(--text-muted);
}
.bc-clip-actions {
  display: flex; gap: var(--space-2); align-items: center; flex-shrink: 0;
}
.bc-group-actions {
  display: flex; gap: var(--space-2); align-items: center; margin-left: auto;
}
.bc-curation-group .bc-type-header { align-items: center; }
.bc-go-form {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-2);
}
.bc-go-form.hidden { display: none; }
.bc-go-form .bc-go-text {
  flex: 1;
  min-width: 0;
}

/* ── Micro-openers panel ─────────────────────────────────────────── */
.mo-phrase-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.mo-phrase-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 4px 4px 10px;
  background: var(--bg-card-subtle);
  border: 1px solid var(--border);
  border-radius: 9999px;
  font-size: var(--text-body);
}
.mo-phrase-row .mo-phrase-text { font-weight: 500; }
.mo-phrase-remove {
  padding: 2px;
  background: transparent;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
}
.mo-phrase-remove:hover { color: var(--danger); }
.mo-add-form {
  display: flex;
  align-items: center;
  gap: 8px;
}

.mo-voice-summary {
  padding: var(--space-3) var(--space-4);
  background: var(--bg-card-subtle);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
  font-size: var(--text-body);
}
.mo-summary-pill { display: flex; align-items: center; margin-bottom: var(--space-3); }
.mo-clip-list { list-style: none; margin: 0; padding: 0; }
.mo-clip-list .bc-clip-row { padding: var(--space-2) 0; border-bottom: 1px solid var(--border-subtle, var(--border)); }
.mo-clip-list .bc-clip-row:last-child { border-bottom: none; }

/* ── Voice fixed-config (Inworld + Cartesia via LiveKit Inference) ── */
.vfx-section { margin-bottom: var(--space-5); }
.vfx-tag {
  font-size: var(--text-caption);
  font-weight: 500;
  color: var(--text-muted);
  background: var(--bg-card-subtle);
  padding: 2px 8px;
  border-radius: 4px;
  vertical-align: middle;
  margin-left: 8px;
}
/* Primary side carries a third slider (agent-wide Volume) alongside
   Speaking rate + Temperature; fallback keeps the default 2-up row. */
.form-row.vfx-slider-row { grid-template-columns: repeat(3, 1fr); }
.form-row.vfx-slider-row-4 { grid-template-columns: repeat(4, 1fr); }
.form-row.vfx-slider-row .form-group,
.form-row.vfx-slider-row-4 .form-group { min-width: 0; }
/* "Loudness calibration +12.0 dB" outruns a quarter-width column, so let the
   value drop to a second line and bottom-align every label in the row — the
   sliders stay on one baseline whether their label wrapped or not. */
.form-row.vfx-slider-row .form-label,
.form-row.vfx-slider-row-4 .form-label {
  flex-wrap: wrap;
  align-items: flex-end;
  min-height: 38px;
}
.vfx-inline-hint { font-weight: 400; color: var(--text-muted); }
.vfx-readonly { background: var(--bg-card-subtle); color: var(--text-muted); cursor: not-allowed; }
.vfx-side-hint { margin-top: var(--space-3); font-size: 12px; }
/* ── .vfx-segmented — the third segmented vocabulary, retired ────────────
   This was a full reimplementation: a drawn 1px border instead of the lit
   hairline, no trough, no backdrop-filter, no inset lighting, `overflow:
   hidden` instead of visible, a solid `background: var(--accent)` selected
   state (precisely the "solid indigo block" the 2026-08-07 glass pass
   retired), a hand-rolled 80ms transition, and no focus ring or press scale
   at all — rendering two paces behind the .segmented tab strip on the same
   page.

   It now uses the shared chassis (.seg-control / .seg-btn) and keeps only
   what genuinely differs: the shared component is driven by an `.active`
   class on a <button>, and this control is a radiogroup. Those three
   `:has()` bridges map radio state onto the shared appearance, so there is
   ONE segmented recipe and the radios keep their semantics — arrow-key
   selection, a native :checked, and the `input[name=...]:checked` read that
   applyToConfig depends on. Nothing here re-declares a colour or a metric;
   it only points radio state at the tokens the chassis already defines. */
.vfx-segmented .seg-btn {
  cursor: pointer;
  /* The chassis positions its own children; the radio is the hit target. */
  padding-inline: var(--space-3);
}
.vfx-segmented .seg-btn input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: inherit;
}
/* Selected — the shared thumb, not a solid fill. */
.vfx-segmented .seg-btn:has(input:checked) {
  background: var(--seg-thumb-bg);
  color: var(--seg-thumb-label, var(--accent));
  font-weight: 600;
  box-shadow: var(--seg-thumb-shadow);
}
/* Disabled — the shared disabled tier, so `is-inert` no longer needs to
   restate what [disabled] already means. */
.vfx-segmented .seg-btn:has(input:disabled) {
  color: var(--fg-disabled);
  background: transparent;
  box-shadow: none;
  cursor: not-allowed;
}
/* Keyboard focus lands on the radio; the ring belongs on the segment. */
.vfx-segmented .seg-btn:has(input:focus-visible) {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent);
}
.vfx-segmented .seg-btn:has(input:not(:disabled)):active { scale: 0.97; }
@media (prefers-reduced-motion: reduce) {
  .vfx-segmented .seg-btn:has(input:not(:disabled)):active { scale: 1; }
}
.vfx-migration-modal { z-index: var(--z-menu); }
.vfx-migration-header h3 { margin: 0; }
.vfx-migration-before {
  background: var(--bg-card-subtle);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  margin: var(--space-3) 0;
  font-size: var(--text-body);
}
.vfx-migration-before code { font-family: var(--font-mono); font-size: 12px; }
.vfx-migration-summary { margin-top: var(--space-4); }
.vfx-migration-summary ol { padding-left: var(--space-5); }
.vfx-migration-summary li { margin-bottom: 4px; font-size: var(--text-body); }
.vfx-migration-warning {
  background: rgba(245, 158, 11, 0.10);
  border: 1px solid rgba(245, 158, 11, 0.25);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  font-size: var(--text-body);
  margin-top: var(--space-4);
}
.bc-legacy-section {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.bc-legacy-section h4 { margin: 0 0 var(--space-2); font-size: 14px; }
.bc-legacy-tag { font-size: var(--text-caption); font-weight: 400; color: var(--text-muted); margin-left: 6px; }
.bc-legacy-list { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.bc-legacy-row { opacity: 0.7; font-style: italic; }
.bc-status-legacy { background: rgba(107,114,128,0.12); color: var(--text-muted); font-style: normal; }

/* Phase 4 (2026-05-17): Score column for admin call list view.
   Compact mono numeral (S11 · 2026-07-04), semantically color-banded by
   health-score tier — replaces the earlier activity-glyph. The big drawer
   numeral (.cd-diag-score) stays; this is the correct list/detail hierarchy.
   Replaces the Starred column for users with role starting 'agency_'. */
.cell-score {
  text-align: center;
  width: 3rem;
  white-space: nowrap;
}
.card-score {
  display: inline-flex;
  align-items: center;
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 1;
}
/* B2 — the headline metric was the SMALLEST type on its row: 11px next to
   13px siblings, the one number an operator scans a 180-row table for,
   rendered at caption tier. 14px is one step above the row, which is what
   a headline number should be.
   B3 — and the tracking went the wrong way. Negative letter-spacing is for
   large type; small type wants zero or a hair positive (Apple §15). The
   -0.02em was tightening the one place that could least afford it. */
.cell-score-num {
  font-family: var(--font-mono);
  font-size: 0.875rem;   /* 14px */
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  line-height: 1;
}
/* AA-safe text tier colors (the --pill-*-fg text variants, not the raw
   fills) so the numeral clears contrast on both themes. */
.cell-score-num--green { color: var(--pill-success-fg); }
.cell-score-num--amber { color: var(--pill-warning-fg); }
.cell-score-num--red   { color: var(--pill-danger-fg); }
/* No opacity. --text-muted at 60% measured 2.7:1 — the same em-dash
   concept rendered at the two opposite extremes in adjacent columns. One
   placeholder convention: the dash, at the muted tier, full strength. */
.cell-score-num--none  { color: var(--text-muted); }

/* ──────────────────────────────────────────────────────────────────────
   Runtime Context tab (Phase 3, 2026-05-20)
   Per-agent declarative selection of variables that get auto-injected
   into the agent's "## Runtime Context" block at session start.
   Modeled on .kb-tab-* — same spacing tokens, same border/surface vars.
   ────────────────────────────────────────────────────────────────────── */

.rc-tab-help {
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}
.rc-tab-help p + p { margin-top: var(--space-2); }
.rc-tab-help code {
  font-family: var(--font-mono);
  font-size: 12px;
  padding: 1px 5px;
  border-radius: var(--radius-sm, 4px);
  background: var(--bg-elevated);
  color: var(--text-primary);
}

.rc-tab-section-help {
  margin-top: calc(var(--space-1) * -1);
  margin-bottom: var(--space-2);
}

/* Standard variable rows — checkbox + body */
.rc-tab-var-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.rc-tab-var-row {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-card);
  cursor: pointer;
  transition: border-color 0.12s ease, opacity 0.12s ease;
}
.rc-tab-var-row:hover { border-color: var(--border-hover); }
.rc-tab-var-row.is-disabled { opacity: 0.55; }
.rc-tab-var-row.is-unavailable {
  opacity: 0.4;
  cursor: not-allowed;
  background: transparent;
}
.rc-tab-var-row.is-unavailable:hover { border-color: var(--border); }
.rc-tab-var-row.is-unavailable .rc-tab-var-checkbox {
  pointer-events: none;
  filter: grayscale(1);
}
.rc-tab-var-checkbox {
  margin-top: 3px;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  cursor: pointer;
}
.rc-tab-var-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1 1 auto;
  min-width: 0;
}
.rc-tab-var-label {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.rc-tab-var-name {
  font-family: var(--font-mono);
  font-size: var(--text-body);
  font-weight: 600;
}
.rc-tab-var-displayname {
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.rc-tab-var-desc {
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.45;
}
.rc-tab-var-preview {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
  background: var(--bg-elevated);
  padding: 6px 10px;
  border-radius: var(--radius-sm, 4px);
  margin-top: 2px;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Custom variables */
.rc-tab-custom-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: var(--space-2);
}
/* .rc-tab-custom-empty is retired — an italic muted line was this tab's own
   answer to "nothing here yet", and the canonical .empty-state--line is the
   app's. One empty state, one weight. */
.rc-tab-custom-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-card);
}
.rc-tab-custom-name {
  font-family: var(--font-mono);
  font-size: var(--text-body);
}
.rc-tab-custom-remove {
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  padding: 4px;
  border-radius: var(--radius-sm, 4px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.rc-tab-custom-remove:hover {
  background: var(--bg-elevated);
  color: var(--danger);
}

/* Add-custom-variable input */
.rc-tab-add-row { margin-top: var(--space-2); }
.rc-tab-add-input-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--bg-card);
}
.rc-tab-add-input {
  font-family: var(--font-mono);
  font-size: var(--text-body);
}
.rc-tab-add-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
}
.rc-tab-add-error {
  font-size: 12px;
  color: var(--pill-danger-fg);
  line-height: 1.4;
}

/* Footer: Reset to defaults link */
.rc-tab-footer {
  display: flex;
  justify-content: flex-end;
}
/* .rc-tab-reset-link retired — the Runtime Context reset now uses the unified
   .btn.btn-ghost.btn-sm "Reset to defaults" affordance. */

/* ──────────────────────────────────────────────────────────────────────
   Micro-openers feature kill-switch (2026-05-21)
   When MICRO_OPENERS_FEATURE_DISABLED = true in js/micro-openers-panel.js,
   the entire section renders muted + non-interactive with an info banner.
   ────────────────────────────────────────────────────────────────────── */
.mo-section-feature-disabled {
  opacity: 0.5;
}
.mo-section-feature-disabled input,
.mo-section-feature-disabled button {
  cursor: not-allowed;
  pointer-events: none;
}
.mo-section-feature-disabled .checkbox-label {
  cursor: not-allowed;
}
.mo-feature-disabled-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin-bottom: var(--space-3);
  margin-top: var(--space-2);
  line-height: 1.45;
}
.mo-feature-disabled-banner i,
.mo-feature-disabled-banner svg {
  flex-shrink: 0;
  margin-top: 1px;
  color: var(--pill-warning-fg);
}

/* Multilingual section (Theme 1, 2026-05-23) — opt-in secondary voice
   for mid-call language switching. Tinted background distinguishes it
   from the primary/fallback voice sections without breaking the
   agent-edit-section visual rhythm. */
.voice-multilingual-section {
  background: rgba(99, 102, 241, 0.04);
  border-color: rgba(99, 102, 241, 0.18);
}
.voice-multilingual-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.voice-multilingual-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.voice-multilingual-body.is-hidden {
  display: none;
}
.voice-multilingual-section .form-hint {
  font-size: 12px;
  color: var(--text-muted, #94a3b8);
  margin-left: var(--space-2);
}

/* ============================================
   Phase 1.9 — cross-page analytics utilities (2026-05-24)

   Four small components extracted ahead of the admin-analytics
   rewrite. Each is reusable across any future page that drops a
   chart legend, a trend column, or a client-named row.

   Scope: NEW classes only — no edits to existing rules. Safe to
   revert in isolation.
   ============================================ */

/* Inline chart legend with colored swatches. Anchored top-right
   of a card-head row next to the title. */
.legend-inline {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  font-size: var(--text-caption);
  color: var(--text-secondary);
}
.legend-inline span[data-client] {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: default;
  transition: opacity var(--duration-fast) ease;
}
.legend-inline .sw {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  vertical-align: -1px;
}

/* 14-bar mini sparkline used in table cells. Per-row normalised —
   the tallest bar in any row sits at ~95% height. Width fixed at
   ~64px (14 × 4px + 13 × 1px gap) so columns align across rows. */
.trend-mini {
  display: inline-flex;
  align-items: flex-end;
  gap: 1px;
  height: 16px;
  width: 64px;
  vertical-align: middle;
}
.trend-mini span {
  width: 4px;
  background: rgba(var(--accent-rgb), 0.55);
  border-radius: 1px;
  min-height: 2px;
  flex: none;
}
.trend-mini.is-up span   { background: var(--success); }
.trend-mini.is-down span { background: var(--danger); }

/* Δ vs prev cell wrapper — colored text only, no fill. Pairs with
   Unicode ▲ ▼ glyphs inside the inner span. */
.trend-cell {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-variant-numeric: tabular-nums;
}
.trend-cell .up   { color: var(--pill-success-fg); font-weight: 500; }
.trend-cell .down { color: var(--pill-danger-fg);  font-weight: 500; }
.trend-cell .flat { color: var(--text-muted); }

/* Inline avatar + client name in table cells. The logo is a 20-22px
   gradient-filled rounded square with white initials inside. Used in
   admin-analytics breakdown table and admin-calls Client column.
   Gradient stops are assigned per-row by inline style (deterministic
   from organizations.slug) so the same client gets the same color
   everywhere it appears. */
.client-cell {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.client-cell__logo {
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 4px;
  display: grid;
  place-items: center;
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: -0.01em;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
[data-theme="light"] .client-cell__logo,
[data-theme="light"] .admin-user-avatar {
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.client-cell__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* As a link (analytics breakdown, O14) the name keeps the row's ink: the
   row is the control that pins, the name is the way through to the client,
   and an accent-coloured cell in every row would read as the table's one
   highlight. Hover underlines; the attribute-selector light-theme link rule
   in global.css outranks a bare class, hence the second selector. */
a.client-cell__name,
[data-theme="light"] a.client-cell__name {
  color: inherit;
}
a.client-cell__name:hover { text-decoration: underline; text-underline-offset: 2px; }
a.client-cell__name:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* ============================================
   admin-analytics page — chart / table / funnel / friction
   (S7 de-island, 2026-07-04). Genuinely page-specific visuals
   lifted from the former embedded <style> in admin-analytics.html.
   KPI tiles now use the canonical .kpi-card family (.kpi-card--sm);
   the div-bar chart / table / funnel visuals move verbatim, tokenized:
   radii, spacing, borders and type snapped to the scale. Per-pixel
   chart geometry (grid offsets, bar gaps, fixed track widths) stays px
   by design — it's layout math, not spacing rhythm.
   ============================================ */

/* Stacked-by-client daily bar chart scaffold. Div-based bars so theme
   tokens carry through on light/dark toggle (a Chart.js canvas would
   render fixed colors that don't track the variables). */
.aa-chart-card { padding: var(--space-5) var(--space-6); }
.aa-chart-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  column-gap: var(--space-3);
  row-gap: var(--space-1);   /* wrapped legend hugs the title instead of a full 12px gap */
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.aa-chart-head h3 { font-size: var(--text-h2); font-weight: 600; letter-spacing: -0.004em; }

.aa-chart-stage {
  position: relative;
  height: 240px;
  margin-top: var(--space-1);   /* was space-3 — title→chart gap was ~28px total, too separated */
}
.aa-grid {
  position: absolute; left: 28px; right: 24px; top: 0; bottom: 22px;
  pointer-events: none;
}
.aa-line {
  position: absolute; left: 0; right: 0;
  border-top: 1px solid var(--border-subtle);
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.aa-line::before {
  content: attr(data-label);
  position: absolute; left: -28px; top: -7px;
  width: 24px; text-align: right;
}
.aa-bars {
  position: absolute; left: 28px; right: 24px; top: 0; bottom: 22px;
  display: flex; justify-content: space-between;
  gap: 4px;
}
/* Explicit height:100% so the inner column-reverse spans' %-heights
   resolve — without it the flex children collapse to 0 (v1 empty-chart bug). */
.aa-stack-bar {
  flex: 1;
  min-width: 0;
  height: 100%;
  display: flex; flex-direction: column-reverse;
  border-radius: 2px 2px 0 0;
  overflow: hidden;
  cursor: default;
  transition: filter var(--duration-fast) ease;
}
.aa-stack-bar:hover { filter: brightness(1.18); }
.aa-stack-bar.is-partial { opacity: 0.55; }
.aa-stack-bar > span {
  display: block;
  width: 100%;
  transition: opacity var(--duration-fast) ease;
}
.aa-x {
  position: absolute; left: 28px; right: 24px; bottom: 0;
  display: flex; justify-content: space-between;
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.aa-x > span {
  flex: 1; min-width: 0;
  text-align: center;
  white-space: nowrap;
  letter-spacing: 0.04em;
}
.aa-x > span:first-child { text-align: left; }
.aa-x > span:last-child  { text-align: right; }

/* Per-day baseline floor — dashboard-analytics trend only (scoped to
   #da-bars). A faint accent tick under every day column so a sparse
   window reads as a populated timeline with a spike, not a broken bar. */
#da-bars .aa-stack-bar {
  background: linear-gradient(to top, color-mix(in srgb, var(--accent) 24%, transparent) 0 3px, transparent 3px);
}

/* Chart tooltip — day total + per-client breakdown above a hovered bar. */
.aa-tooltip {
  position: absolute;
  display: none;
  min-width: 220px;
  max-width: 280px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 32px -8px rgba(0,0,0,0.6), 0 4px 12px -4px rgba(0,0,0,0.4);
  padding: 10px 12px;
  pointer-events: none;
  z-index: 5;
  font-size: var(--text-sm);
}
.aa-tooltip__head {
  font-size: var(--text-caption);
  letter-spacing: 0.08em;
  color: var(--text-muted);
  font-family: var(--font-mono);
  padding-bottom: 6px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--border-subtle);
}
.aa-tooltip__row {
  display: flex; justify-content: space-between; align-items: center;
  gap: 12px;
  padding: 2px 0;
  color: var(--text-secondary);
}
.aa-tooltip__row--total {
  color: var(--text-primary);
  font-weight: 500;
  margin-bottom: 4px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--border-subtle);
}
.aa-tooltip__name { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.aa-tooltip__sw { display: inline-block; width: 8px; height: 8px; border-radius: 2px; flex: none; }
.aa-tooltip__val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* Per-client breakdown table card. */
.aa-table-head {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: var(--space-3); column-gap: var(--space-3); row-gap: var(--space-1); flex-wrap: wrap;
}
.aa-table-head h2 { font-size: var(--text-h2); font-weight: 600; letter-spacing: -0.004em; }
.aa-table-subtitle {
  color: var(--text-muted);
  font-size: var(--text-caption);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 500;
  font-family: var(--font-mono);
}
.aa-table-card {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-card);
  overflow: hidden;
}
.aa-table-card .data-table th { background: var(--bg-card); }
.aa-table-card .data-table tbody tr.aa-row { cursor: pointer; }
.aa-table-card .data-table tbody td { padding: var(--space-3) var(--space-4); }
.aa-table-card .data-table tbody td .trend-mini { vertical-align: middle; }
/* Pinned row — persists after click. Stronger than the hover highlight
   so the pin reads as deliberate. */
.aa-table-card .data-table tbody tr.aa-row.is-pinned {
  background: rgba(var(--accent-rgb), 0.10) !important;
  box-shadow: inset 4px 0 0 var(--accent) !important;
}

/* Every chart segment renders as two stacked sub-bars (unbooked bottom,
   booked top) in the same client hue. On highlight the booked share gets
   diagonal hatching + a divider line at the seam (opacity overlays are
   per-theme, not tokens — deliberate viz values). */
.aa-stack-bar > .aa-seg {
  display: flex;
  flex-direction: column-reverse;
  width: 100%;
  transition: opacity var(--duration-fast) ease;
}
.aa-seg-booked,
.aa-seg-unbooked {
  width: 100%;
  transition: box-shadow var(--duration-fast) ease;
}
.aa-stack-bar > .aa-seg.is-highlighted.has-split .aa-seg-booked {
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0 2px,
    rgba(255, 255, 255, 0.38) 2px 4px
  );
}
.aa-stack-bar > .aa-seg.is-highlighted.has-split .aa-seg-unbooked {
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.78);
}
[data-theme="light"] .aa-stack-bar > .aa-seg.is-highlighted.has-split .aa-seg-booked {
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0 2px,
    rgba(0, 0, 0, 0.28) 2px 4px
  );
}
[data-theme="light"] .aa-stack-bar > .aa-seg.is-highlighted.has-split .aa-seg-unbooked {
  box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.52);
}

/* The .aa-table-card-scoped sort-arrow fork that used to sit here is GONE.
   It was one of three copies of the same header affordance (this one, an
   analytics-deck.css copy that overrode it, and the canonical .th-sort block
   further down); all three collapsed into the `.sort-icon` rules beside
   .th-sort once the analytics head adopted the Lucide glyph (U7). `.data-table th.sortable { cursor: pointer; user-select: none }`
   was already canonical near the top of this file, so the scoped copy of
   that was doing nothing at all. */

/* Responsive: narrower chart padding + horizontally-scrollable table. */
@media (max-width: 900px) {
  .aa-table-card { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; }
  .aa-table-card .data-table { min-width: 800px; }
  .aa-chart-card { padding: var(--space-4) var(--space-3); }
}

/* ── Call-quality trend (quality-trend.js) ───────────────────────────
   Reuses the aa-* grid/bars/x/tooltip scaffold. Adds: per-day score
   columns (single accent hue, 0–100 scale), a dashed version-boundary
   marker, and a thin call-count band for sample-size context. */
.qt-filters { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.qt-col {
  flex: 1; min-width: 0; height: 100%;
  position: relative;
  display: flex; flex-direction: column; justify-content: flex-end;
}
/* Subtle dashed marker at a score-version boundary (scores only compare
   within a version). Spans the plot height at the column's leading edge. */
.qt-col[data-vstart]::before {
  content: '';
  position: absolute; left: -2px; top: 0; bottom: 0;
  border-left: 1px dashed var(--text-muted);
  opacity: 0.5;
  pointer-events: none;
}
.qt-bar {
  display: block; width: 100%;
  background: var(--accent);
  border-radius: 2px 2px 0 0;
  min-height: 2px;
  transition: opacity var(--duration-fast) ease, filter var(--duration-fast) ease;
}
/* Provisional-only day — lighter/hollow fill (tier withheld, softer evidence). */
.qt-bar--provisional {
  background: rgba(var(--accent-rgb), 0.18);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.qt-col:hover .qt-bar { filter: brightness(1.18); }
.qt-counts-wrap { margin-top: 8px; }
.qt-counts-cap {
  display: block;
  margin: 0 0 3px 28px;
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-family: var(--font-mono);
  letter-spacing: 0.06em;
  line-height: 1;
}
.qt-counts {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 4px;
  height: 22px;
  margin: 0 24px 0 28px;
}
.qt-counts > .qt-cbar {
  flex: 1; min-width: 0;
  background: rgba(var(--accent-rgb), 0.30);
  border-radius: 1px 1px 0 0;
}

/* ── Dashboard Analytics (client product-usage view) ─────────────── */
.aa-header-controls { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.da-toolbar { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-5); }
.da-scope-wrap { display: flex; align-items: center; gap: var(--space-2); }
.da-scope-label { font-size: var(--text-sm); color: var(--text-muted); }
/* Promoted out of the page-scoped stopgap in analytics-deck.css, which
   suppressed the native chrome and hung a separate .da-select__chevron span
   beside it. The app already has one answer for this: .form-select's baked
   lucide chevron. Same glyph, same 36px height, same hover and focus as
   every other select — and no companion element to keep positioned.
   analytics-deck.css has since dropped its .da-select fork, so this is the
   whole treatment. */
.da-scope {
  appearance: none;
  -webkit-appearance: none;
  background-color: var(--bg-card);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238A8A98' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 15px 15px;
  color: var(--text-primary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 7px var(--space-8) 7px 10px;
  font: inherit;
  font-size: var(--text-sm);
  min-height: 36px;
  min-width: 170px;
  cursor: pointer;
  transition: border-color var(--motion-hover) var(--ease-out);
}
[data-theme="light"] .da-scope {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B6B78' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
.da-scope:hover { border-color: var(--border-hover); }
.da-scope:focus-visible {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent);
}
.da-download { margin-left: auto; }
.da-funnel { display: flex; flex-direction: column; gap: 10px; }
.da-funnel-row { display: grid; grid-template-columns: 140px 1fr 64px; align-items: center; gap: var(--space-3); }
.da-funnel-label { font-size: var(--text-sm); color: var(--text-secondary); }
.da-funnel-track { height: 22px; background: var(--bg-elevated); border-radius: var(--radius-sm); overflow: hidden; box-shadow: inset 0 0 0 1px var(--border-subtle); }
.da-funnel-bar { display: block; height: 100%; background: color-mix(in srgb, var(--accent) 55%, var(--bg-card)); border-radius: var(--radius-sm); transition: width .3s ease; }
.da-funnel-val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--text-sm); color: var(--text-primary); text-align: right; white-space: nowrap; }
/* Status pill — semantic muted-fill + saturated (AA-safe) text, matching
   the canonical badge silhouette. */
.da-status { display: inline-block; padding: 2px 9px; border-radius: var(--radius-full); font-size: var(--text-caption); font-weight: 600; }
.da-status--success { color: var(--pill-success-fg); background: color-mix(in srgb, var(--success) 10%, var(--bg-card)); }
.da-status--warning { color: var(--pill-warning-fg); background: color-mix(in srgb, var(--warning) 12%, var(--bg-card)); }
.da-status--danger { color: var(--pill-danger-fg); background: color-mix(in srgb, var(--danger) 13%, var(--bg-card)); }
.da-friction { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.da-friction-chip { display: inline-flex; align-items: baseline; gap: 6px; padding: 6px 12px; border-radius: var(--radius-md); background: var(--bg-elevated); color: var(--text-secondary); font-size: var(--text-sm); }
.da-friction-chip .da-friction-n { font-family: var(--font-mono); font-weight: 600; color: var(--text-primary); }
.da-friction-chip.is-bad { background: color-mix(in srgb, var(--danger) 12%, var(--bg-card)); color: var(--pill-danger-fg); }
.da-friction-chip.is-bad .da-friction-n { color: var(--pill-danger-fg); }
#view-dashboard.is-busy { opacity: .6; pointer-events: none; }

/* ── Semantic badge variants (Phase 8) ─────────────────────────────
   Single-dash .badge-<state> is the canonical status vocabulary:
   muted tint fill + AA text tier (--pill-*-fg) + hairline tint border,
   per taste ruling 4.3. Declared last so they beat the .badge base
   defaults regardless of which base block a page's cascade lands on. */
.badge-success { --chip-hue: var(--success); color: var(--pill-success-fg); }
.badge-warning { --chip-hue: var(--warning); color: var(--pill-warning-fg); }
.badge-danger  { --chip-hue: var(--danger);  color: var(--pill-danger-fg); }
.badge-info    { --chip-hue: var(--info);    color: var(--pill-info-fg); }
.badge-neutral { color: var(--text-secondary); }
.badge-accent  { --chip-hue: var(--accent);  color: var(--accent); }

/* Role-badge colours re-declared after the .badge base for the same
   cascade reason as the semantic variants above (Phase 8 fix). These
   also serve as colour tokens on non-badge icon tiles. */
.role-badge--inbound {
  color: rgb(165, 180, 252);
  background: rgba(99, 102, 241, 0.14);
  border-color: rgba(99, 102, 241, 0.30);
}
.role-badge--outbound {
  color: rgb(252, 211, 77);
  background: rgba(217, 119, 6, 0.16);
  border-color: rgba(217, 119, 6, 0.32);
}
.role-badge--confirmation {
  color: rgb(110, 231, 183);
  background: rgba(16, 185, 129, 0.14);
  border-color: rgba(16, 185, 129, 0.30);
}

/* ============================================================================
   Onboarding answer cards (js/answers-view.js) — shared by the client
   questionnaire review section and the admin per-client answers tab. Read-only
   summary of submitted answers; label/value rows grouped by section.
   ============================================================================ */
.answers-cards { display: flex; flex-direction: column; gap: var(--space-3); }
.answers-card {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--bg-card-subtle);
  padding: var(--space-4);
}
.answers-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.answers-card-title {
  margin: 0;
  font-size: var(--text-h2);
  font-weight: 600;
  color: var(--text-primary);
}
.answers-card-edit {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: var(--text-caption);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-in-out), color var(--duration-fast) var(--ease-in-out);
}
.answers-card-edit:hover { background: var(--accent-muted); color: var(--accent); }
.answers-card-edit svg { width: 13px; height: 13px; }

.answers-card-list { margin: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.answers-row {
  display: grid;
  grid-template-columns: minmax(140px, 34%) 1fr;
  gap: var(--space-3);
  align-items: start;
}
.answers-row dt {
  margin: 0;
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.5;
}
.answers-row dd {
  margin: 0;
  font-size: var(--text-body);
  color: var(--text-primary);
  line-height: 1.5;
  word-break: break-word;
}
.answers-row.is-missing dd { color: var(--warning); }

/* Value sub-lists */
.answers-bullets,
.answers-qa,
.answers-hours,
.answers-contacts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.answers-bullets li { position: relative; padding-left: var(--space-4); }
.answers-bullets li::before {
  content: '';
  position: absolute;
  left: var(--space-1);
  top: 0.62em;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--text-muted);
}
.answers-qa li,
.answers-contacts li { display: flex; flex-direction: column; gap: 1px; }
.answers-qa-q { color: var(--text-primary); font-weight: 500; }
.answers-qa-a { color: var(--text-secondary); }
.answers-hours li {
  display: grid;
  grid-template-columns: 48px 1fr;
  gap: var(--space-2);
}
.answers-hours-day { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.answers-sub { color: var(--text-muted); font-size: var(--text-caption); }

/* Soft truncation ("Show more") */
.answers-truncated .answers-full { display: none; }
.answers-truncated.is-expanded .answers-short { display: none; }
.answers-truncated.is-expanded .answers-full { display: inline; }
.answers-toggle {
  display: inline;
  margin-left: var(--space-1);
  padding: 0;
  border: none;
  background: none;
  color: var(--accent-strong);
  font-family: var(--font-body);
  font-size: var(--text-caption);
  cursor: pointer;
}
.answers-toggle:hover { text-decoration: underline; }

@media (max-width: 767px) {
  .answers-row { grid-template-columns: 1fr; gap: 2px; }
}

/* ============================================================================
   Admin client-detail → Onboarding questionnaire progress
   (js/admin-onboarding-answers.js)

   One card answering "where is this client with their questionnaire": a status
   pill, an answered/answerable meter, the timestamps the schema can actually
   supply, and a per-section breakdown. The section had no rules of its own
   before this block — it borrowed .admin-detail-section*, which it still does
   for its frame. All colours come from existing global tokens, so both themes
   ship with them.
   ============================================================================ */
.admin-onboarding-answers .admin-detail-section-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.oa-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.oa-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}

/* Mono on the numeral only; the words around it are prose. Matches the
   .count-n treatment used on the calls header. */
.oa-count {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
}
.oa-count-total {
  font-family: var(--font-body);
  font-weight: 400;
  color: var(--text-muted);
}

/* Shared by the overall meter and the per-section ones. */
.oa-meter {
  height: 4px;
  border-radius: 999px;
  background: var(--border);
  overflow: hidden;
}
.oa-meter > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
}

.oa-facts {
  margin: 0;
  display: grid;
  /* min() so the track can drop below its ideal width instead of forcing a
     horizontal scroll on a narrow card — auto-fit will not shrink past a bare
     minmax() minimum. */
  grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr));
  gap: var(--space-2) var(--space-5);
}
.oa-fact {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  min-width: 0;
}
.oa-fact dt {
  flex: 0 0 auto;
  width: 88px;
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.oa-fact dd {
  margin: 0;
  min-width: 0;
  font-size: var(--text-sm);
  color: var(--text-primary);
}
/* The exact stamp sits beside the relative one rather than in a title
   attribute — an operator comparing two clients shouldn't have to hover. */
.oa-fact-exact {
  margin-left: var(--space-2);
  font-size: var(--text-caption);
  color: var(--text-muted);
  white-space: nowrap;
}

/* Columns rather than one long list: eight sections stacked single-file made
   the card twice as tall as the rest of the detail page for no extra
   information. */
.oa-sections {
  list-style: none;
  margin: 0;
  padding: var(--space-4) 0 0;
  border-top: 1px solid var(--border-subtle);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
  gap: var(--space-2) var(--space-6);
}
.oa-section {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 120px 46px;
  align-items: center;
  gap: var(--space-3);
}
.oa-section-name {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.oa-section.is-empty .oa-section-name { color: var(--text-muted); }
.oa-section-count {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-align: right;
}

/* Zero-data state — the one nearly every client is in today. */
.oa-note {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.oa-note-action { align-self: flex-start; }

.oa-cta { display: flex; }
.oa-cta-count {
  color: var(--text-muted);
  font-weight: 400;
}

/* ── The two fixed rows: which form, what link ─────────────────────────────
   A label column, one value that may be long, and the actions. Both rows share
   the grid so "Form" and "Link" sit on one optical column and the eye drops
   straight down the card. The label column matches .oa-fact dt's rhythm
   (narrower, because these two words are short). */
.oa-row {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
}
.oa-row-label {
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.oa-row-value {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.oa-row-value.is-muted {
  font-weight: 400;
  color: var(--text-muted);
}
.oa-row-actions {
  display: flex;
  gap: var(--space-2);
}
/* A URL is a value, not prose — mono, and small enough that a 90-character
   token fits without the field scrolling on a desktop card. */
.oa-link-input {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  min-width: 0;
}

/* The 034 trap: a link pinned to a form with nothing published silently
   resolves to something else. Stated in warning tone because the operator's
   next action is to publish that form, not to shrug. */
.oa-mismatch {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--pill-warning-border);
  background: var(--pill-warning-bg);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-secondary);
}
.oa-mismatch i,
.oa-mismatch svg {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  color: var(--pill-warning-fg);
}

@media (max-width: 640px) {
  /* The label stops being a column and becomes a caption above its value;
     the actions keep their own line so two buttons never squeeze a URL. */
  .oa-row {
    grid-template-columns: 1fr;
    gap: var(--space-2);
  }
  /* A grid item stretches by default and .btn centres its own contents, so
     without this the action sits mid-row with its label floating. */
  .oa-row > .btn,
  .oa-row-actions { justify-self: start; }
  .oa-row-actions { justify-content: flex-start; }
}

/* ── Form picker ───────────────────────────────────────────────────────────
   Two beats in one modal: choose a form, then the body becomes the link. Only
   published forms are listed, so there is nothing here to disable or explain.
   ────────────────────────────────────────────────────────────────────────── */
.oa-pick-search {
  position: relative;
  margin-bottom: var(--space-3);
}
.oa-pick-search i,
.oa-pick-search svg {
  position: absolute;
  left: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  color: var(--text-muted);
  pointer-events: none;
}
.oa-pick-search .form-input { padding-left: calc(var(--space-3) * 2 + 15px); }

.oa-pick-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-height: min(46vh, 340px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.oa-pick-item {
  width: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 18px;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  /* Not transparent at rest: a bare name/key pair on a flat modal body reads
     as a caption, not as the thing you are meant to click. */
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: transparent;
  cursor: pointer;
  text-align: left;
  transition: background var(--motion-hover) var(--ease-out),
              border-color var(--motion-hover) var(--ease-out);
}
.oa-pick-item:hover { background: var(--bg-card-hover); }
.oa-pick-item.is-on {
  background: var(--accent-muted);
  border-color: var(--accent);
}
.oa-pick-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.oa-pick-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.oa-pick-key {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
}
/* The tick is the whole selection signal, so it earns a real entrance:
   it grows from nothing rather than blinking on. */
.oa-pick-tick {
  width: 18px;
  height: 18px;
  color: var(--accent);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 140ms var(--ease-out), transform 220ms var(--ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1));
}
.oa-pick-item.is-on .oa-pick-tick {
  opacity: 1;
  transform: scale(1);
}
.oa-pick-none {
  margin: var(--space-4) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.oa-generated {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.oa-generated-icon {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--text-inverse);
  animation: oa-pop 320ms var(--ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)) both;
}
.oa-generated-icon i,
.oa-generated-icon svg { width: 17px; height: 17px; }
@keyframes oa-pop {
  from { transform: scale(0.4); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}
.oa-generated-title {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}
.oa-generated-sub {
  font-size: var(--text-caption);
  color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
  /* The app-wide rule further up this file clamps every duration to 1ms,
     which makes motion instant but leaves the transforms and the keyframe's
     `both` fill in play — so a tick would still land pre-scaled for a frame
     and the check badge would still pop. These remove them outright. */
  .oa-pick-item,
  .oa-pick-tick { transition: none; }
  .oa-pick-tick { transform: none; }
  .oa-generated-icon { animation: none; }
}

/* ============================================================================
   Admin client-detail → Service areas & booking
   (js/client-service-setup.js)

   The home for what the setup wizard's Config and Mode steps used to force.
   Label-left / control-right rows so four settings read as one short list
   rather than four form groups — this is an operator page and the settings
   are already named by their labels.
   ============================================================================ */
.css-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.css-row {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  align-items: center;
  gap: var(--space-4);
}
.css-row--switch { grid-template-columns: 148px auto; }
.css-row-label {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.css-row-fields {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.css-input-sm { width: auto; min-width: 108px; }
.css-input-md { width: auto; min-width: 148px; }
.css-dash { color: var(--text-muted); }

/* Segmented control — three mutually exclusive answers on one line, which is
   what the old Step 4 spent two full-height cards saying. */
.css-seg {
  display: inline-flex;
  /* A grid item stretches to its track by default, which turned three short
     options into a full-width bar with the choices huddled at one end. */
  justify-self: start;
  padding: 3px;
  gap: 2px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-elevated);
}
.css-seg-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px var(--space-3);
  border: 0;
  border-radius: calc(var(--radius-md) - 3px);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-caption);
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--motion-hover) var(--ease-out),
              color var(--motion-hover) var(--ease-out);
}
.css-seg-btn i,
.css-seg-btn svg { width: 14px; height: 14px; }
.css-seg-btn:hover:not(.is-on) { color: var(--text-primary); }
.css-seg-btn.is-on {
  background: var(--bg-card-solid);
  color: var(--accent);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.css-seg-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Day pills. Seven of them, so they are letters in a row rather than a
   checkbox list — the shape of a week is recognisable at a glance. */
.css-days {
  display: inline-flex;
  gap: 4px;
  margin-left: var(--space-2);
}
.css-day {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--motion-hover) var(--ease-out),
              color var(--motion-hover) var(--ease-out),
              border-color var(--motion-hover) var(--ease-out),
              transform 90ms var(--ease-out);
}
.css-day:hover { color: var(--text-primary); }
.css-day:active { transform: scale(0.9); }
.css-day.is-on {
  background: var(--accent-muted);
  border-color: var(--accent);
  color: var(--accent);
}
.css-day:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.css-warn {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--pill-warning-border);
  background: var(--pill-warning-bg);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-secondary);
}
.css-warn i,
.css-warn svg {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  color: var(--pill-warning-fg);
}

/* Autosave feedback, borrowed in spirit from the wizard's chip: it appears
   where the operator already is, says one word, and leaves. */
.css-chip {
  font-size: var(--text-caption);
  color: var(--text-muted);
  opacity: 0;
  transition: opacity 180ms var(--ease-out);
}
.css-chip[data-state="saving"],
.css-chip[data-state="saved"],
.css-chip[data-state="error"] { opacity: 1; }
.css-chip[data-state="error"] { color: var(--danger); }

@media (max-width: 640px) {
  .css-row,
  .css-row--switch {
    grid-template-columns: 1fr;
    gap: var(--space-2);
  }
  .css-row--switch {
    grid-template-columns: 1fr auto;
    align-items: center;
  }
  .css-days { margin-left: 0; }
  .css-seg { width: 100%; }
  .css-seg-btn { flex: 1; justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  .css-seg-btn,
  .css-day,
  .css-chip { transition: none; }
  .css-day:active { transform: none; }
}

/* The "View answers" overlay shipped with markup but no container rules: the
   panel was transparent and unbounded, so long answer sets ran off both ends
   of the viewport with the page scrolling behind them. */
body.admin-oa-modal-open { overflow: hidden; }
.admin-oa-modal {
  display: flex;
  flex-direction: column;
  width: min(880px, 100%);
  max-height: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-sheet);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.admin-oa-modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
}
.admin-oa-modal-title {
  margin: var(--space-1) 0 0;
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}
.admin-oa-modal-body {
  padding: var(--space-5);
  overflow-y: auto;
  overscroll-behavior: contain;
}

@media (max-width: 767px) {
  .oa-section { grid-template-columns: minmax(0, 1fr) 64px 42px; }
  .oa-fact dt { width: 80px; }
  .admin-oa-modal-head,
  .admin-oa-modal-body { padding: var(--space-4); }
}

/* ═══════════════════════════════════════════════════════════════════════
   W5 — ALL CALLS (/admin-calls)
   Audit: scratchpad/audit-findings/admin-calls.md. Ids are that document's.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── A4/A6 · The header answers "how much am I looking at" ──────────────
   The count lived in the pagination footer, ten rows below the fold, and
   only appeared when there was more than one page. It is a property of
   the view, so it sits with the view's title.
   A4 is solved by structure rather than alignment: the title and its
   count are one block now, so there is no second column to mis-centre
   against — the Export control moved down onto the toolbar (A5). */
.page-heading {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
/* NOT flex: the line is a sentence with inline spans in it, and flex makes
   every text node its own item — which eats the space between the numeral
   and the word and prints "180calls". Block, with a reserved height so the
   title does not jump when the count lands. */
#calls-count-line {
  display: block;
  min-height: 18px;
}
/* Mono + tabular only on the NUMBER. The words around it are prose and
   belong in the body face — the whole line in mono was the mistake the
   clients page is fixing in the same wave. */
.count-n {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--text-secondary);
}
.count-sep { margin: 0 6px; opacity: 0.55; }
.skeleton-inline {
  display: inline-block;
  height: 11px;
  border-radius: var(--radius-xs);
  vertical-align: baseline;
}

/* ── A5/D7 · One toolbar instead of three chrome bands ──────────────────
   The page opened with a title row, then a 1280px-wide search field alone
   on its own row, then the filter row: three bands and ~78px of chrome
   before the first call. A search field that wide is not a bigger target,
   it is a longer line to scan. 360px is a real search field; everything
   else joins it on one row, grouped [search][period] ··· [filters][Export]
   so the row reads as two clusters rather than one mash. */
@media (min-width: 768px) {
  #admin-filter-bar .search-wrapper {
    flex: 0 1 360px;
    max-width: 360px;
    min-width: 220px;
  }
  /* ONE height across the row. The search field was 40px and the segmented
     control 36px, so the row had two baselines and the shorter control
     floated 2px off centre against the taller ones — the "ragged" reading.
     36px is the app's md control height (S2), so the field comes down to
     the segmented rather than the other way round. */
  #admin-filter-bar .search-wrapper .search-input,
  #admin-filter-bar .filter-trigger {
    height: 36px;
    min-height: 36px;
  }
  /* The cluster used to be laid out here as a right-aligned second row.
     It has no layout of its own on this page any more: it lives inside the
     Filters panel at every width, and the panel styles it (see "Filters:
     one control, two surfaces" at the end of this file). Right-aligning it
     was what produced the ragged second row — a group of four selects
     pinned to the right edge with ~600px of nothing between them and the
     search they belonged to. */
  #admin-filter-bar .filter-group--action .download-menu-wrap { display: flex; }
}
@media (min-width: 1440px) {
  #admin-filter-bar .search-wrapper { flex: 0 1 360px; max-width: 360px; }
}

/* The period toggle keeps its short labels until there is genuinely room
   for the long ones. With a 260px rail the content column is ~1100px at a
   1440 viewport, and "Today / 7 days / 30 days / 90 days / All time" costs
   ~90px more than "Today / 7d / 30d / 90d / All" — the difference between
   the toolbar being one row and two. The existing 768-1099 rule already
   made this trade; it just stopped one breakpoint too early. */
@media (min-width: 1100px) and (max-width: 1599px) {
  #admin-filter-bar .seg-label-full { display: none; }
  #admin-filter-bar .seg-label-short { display: inline; }
}

/* A8 (cont.) — the Score header needs room for its own affordance. The
   column was pinned to 3rem for a two-digit numeral, so a sortable header
   pushed its arrow past the card edge and it vanished. The column sizes to
   its header instead, and the numerals right-align under it — tabular
   figures belong on a right edge, and it puts them beside the hover
   chevron rather than under it. */
.data-table--admin .cell-score {
  width: auto;
  min-width: 6.5rem;
  text-align: right;
}
.data-table--admin th.cell-score .th-sort {
  justify-content: flex-end;
  padding-right: 30px;   /* matches the row's reserved chevron gutter (E5) */
}

/* ── D4 · A way out of the search field ─────────────────────────────────
   32px visible, 40px hit (the app's floor), and it only exists when there
   is something to clear — a permanent × on an empty field is noise. */
.search-clear {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
  transition: color var(--motion-hover) var(--ease-out),
              background-color var(--motion-hover) var(--ease-out),
              scale var(--motion-press) var(--ease-out);
}
.search-clear::before {
  content: '';
  position: absolute;
  inset: -6px;   /* 28 + 12 = 40px hit box */
}
.search-clear i,
.search-clear svg { width: 14px; height: 14px; }
.search-clear:hover { color: var(--text-primary); background: var(--bg-control-hover); }
.search-clear:active { scale: 0.96; }
.search-clear[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .search-clear:active { scale: 1; }
}

/* ── A7 · The pagination belongs to the table ───────────────────────────
   It floated 17px below the card it described, reading as page furniture
   rather than as that table's footer. One card owns the rows AND the
   controls that move through them; the table drops its own chrome and
   inherits the card's. */
.table-card {
  border-radius: var(--radius-xl);
  border: 1px solid var(--border-subtle);
  box-shadow: var(--shadow-md);
  background: var(--bg-card);
  /* clip, not hidden: both clip the table's square corners to the card's,
     but hidden makes the card a scrollport and the sticky column header
     would stick to it rather than to the viewport (see STICKY HEADERS). */
  overflow: clip;
}
.table-card .table-container {
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.table-card .pagination {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--separator);
}
.table-card .empty-state { border: 0; }
/* Mobile renders CARDS, and a card list inside a card is a nested card.
   The wrapper stands down where it would nest. */
@media (max-width: 767px) {
  .table-card {
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
    overflow: visible;
  }
  .table-card .pagination { padding: var(--space-3) 0; border-top: 0; }
}

.pagination-lead {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-width: 0;
}
.pagination-info {
  color: var(--text-muted);
  white-space: nowrap;
}
/* Rows-per-page. The label is the same small-label recipe as the filter
   row (B4) so the two say "label" the same way. */
.pagination-size {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.pagination-size-label {
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.form-select-sm {
  width: auto;
  min-width: 0;
  min-height: 30px;
  height: 30px;
  padding: 0 26px 0 var(--space-2);
  font-size: var(--text-sm);
  background-size: 13px 13px;
  background-position: right var(--space-2) center;
}
/* F10 — Previous and Next were visual twins told apart only by which one
   was dimmed. The chevrons sit on the OUTSIDE of each label, so the pair
   points away from itself and direction is readable before you read the
   words. */
.pagination-prev i,
.pagination-prev svg { margin-right: -2px; }
.pagination-next i,
.pagination-next svg { margin-left: -2px; }

/* ── E4 · A refetch is not an empty state ───────────────────────────────
   Changing a filter used to replace the table with blank skeleton bands:
   the card collapsed ~1670px → ~330px and rebounded, and "loading" looked
   exactly like "nothing found". The rows that are already true stay on
   screen, dimmed and inert, at a pinned height. */
#calls-container.is-refetching {
  opacity: 0.5;
  pointer-events: none;
  transition: opacity var(--duration-normal) var(--ease-out);
}
#calls-container.is-refetching .data-table { cursor: progress; }
@media (prefers-reduced-motion: reduce) {
  #calls-container.is-refetching { transition: none; }
}

/* ── A8 · Sortable column headers ───────────────────────────────────────
   The <th> keeps the header's type; the button inside carries the hit
   area and the keyboard reach. `font: inherit` so the control is
   invisible as a control and reads purely as the column label. */
.data-table th.sortable { padding: 0; }
.th-sort {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  min-height: 40px;   /* the app's hit floor, inside the header's own box */
  padding: var(--space-3) var(--space-4);
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--motion-hover) var(--ease-out);
}
/* --text-primary, not --text-secondary: a header hover is the affordance for
   a control the reader cannot otherwise see, so it lifts all the way. Taken
   from the analytics deck's copy of this block when the two were merged. */
.th-sort:hover { color: var(--text-primary); }
.th-sort:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius-xs);
}
/* A numeric column's label sits over right-aligned figures, so the button —
   which is the full width of the cell — has to align to the same edge. */
.data-table th.num .th-sort { justify-content: flex-end; }

/* The ↕ / ▲ / ▼ text-glyph fork that sat here is gone (U7): the analytics
   table now draws the same Lucide pair as .sort-icon above. One glyph, one
   ladder. */
/* The button owns the cell, so the cell stops padding it — otherwise the
   header text sits at double indent and the 40px hit floor is measured
   inside a box that already had its own inset. Promoted out of
   analytics-deck.css, where it was scoped to .aa-table-card; every sortable
   table in the app needs it. */
.data-table th.sortable { padding: 0; }
.data-table th.sortable .sort-icon { margin-left: 0; }
.data-table th.sorted .th-sort { color: var(--text-primary); }
/* The neutral chevron pair is an affordance, so it whispers; the
   directional arrow is state, so it speaks. */
.data-table th.sorted .sort-icon { opacity: 1; color: var(--accent); }

/* ── E5 · The row says it opens ─────────────────────────────────────────
   A 5% hover tint was the only affordance for a drawer that is the whole
   point of the table. A chevron slides in at the right edge on hover —
   the macOS disclosure convention, and it costs no layout because the
   room is reserved permanently.
   Admin only: the client table's last cell is the handled ring, an actual
   control, and a decorative glyph on top of it would be a lie. */
.data-table--admin tbody tr.clickable td:last-child {
  position: relative;
  padding-right: 30px;
}
.data-table--admin tbody tr.clickable td:last-child::after {
  content: '';
  position: absolute;
  right: 8px;
  top: 50%;
  width: 16px;
  height: 16px;
  margin-top: -8px;
  background-color: var(--text-muted);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E") center / 16px 16px no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E") center / 16px 16px no-repeat;
  opacity: 0;
  transform: translateX(-3px);
  transition: opacity var(--motion-hover) var(--ease-out),
              transform var(--motion-hover) var(--ease-out);
  pointer-events: none;
}
.data-table--admin tbody tr.clickable:hover td:last-child::after,
.data-table--admin tbody tr.clickable:focus-visible td:last-child::after {
  opacity: 0.75;
  transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
  .data-table--admin tbody tr.clickable td:last-child::after {
    transform: none;
    transition: opacity var(--motion-hover) var(--ease-out);
  }
}

/* E5 (second half) — hover was a 5% whisper on the one gesture that
   matters here. 7% is felt without becoming a selection. */
.data-table--admin tbody tr:hover {
  background: rgba(var(--accent-rgb), 0.07);
}
[data-theme="light"] .data-table--admin tbody tr:hover {
  background: rgba(var(--accent-rgb), 0.06);
}

/* ── C4 · Three encodings for three adjacent columns ────────────────────
   Status was a tinted pill, Category was a pill, and Sentiment was bare
   coloured text with an icon — three grammars in three neighbouring
   columns, with Sentiment the only one that was not a chip. It becomes an
   icon with a tooltip: it is the softest of the three signals and it was
   taking the most horizontal room to say so. The client table already
   made this move; this brings admin onto it. */
.data-table--admin .td-sentiment .cell-sentiment { justify-content: flex-start; }

/* ── D11 · The category family gets its glyphs ──────────────────────────
   D11 originally read "every category chip carries a glyph; Returning
   shipped without one" and added the one. That premise was backwards:
   Returning was the ONLY member with a glyph, so the column read as one
   icon among wordmarks. The family closes here.

   One recipe, six masks. A CSS mask rather than a lucide <i>/<svg> in the
   markup, because that is what the Returning glyph already shipped as and
   because a mask needs no hydration pass, cannot mis-size, and takes its
   colour from the chip's own ink for free — `background-color: currentColor`
   is the whole "icon colour follows each chip's ink" requirement. The chips
   are emitted by four different renderers (admin table, client table, mobile
   cards, detail drawer); a mask means none of them has to change except to
   name the category.

   12px in a 24px chip whose face is 11px/600. One notch above the cap height
   is the standard optical relationship — an icon matched to cap height reads
   undersized next to the letterforms it sits beside. The chip's own height,
   radius and padding are untouched, so the 24px system holds; the only
   dimension that moves is width, by 17px (12 glyph + 5 gap).

   Glyph choices, where they are not self-evident:
     · Booking is calendar-PLUS, not a check — the Booked STATUS pill is
       already a check, and the two must not collide in one row.
     · Spam is `ban`, not `shield-alert`. The spam chip is deliberately
       receding (opacity 0.55/0.65 below); an alarm triangle inside a chip
       that is trying to get out of the way fights itself. A circle-slash
       reads as "discard" and stays quiet.
     · Inquiry is help-circle, not message-circle: every row here is a
       message, so message-circle would be the one glyph that says nothing.
     · `Other` gets NO glyph, and that is the decision, not an omission. It
       is the residual bucket — the absence of a category — and it is also
       the one chip with no hue. Giving it a glyph would spend the same
       visual commitment on "we could not classify this" as on a real
       category. The column still reads consistently because Other is
       consistently the bare one. */
.pill.cat-booking::before,   .cd-pill.cat-booking::before,
.pill.cat-inquiry::before,   .cd-pill.cat-inquiry::before,
.pill.cat-complaint::before, .cd-pill.cat-complaint::before,
.pill.cat-returning::before, .cd-pill.cat-returning::before,
.pill.cat-hangup::before,    .cd-pill.cat-hangup::before,
.pill.cat-spam::before,      .cd-pill.cat-spam::before {
  content: '';
  /* Both families are inline-flex now, so this is a flex item and its box is
     honoured either way. Kept explicit because a bare inline box would ignore
     width/height and collapse the glyph to nothing if that ever changed. */
  display: inline-block;
  vertical-align: -2px;
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  background-color: currentColor;
  -webkit-mask: var(--cat-glyph) center / contain no-repeat;
  mask: var(--cat-glyph) center / contain no-repeat;
}
.pill.cat-booking,   .cd-pill.cat-booking {
  --cat-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v4'/%3E%3Cpath d='M16 2v4'/%3E%3Cpath d='M21 13V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8'/%3E%3Cpath d='M3 10h18'/%3E%3Cpath d='M16 19h6'/%3E%3Cpath d='M19 16v6'/%3E%3C/svg%3E");
}
.pill.cat-inquiry,   .cd-pill.cat-inquiry {
  --cat-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E");
}
.pill.cat-complaint, .cd-pill.cat-complaint {
  --cat-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3'/%3E%3Cpath d='M12 9v4'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E");
}
.pill.cat-returning, .cd-pill.cat-returning {
  --cat-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12a9 9 0 1 0 3-6.7L3 8'/%3E%3Cpath d='M3 3v5h5'/%3E%3C/svg%3E");
}
.pill.cat-hangup,    .cd-pill.cat-hangup {
  --cat-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91'/%3E%3Cpath d='m2 2 20 20'/%3E%3C/svg%3E");
}
.pill.cat-spam,      .cd-pill.cat-spam {
  --cat-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m4.9 4.9 14.2 14.2'/%3E%3C/svg%3E");
}

/* The client table shows the same category chips WITHOUT the taxonomy's
   colour — a deliberate standing decision (see call-table.js: clients
   reserve saturated colour for STATUS). The glyph is not colour, so it
   crosses over; the hue does not. This is the opt-out, and it has to sit
   after the .pill.cat-* hue rules to win at equal specificity. */
.pill.pill--neutral {
  --chip-hue: currentColor;
  color: var(--text-secondary);
}

/* ═══════════════════════════════════════════════════════════════════════
   GLASS PASS — the controls join the rail's material language
   ═══════════════════════════════════════════════════════════════════════
   The sidebar is a floating glass panel now (css/sidebar.css, --rail-v4-*),
   and next to it the segmented controls and secondary buttons read as
   opaque plastic: flat fills, drawn borders, no lit edges. This brings the
   two control families onto the same material grammar the rail speaks.

   The grammar, per Apple's materials guidance:
     · the FILL is white-alpha, so the control takes its colour from
       whatever it sits on instead of hard-coding a surface hex;
     · a bright 0.5px INNER TOP EDGE reads as light catching the material,
       which is what separates glass from a painted rectangle;
     · a hairline RING at 0.5px defines the object without a drawn border;
     · a tight CONTACT shadow plus one soft ambient layer carry the lift.

   Two rules govern the whole pass:
   1. LIGHT IS NOT INVERTED DARK. White-alpha over a dark ground reads as
      glass; the same move on a white card is invisible. Light gets the lit
      white cap — the macOS/iOS button — defined by its bottom stop, its
      hairline and its shadow, which is exactly what the rail's own light
      FIELD token does (--rail-v4-field-edge is a lit white edge over a
      near-transparent fill, not a tint). The rail's SELECTION is the one
      deliberate exception: it carries a real accent tint, because it is the
      only object in the rail answering "where am I".
   2. NO FILL MAY MOVE A LABEL'S CONTRAST. The segmented thumb's top stop
      is capped at 0.11 by a prior measurement (--accent hits 4.47:1 at
      0.13) and it is NOT raised here — the glass comes entirely from the
      edges and the shadow. Every value below was checked against the label
      that sits on it.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── The trough: a deeper recess, lit along its lower inner edge ────────
   A groove looks sunken because light falls INTO it and catches the far
   wall. It had the top shadow and no far wall. */
.seg-control,
.segmented,
.segmented-control {
  border-color: rgba(255, 255, 255, 0.10);
  box-shadow:
    inset 0 1px 2px rgba(0, 0, 0, 0.32),
    inset 0 -0.5px 0 rgba(255, 255, 255, 0.055),
    0 0.5px 0 rgba(255, 255, 255, 0.05);

  /* The thumb, redefined IN SCOPE. global.css owns the token; overriding
     it on the control means every consumer — the calls period toggle, the
     analytics range strips, the agent-edit tabs, the to-do and calendar
     view switches — picks up the glass chip without a per-page rule, and
     the sliding .seg-indicator (which reads the same token) stays in
     lockstep with the static .active state by construction. */
  --seg-thumb-bg: linear-gradient(180deg, rgba(255,255,255,0.11) 0%, rgba(255,255,255,0.075) 100%);
  /* THE SELECTED LABEL LIFTS A STEP, and this is a contrast fix, not a
     taste one. The 0.11 cap was measured against --bg-card directly — but
     the thumb does not sit on the card, it sits on the TROUGH, which is
     itself 5% white over the card. Measured against the ground it actually
     has, --accent on the thumb's top stop is 4.01:1 on a card and 3.60:1
     on a sheet: both under AA, and both there before this pass. Lifting to
     --accent-strong clears it everywhere (worst case 5.10:1 on a sheet)
     and is the physically right answer anyway — a raised glass chip is the
     part of the control the light reaches first. */
  --seg-thumb-label: var(--accent-strong);
  --seg-thumb-shadow:
    inset 0 0.5px 0 rgba(255, 255, 255, 0.22),
    0 0 0 0.5px rgba(255, 255, 255, 0.09),
    0 1px 2px rgba(0, 0, 0, 0.38),
    0 4px 10px -3px rgba(0, 0, 0, 0.30);
}

[data-theme="light"] .seg-control,
[data-theme="light"] .segmented,
[data-theme="light"] .segmented-control {
  /* Fill stays at the audited 0.04 — see the note on the base rule; 0.055
     dropped the unselected label to 4.44:1 on a white card. The recess
     comes from the inset, not from a darker groove. */
  border-color: rgba(0, 0, 0, 0.06);
  box-shadow:
    inset 0 1px 2px rgba(30, 30, 50, 0.09),
    inset 0 -0.5px 0 rgba(255, 255, 255, 0.9),
    0 0.5px 0 rgba(255, 255, 255, 0.85);

  --seg-thumb-bg: linear-gradient(180deg, rgba(255,255,255,0.99) 0%, rgba(255,255,255,0.90) 100%);
  /* Light needs no lift: --accent on the near-white cap measures 5.29:1.
     Going to --accent-strong here would only darken it toward black and
     cost the label its accent identity. */
  --seg-thumb-label: var(--accent);
  --seg-thumb-shadow:
    inset 0 0.5px 0 #FFFFFF,
    0 0 0 0.5px rgba(0, 0, 0, 0.055),
    0 1px 2px rgba(30, 30, 50, 0.13),
    0 4px 10px -3px rgba(30, 30, 50, 0.10);
}

/* Hover on an UNSELECTED segment. It was colour-only, which on a glass
   trough reads as nothing happening. The fill is deliberately well under
   the thumb's (0.035 against 0.11) so a hovered segment can never be
   mistaken for the selected one — it is a pre-echo, not a state. */
.seg-btn:not(.active):hover,
.segmented-btn:not(.active):hover {
  background: rgba(255, 255, 255, 0.035);
}
/* Light sits at 0.40, not 0.55: on a white card a 0.55 hover fill lands
   close enough to the near-white thumb to read as a second selection. */
[data-theme="light"] .seg-btn:not(.active):hover,
[data-theme="light"] .segmented-btn:not(.active):hover {
  background: rgba(255, 255, 255, 0.40);
}

/* A disabled segment, anywhere. The recipe existed but was scoped to
   .agent-edit-tabs, so admin-analytics' "Custom" range — which IS disabled
   in JS, and whose click handler is therefore unreachable — still rendered
   as a live segment and still lit up under the pointer. A control that
   cannot be used has to say so before it is clicked, not after.

   Declared here, at the end, on purpose: the hover rules above are (0,3,0)
   and so is `.seg-btn[disabled]:hover`, so source order is what settles it. */
.seg-btn[disabled],
.seg-btn[disabled]:hover,
.segmented-btn[disabled],
.segmented-btn[disabled]:hover {
  color: var(--fg-disabled);
  background: transparent;
  cursor: not-allowed;
}

/* ── .btn-secondary: the same material, and a hover that finally shows ──
   THE BUG THIS ALSO FIXES: --bg-elevated and --bg-card-hover are the SAME
   hex in dark (#1F1F25), so `.btn-secondary:hover` was changing the border
   and nothing else — the fill moved from #1F1F25 to #1F1F25. Every
   secondary button in the app has been hovering invisibly. White-alpha
   fills make the two states genuinely different numbers.
   The label stays --text-primary on both, and 6.5% white over the darkest
   ground this button sits on (--bg-page #08080B) still measures >11:1. */
.btn-secondary {
  background: rgba(255, 255, 255, 0.065);
  color: var(--text-primary);
  border: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow:
    inset 0 0.5px 0 rgba(255, 255, 255, 0.14),
    0 1px 2px rgba(0, 0, 0, 0.32);
}
.btn-secondary:hover {
  background: rgba(255, 255, 255, 0.105);
  border-color: rgba(255, 255, 255, 0.16);
  box-shadow:
    inset 0 0.5px 0 rgba(255, 255, 255, 0.20),
    0 2px 6px rgba(0, 0, 0, 0.34);
  transform: translateY(-1px);
}
/* Press inverts the lighting, same as the filled variants: the ambient
   layer collapses and the cap sinks into its own shadow. */
.btn-secondary:active {
  transform: scale(0.97);
  background: rgba(255, 255, 255, 0.135);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.30);
}

/* Light: the lit white cap, not an inverted tint. A white-alpha fill on a
   white card is invisible, so definition comes from the gradient's bottom
   stop, the hairline and the contact shadow — the macOS button, and the
   same construction the rail's light selection chip uses. */
[data-theme="light"] .btn-secondary {
  background: linear-gradient(180deg, #FFFFFF 0%, #F7F6F4 100%);
  border-color: rgba(0, 0, 0, 0.09);
  box-shadow:
    inset 0 0.5px 0 #FFFFFF,
    0 1px 2px rgba(30, 30, 50, 0.10);
}
[data-theme="light"] .btn-secondary:hover {
  background: linear-gradient(180deg, #FFFFFF 0%, #F1F0ED 100%);
  border-color: rgba(0, 0, 0, 0.13);
  box-shadow:
    inset 0 0.5px 0 #FFFFFF,
    0 2px 6px rgba(30, 30, 50, 0.13);
}
[data-theme="light"] .btn-secondary:active {
  background: #EDECE9;
  box-shadow: inset 0 1px 2px rgba(30, 30, 50, 0.14);
}

/* ── .btn-ghost: neutral, like the rail's rows ──────────────────────────
   The ghost hover was an accent tint, which made a borderless label look
   like a selected item — the rail settled that argument: hover is neutral
   white-alpha, selection is the accent. Matches --nav-hover-bg / press. */
.btn-ghost:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.06);
}
.btn-ghost:active {
  background: rgba(255, 255, 255, 0.095);
}
[data-theme="light"] .btn-ghost:hover  { background: rgba(0, 0, 0, 0.045); }
[data-theme="light"] .btn-ghost:active { background: rgba(0, 0, 0, 0.07); }

/* Reduced transparency: the material is refused, so the controls flatten
   to the solid colours the glass was resolving to rather than to a
   different, heavier surface appearing. Same contract the rail keeps. */
@media (prefers-reduced-transparency: reduce) {
  .seg-control,
  .segmented,
  .segmented-control {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--bg-card-subtle);
  }
  .btn-secondary { background: var(--bg-elevated); }
  .btn-secondary:hover { background: var(--bg-control-hover); }
  [data-theme="light"] .btn-secondary { background: var(--bg-elevated); }
  [data-theme="light"] .btn-secondary:hover { background: var(--bg-control-hover); }
}

/* ═══════════════════════════════════════════════════════════════════════
   W5 · E2/E3/E7 — the call drawer becomes interruptible
   ═══════════════════════════════════════════════════════════════════════
   E2. The drawer was @keyframes, which cannot be grabbed and reversed
   mid-flight (Apple §3: interruptibility is the single most important
   principle). Clicking a second row while the first was still arriving, or
   Escaping mid-open, meant waiting for a scripted animation to finish its
   run. A class-toggled TRANSITION on transform always animates from the
   live presentation value, so an interrupted open reverses from wherever it
   actually is instead of jumping.

   The curve is the drawer curve — cubic-bezier(0.32, 0.72, 0, 1), a
   critically damped settle with no overshoot, over 380ms. ease-out-expo
   (the old panelIn) front-loads almost all its travel and then creeps, which
   is what made this drawer read as "arrives, then hangs".

   E3. And it was dimming the page like a modal — rgba(0,0,0,0.35) with
   blur(12px) across the full viewport — for a READING PANE the operator
   opens dozens of times a session. Apple's rule: dim to focus (a modal
   task), separate to keep flow (a parallel panel). This is the second one.
   0.28 with blur(2px) keeps the list legible behind the drawer, which is
   the point of opening a drawer instead of navigating.

   SCOPED TO #call-detail-panel on purpose. `.panel-overlay` is also the
   chassis for centred in-flow dialogs (notification settings, the broadcast
   composer) which never set `.is-open` — a global default of opacity: 0
   would render every one of them invisible. Those are modal tasks and keep
   the heavier scrim they should have.
   ═══════════════════════════════════════════════════════════════════════ */
#call-detail-panel,
#call-detail-panel.is-closing {
  animation: none;
  opacity: 0;
  background: rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  transition: opacity var(--duration-normal) var(--ease-out);
}
[data-theme="light"] #call-detail-panel,
[data-theme="light"] #call-detail-panel.is-closing {
  background: rgba(30, 30, 50, 0.20);
}
#call-detail-panel.is-open { opacity: 1; }

#call-detail-panel .panel-sheet,
#call-detail-panel.is-closing .panel-sheet {
  animation: none;
  transform: translateX(100%);
  transition: transform 380ms cubic-bezier(0.32, 0.72, 0, 1);
}
#call-detail-panel.is-open .panel-sheet { transform: translateX(0); }

/* Spatial consistency (Apple §7): the sheet arrives from the bottom on
   mobile, so it leaves the same way. */
#call-detail-panel.panel-mobile .panel-sheet,
#call-detail-panel.panel-mobile.is-closing .panel-sheet {
  transform: translateY(100%);
}
#call-detail-panel.panel-mobile.is-open .panel-sheet { transform: translateY(0); }

/* E7 — the hint is scoped to "while the drawer exists". A permanent
   will-change on a 440-720px sheet keeps a compositor layer alive for the
   whole session; this one is created on open and released on close, which
   is what the property is for. */
#call-detail-panel:not(.hidden) .panel-sheet { will-change: transform; }

@media (prefers-reduced-motion: reduce) {
  #call-detail-panel .panel-sheet,
  #call-detail-panel.is-closing .panel-sheet,
  #call-detail-panel.is-open .panel-sheet,
  #call-detail-panel.panel-mobile .panel-sheet,
  #call-detail-panel.panel-mobile.is-open .panel-sheet {
    transform: none;
    transition: none;
  }
  /* The crossfade stays — it is the non-vestibular equivalent, not motion
     to be removed (Apple §14). */
}
@media (prefers-reduced-transparency: reduce) {
  #call-detail-panel,
  #call-detail-panel.is-closing {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(0, 0, 0, 0.45);
  }
}

/* E1 (the half that lives in CSS) — the label and the thumb have to land
   together. The segment's colour transition runs at --duration-normal
   (200ms) while the sliding indicator travels on --motion-settle (340ms),
   so the new segment finished turning accent 140ms before the thumb
   arrived under it — the label changed, then the surface caught up.
   Same clock for both, same curve.

   Only applies where an indicator actually slides; a static .active
   control has nothing to stay in step with and keeps the faster hover
   clock, which is what a colour-only change should have. */
.has-indicator .seg-btn,
.has-indicator .segmented-btn {
  transition-duration: var(--motion-settle);
  transition-timing-function: var(--ease-settle);
}
.has-indicator .seg-btn:hover,
.has-indicator .segmented-btn:hover {
  transition-duration: var(--motion-hover);
}
.has-indicator .seg-btn:active,
.has-indicator .segmented-btn:active {
  transition-duration: var(--motion-press);
}
/* The indicator is the one moving object here, and now that it moves on
   transform the hint can actually be honoured. */
.seg-indicator { will-change: transform; }
@media (prefers-reduced-motion: reduce) {
  .has-indicator .seg-btn,
  .has-indicator .segmented-btn { transition-duration: var(--motion-hover); }
  .seg-indicator { will-change: auto; }
}


/* ── Filters: one control, two surfaces ───────────────────────────────────
   Four labelled selects is four things to read before you can narrow a call
   list, and at every width it was the wrong shape for the space: on a phone
   they stacked into ~264px of chrome before the first call; on a wide screen
   they sat on a second row, right-aligned, with 600px of nothing to their
   left and nothing lining up with the search above them.

   So the SET lives behind one trigger and the STATE comes out in front of
   it as chips — which is the part you need at a glance and the part four
   dropdowns were hiding. The panel behind the trigger is one element with
   two presentations: an anchored popover on a pointer, a bottom sheet on a
   phone. Same DOM, same selects, same handlers; only the surface changes. */
.filter-trigger-wrap { position: relative; flex: 0 0 auto; }

.filter-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 40px;
  padding: 0 var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-card);
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  white-space: nowrap;
  transition: background var(--motion-hover) var(--ease-out),
              border-color var(--motion-hover) var(--ease-out),
              color var(--motion-hover) var(--ease-out);
}
.filter-trigger i,
.filter-trigger svg { width: 16px; height: 16px; }
.filter-trigger:hover { background: var(--bg-card-hover); color: var(--text-primary); }
.filter-trigger:active { transform: scale(0.96); }
/* Filters ON is worth seeing without reading the chips. */
.filter-trigger.is-active {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--text-primary);
}
/* The count is an ELEMENT with a `hidden` attribute the script toggles, and
   a class rule outranks the UA's [hidden] { display: none } — so the badge
   was painting "0" on a toolbar with no filters on (visible in the owner's
   2026-08-09 screenshot). A badge that never goes away says nothing. */
.filter-trigger__count[hidden] { display: none; }
.filter-trigger__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--text-inverse);
  font-size: var(--text-overline);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The active-filter chip row that used to live here was removed 2026-08-08.
   It restated what the panel already showed, it changed the toolbar's height
   every time a filter moved — which jumped the call list under it — and its
   per-chip × is covered by Clear all. Active state now lives in exactly two
   places: the count badge on the trigger, and the selects inside the panel.
   Removing it also un-breaks calls.html, whose independent-toggle
   `.filter-chip`s were being restyled by the (later, same-specificity) rules
   this block used to hold — that family is defined once again, up in the
   "Filter Chips (independent toggles)" section. */

/* The panel's shared skin. Placement and entrance differ per surface. */
.filter-panel {
  display: flex;
  flex-direction: column;
  background: var(--glass-surface-bg);
  -webkit-backdrop-filter: var(--glass-surface-filter);
  backdrop-filter: var(--glass-surface-filter);
  box-shadow: var(--glass-surface-shadow);
}
.filter-panel[hidden] { display: none; }

/* ── The header row ──
   Title left, Clear all right, one 16px gutter shared with the body below.
   The title was --text-title (18px) — a section heading inside a 296px menu,
   which is what made the panel read as a small window rather than as the
   trigger's contents. Both items are --text-sm now and the hierarchy comes
   from weight and colour instead, which is the app's own grammar. */
.filter-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 14px var(--space-4) 10px;
}
.filter-panel__head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}
/* Clear all is a text button, not a bordered one: a menu header has room for
   a verb, not for a second control with a box round it. The .btn chassis it
   inherits brings a 36px floor and a border, both of which would set the
   header's height; this takes them off and buys the tap area back with a
   negative-inset ::after so a phone still gets 44px. */
.filter-panel__head .btn {
  position: relative;
  min-height: 0;
  height: auto;
  padding: 0;
  border: none;
  background: none;
  box-shadow: none;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  transition: color var(--motion-hover) var(--ease-out);
}
.filter-panel__head .btn::after { content: ""; position: absolute; inset: -12px -8px; }
.filter-panel__head .btn:hover { background: none; color: var(--accent); }
.filter-panel__head .btn:active { transform: none; color: var(--accent-hover); }
/* The rule above sets box-shadow: none at the same (0,2,0) as
   `.btn:focus-visible` and later in the file, so it was winning — and
   .btn:focus-visible's `outline: none` still applied, which left this button
   with no keyboard focus indicator at all. An outline rather than the shared
   two-layer box-shadow ring, deliberately: that ring paints a 2px --bg-page
   gap, and a page-coloured gap over a translucent panel reads as a hole cut
   in the glass (the same reason the rail carries its own ring colour). */
.filter-panel__head .btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
  color: var(--text-primary);
}

/* ── The rows ──
   16px inner padding on every side (the head supplies the top), and one
   12px rhythm between rows. A row is label + select and nothing else:
   11px overline (the app's own .filter-label, which is also what the table
   column headers under this panel use) over the 36px control. 13.2 + 4 + 36
   = 53px of row, 65px of pitch. */
.filter-panel__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--space-4) var(--space-4);
}
.filter-panel__body .filter-cluster {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
}
.filter-panel__body .filter-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  width: 100%;
  /* Explicit, so the group can never inherit a toolbar flex-basis again —
     a basis on a column child is a row height, which is how this panel
     grew 130px voids between its selects. */
  flex: 0 0 auto;
  min-width: 0;
}
.filter-panel__body .filter-label {
  line-height: 1.2;
}
/* Export is an action, not a filter — it never travels into the panel. */
.filter-panel__body .filter-group--action { display: none; }
.filter-panel__body .form-select { width: 100%; min-height: 36px; }

/* ── Pointer: an anchored popover ─────────────────────────────────────── */
@media (min-width: 768px) {
  .filter-panel {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: var(--z-dropdown);
    /* 296, not 280: "Insufficient evidence" is the longest option on this
       page and at 280 it ran to the caret with no air. Still a menu, not a
       panel. */
    width: 296px;
    border: var(--glass-surface-border);
    border-radius: var(--radius-lg);
    /* Grows from the control that opened it, which is the whole reason a
       popover reads as "this button's contents" rather than as a new
       window that appeared. */
    transform-origin: top left;
    transform: scale(0.96);
    opacity: 0;
    transition: transform var(--motion-hover) var(--ease-out),
                opacity var(--motion-hover) var(--ease-out);
  }
  .filter-panel.is-open { transform: scale(1); opacity: 1; }
  /* The grab handle and the Done button are the sheet's furniture: a
     popover is dismissed by clicking away from it. */
  .filter-panel__grab,
  .filter-panel__foot { display: none; }
  /* Right-aligned when the trigger is near the right edge would need
     measurement; the trigger sits mid-row here, so left is always safe. */
  .filter-bar__end { margin-left: auto; flex: 0 0 auto; }
}

/* ── Phone: a bottom sheet ────────────────────────────────────────────── */
@media (max-width: 767px) {
  .filter-panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: var(--z-panel);
    max-height: 80vh;
    /* Meets the screen edge, so only the top corners are round. */
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    border-top: var(--glass-surface-border);
    padding-bottom: env(safe-area-inset-bottom);
    transform: translateY(100%);
    transition: transform var(--motion-settle) var(--ease-settle);
    will-change: transform;
  }
  .filter-panel.is-open { transform: translateY(0); }
  .filter-panel__grab {
    width: 36px;
    height: 4px;
    margin: var(--space-2) auto 0;
    border-radius: var(--radius-full);
    background: var(--border-strong);
  }
  .filter-panel__foot {
    padding: var(--space-3) var(--space-4) var(--space-4);
    border-top: 1px solid var(--border-subtle);
  }
  .filter-panel__foot .btn { width: 100%; min-height: 44px; }
  .filter-panel__body .form-select { min-height: 44px; }

  /* The block that lived here compacted Export inside .page-header-actions
     and hid .filter-bar__end, because Export used to be exiled to the page
     header on a phone. It rides in the toolbar at every width now, so both
     halves went with it — .page-header-actions has no download menu on any
     page today, and hiding the toolbar's own Export slot would hide Export. */
}

.filter-sheet-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  background: var(--glass-backdrop-bg);
  -webkit-backdrop-filter: var(--glass-backdrop-blur);
  backdrop-filter: var(--glass-backdrop-blur);
  opacity: 0;
  transition: opacity 140ms var(--ease-out);
}
.filter-sheet-scrim.is-open { opacity: 1; }
.filter-sheet-scrim[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .filter-panel,
  .filter-sheet-scrim { transition: none; }
  .filter-trigger:active { transform: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   PROMOTED PRIMITIVES

   Three treatments that were finished inside css/clients.css — a stylesheet
   only admin-clients.html loads — and so had no way of reaching the rest of
   the app. They are named here for what they are; the roster keeps its own
   names for where things sit, which is the part that really is page-local.
   ══════════════════════════════════════════════════════════════════════ */

/* ── Status pill ────────────────────────────────────────────────────────
   ONE encoding for "what state is this thing in": a tinted fill with a
   matching edge and no leading dot, because the tint already IS the
   encoding and a dot beside it states the same fact twice. Tints are pinned
   at .10 fill / .22 edge in both themes so a state reads at the same
   strength wherever it appears.

   The modifiers are named for the TONE, not the domain state. A pill has no
   idea what "onboarding" means; the page that renders it does, and that
   mapping belongs at the render site. */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  padding: 3px 9px;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0.005em;
  line-height: 1.45;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.status-pill--success {
  background: rgba(var(--success-rgb), 0.10);
  color: var(--pill-success-fg);
  border-color: rgba(var(--success-rgb), 0.22);
}
.status-pill--info {
  background: rgba(var(--info-rgb), 0.10);
  color: var(--pill-info-fg);
  border-color: rgba(var(--info-rgb), 0.22);
}
.status-pill--warning {
  background: rgba(var(--warning-rgb), 0.10);
  color: var(--pill-warning-fg);
  border-color: rgba(var(--warning-rgb), 0.22);
}
.status-pill--neutral {
  background: var(--bg-elevated);
  color: var(--text-muted);
  border-color: var(--border-subtle);
}
.status-pill--accent {
  background: rgba(var(--accent-rgb), 0.10);
  color: var(--accent);
  border-color: rgba(var(--accent-rgb), 0.22);
}
/* Light theme's --accent is tuned for large fills, not for 11px text on a
   10%-alpha ground; --accent-hover is the darker step that carries it. */
[data-theme="light"] .status-pill--accent { color: var(--accent-hover); }
/* A glyph beside the label ("on test", the chat feedback states) sits at
   the badge's 12px so a pill and a badge in one row carry the same icon. */
.status-pill i,
.status-pill svg { width: 12px; height: 12px; flex-shrink: 0; }

/* ── Anchored popover ───────────────────────────────────────────────────
   A menu surface that hangs off the control that opened it, positioned by
   JS (which measures the trigger and sets left/top/transform-origin) rather
   than by a positioned ancestor — because the list it belongs to usually
   sits inside an overflow:hidden card, where an absolutely-positioned menu
   would be clipped.

   Same chrome as the app's other dropdown, .admin-detail-menu: 1px border
   over --shadow-lg. The surface differs by theme because the elevation has
   to be real in both — dark steps UP off the card it covers, light stays
   the brightest thing on an off-white page. */
.pop-anchored {
  position: fixed;
  z-index: var(--z-dropdown);
  min-width: 240px;
  max-width: 320px;
  max-height: min(62vh, 480px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-2);
  border-radius: var(--radius-lg);   /* concentric: 12 outer - 8 pad = 4 inner… */
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  /* Materializes rather than fades: blur + scale together read as a real
     surface arriving from the button, which is what the origin is for. */
  opacity: 0;
  transform: scale(0.96);
  filter: blur(3px);
  transition: opacity var(--motion-popover) var(--ease-out),
              transform var(--motion-popover) var(--ease-spring-soft),
              filter var(--motion-popover) var(--ease-out);
}

[data-theme="light"] .pop-anchored { background: var(--bg-card-solid); }

.pop-anchored.is-open {
  opacity: 1;
  transform: scale(1);
  filter: blur(0);
}

/* The trigger is usually a bare ⋯, so the menu has to say what it belongs to. */
.pop-anchored__head {
  padding: 4px var(--space-2) var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pop-anchored__sep {
  height: 1px;
  margin: var(--space-2) calc(var(--space-2) * -1 + 2px);
  background: var(--separator);
}

/* Mobile: a right-edge-anchored 240px card hanging off a 40px button on a
   390px screen is a worse target than the sheet it replaced, and there is no
   hover to make the anchoring legible. JS skips the measurement here. */
/* z-index, added 2026-08-25. The base surface inherits --z-dropdown (100). The
   mobile tab bar is fixed to the bottom at --z-overlay (9999). This sheet is
   ALSO pinned to the bottom, by construction, so the two always collide: a
   four-row menu at 390 overlapped the bar by 82px, and elementFromPoint on the
   last row returned .mobile-nav-item. The rows were not merely hidden, the bar
   was STEALING the taps -- pressing where "Spam" appeared navigated to another
   page.
   --z-panel is the ladder's existing "panel or sheet above a backdrop" tier,
   not a fresh number, which is the same reasoning --above-drawer uses below.
   That rule stays correct: it is declared after this one at equal specificity
   and sits higher again, so a menu opened inside the call drawer still wins. */
.pop-anchored--sheet {
  left: var(--space-2);
  right: var(--space-2);
  top: auto;
  bottom: var(--space-2);
  z-index: var(--z-panel);
  min-width: 0;
  max-width: none;
  max-height: 72vh;
  padding: var(--space-2) var(--space-2) var(--space-4);
  border-radius: var(--radius-2xl);
  transform-origin: bottom center;
}
.pop-anchored--sheet .pop-anchored__head { padding: var(--space-2) var(--space-3); }

/* Opened from INSIDE the call detail drawer (2026-08-15). The base surface
   sits at --z-dropdown (100), which is below --z-drawer (10001) — used as-is
   in the drawer the menu paints behind it and is invisible. --z-popover is
   the ladder's existing "nested above an open drawer" tier; a fresh number
   would break the ladder's own rule. */
.pop-anchored--above-drawer { z-index: var(--z-popover); }

@media (prefers-reduced-motion: reduce) {
  .pop-anchored {
    transform: none;
    filter: none;
    transition: opacity var(--motion-hover) linear;
  }
  .pop-anchored.is-open { transform: none; }
}

/* ── Menu rows (.sheet-action) ──────────────────────────────────────────
   PROMOTED from css/clients.css (2026-08-15) for the same reason .pop-anchored
   itself was: "a row inside a menu surface" stopped being a roster idea the
   moment a second page opened one of these menus. The calls surfaces load
   components.css and not clients.css, so leaving the vocabulary there would
   have meant a second copy of it. */

.sheet-actions { display: flex; flex-direction: column; gap: 2px; }

.sheet-action {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 48px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  color: var(--text-primary);
  font-size: var(--text-sm);
  text-align: left;
  transition: background var(--motion-hover) var(--ease-out);
}
.sheet-action svg { width: 17px; height: 17px; color: var(--text-muted); flex-shrink: 0; }
.sheet-action > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* --bg-control-hover, not --bg-card-hover. A .sheet-action never sits on a
   card — it is a menu row, so it is always inside a popover, sheet or modal,
   i.e. on --bg-elevated or --bg-sheet. In dark, --bg-elevated and
   --bg-card-hover are the same #1F1F25, so this hover painted the colour the
   surface had already painted and rendered nothing at all. A control hover
   has to be a step above whatever it sits on, which is exactly what
   --bg-control-hover is for. Promoted from the .roster-pop-scoped patch that
   used to sit 80 lines up and covered one of the several places these rows
   appear. */
.sheet-action:hover { background: var(--bg-control-hover); }
.sheet-action:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sheet-action.is-current { background: var(--accent-muted); }
.sheet-action__check { color: var(--accent) !important; }
.sheet-action--accent { color: var(--accent); }
[data-theme="light"] .sheet-action--accent { color: var(--accent-hover); }
.sheet-action--accent svg { color: inherit; }
.sheet-action--danger { color: var(--danger); }
.sheet-action--danger svg { color: inherit; }
[data-theme="light"] .sheet-action--danger { color: var(--pill-danger-fg); }

.sheet-note {
  margin-top: var(--space-3);
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: 1.5;
}

/* The ⋯ sheet carries Edit profile above the folder list, so the folder block
   needs a heading to stay unambiguous once the title is just the org name. */
.sheet-group-label {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--text-overline);
  font-weight: 600;
  letter-spacing: var(--tracking-overline);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* The popover's 8px padding sets the concentric inner radius, so the rows sit
   at --radius-md rather than the surface's --radius-lg. */
.pop-anchored .sheet-action {
  min-height: 34px;
  padding: 7px var(--space-2);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}
.pop-anchored .sheet-action svg { width: 15px; height: 15px; }

.pop-anchored .sheet-group-label { margin: var(--space-3) var(--space-2) var(--space-1); }
.pop-anchored .sheet-note {
  margin: var(--space-2) var(--space-2) var(--space-1);
  font-size: 10.5px;
}

/* Touch targets grow with the sheet variant. */
.pop-anchored--sheet .sheet-action { min-height: 48px; padding: 10px var(--space-3); }
.pop-anchored--sheet .sheet-action svg { width: 17px; height: 17px; }

/* ── Classification ⋯ menu (2026-08-15) ─────────────────────────────────
   The check is drawn on EVERY row and hidden when the row is off, rather
   than rendered conditionally: the state flips on click without touching
   innerHTML, so the clicked row keeps focus, and reserving the glyph's box
   means the labels do not shift under the cursor. Scoped to .cc-menu because
   the roster's menus render their checks conditionally and would be blanked
   by an unscoped rule. */
.cc-menu .sheet-action__check { visibility: hidden; }
.cc-menu .sheet-action[aria-checked="true"] .sheet-action__check { visibility: visible; }

/* ── Classification ⋯ trigger (agency admin) ────────────────────────────
   Same control as the roster's .roster-more (css/clients.css): ALWAYS
   present, just quiet — hover-only actions are undiscoverable and dead on
   touch. 0.75 is the measured floor (3.62:1 dark / 3.29:1 light against the
   row, over the 3:1 WCAG 1.4.11 minimum for a UI component). */
.call-classify-btn {
  position: relative;
  width: 30px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  opacity: 0.75;
  background: transparent;
  transition: color var(--motion-hover) var(--ease-out),
              background var(--motion-hover) var(--ease-out),
              opacity var(--motion-hover) var(--ease-out),
              transform var(--motion-press) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
.call-classify-btn::before { content: ""; position: absolute; inset: -6px -7px; }  /* 44 x 44 hit area */
.call-classify-btn i[data-lucide],
.call-classify-btn svg { width: 16px; height: 16px; }
.call-classify-btn:hover,
/* The popover hangs off this button, so the button stays lit while it is
   open or the menu reads as belonging to nothing. */
.call-classify-btn[aria-expanded="true"] { color: var(--text-primary); background: var(--bg-control-hover); opacity: 1; }
.call-classify-btn:active { transform: scale(0.94); }
.call-classify-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 1; }


/* ── Click-to-copy affordance ───────────────────────────────────────────
   A value that is also a button. The glyph only appears on hover or focus,
   so at rest the line reads as text and nothing about it is furniture. */
.admin-copy {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 0;
  padding: 0;
  background: transparent;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: color var(--motion-hover) var(--ease-out);
}
.admin-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--radius-xs); }
.admin-copy .admin-copy__value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* The glyph slot is the same size whether it is showing, hidden or confirming,
   so nothing on the line ever moves. */
.admin-copy__glyph {
  position: relative;
  display: inline-block;
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}
.admin-copy__icon {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  transform: scale(0.25);
  filter: blur(4px);
  transition: opacity var(--motion-popover) cubic-bezier(0.2, 0, 0, 1),
              transform var(--motion-popover) cubic-bezier(0.2, 0, 0, 1),
              filter var(--motion-popover) cubic-bezier(0.2, 0, 0, 1);
}
.admin-copy__icon svg { width: 12px; height: 12px; display: block; }
.admin-copy:hover .admin-copy__icon--idle,
.admin-copy:focus-visible .admin-copy__icon--idle {
  opacity: 1;
  transform: scale(1);
  filter: blur(0);
}
.admin-copy.is-copied .admin-copy__icon--idle { opacity: 0; transform: scale(0.25); filter: blur(4px); }
.admin-copy.is-copied .admin-copy__icon--done { opacity: 1; transform: scale(1); filter: blur(0); }
/* At 12px a check alone is easy to miss, so the value itself confirms too. */
.admin-copy.is-copied { color: var(--accent); }
[data-theme="light"] .admin-copy.is-copied { color: var(--accent-hover); }

.admin-copy__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .admin-copy__icon { transition: opacity var(--motion-hover) linear; transform: none; filter: none; }
  .admin-copy.is-copied .admin-copy__icon--idle,
  .admin-copy .admin-copy__icon { transform: none; filter: none; }
  .admin-copy.is-copied .admin-copy__icon--done { transform: none; filter: none; }
}

/* ─────────────────────────────────────────────────────────────────────
   Booking-rate note (2026-08-09)

   The rate's denominator is smaller than the calls count sitting next to
   it, so the label carries a "*" that answers why. The mark is a button:
   hover alone would strand keyboard and touch, and the bubble lives on
   <body> (js/kpi-cards.js) because .kpi-card is overflow:hidden.
   ───────────────────────────────────────────────────────────────────── */
.kpi-note {
  flex: 0 0 auto;
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  width: auto;
  height: auto;
  padding: 0;
  /* Cancel .kpi-label's flex gap so the "*" reads as part of the word rather
     than as a separate item in the row. */
  margin-left: calc(-1 * var(--space-2));
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: help;
  line-height: 1;
  transition: color var(--duration-fast) ease;
}
/* Hit area, not layout. A 20px-wide button would push the glyph off the word;
   an inset pseudo-element gives thumb and pointer the same target without
   spending a single pixel of the label row. */
.kpi-note::after {
  content: '';
  position: absolute;
  inset: -10px -8px;
}
.kpi-note__mark {
  font-size: 13px;
  font-weight: 600;
  /* An asterisk renders high in its em box already; a couple of px down
     lands it on the label's cap height instead of above it. */
  padding-top: 3px;
}
.kpi-note:hover,
.kpi-note[aria-expanded="true"] { color: var(--text-secondary); }
.kpi-note:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  color: var(--text-secondary);
}

.kpi-note-bubble {
  position: fixed;
  left: 0;
  top: 0;
  z-index: var(--z-menu);
  max-width: 272px;
  padding: 10px 12px;
  background: var(--bg-elevated, var(--bg-card));
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0;
  text-transform: none;
  white-space: normal;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--duration-fast) ease, visibility var(--duration-fast) ease;
}
.kpi-note-bubble.is-open {
  opacity: 1;
  visibility: visible;
}
.kpi-note-bubble::before {
  /* Rotated square, not a border triangle: it inherits the surface AND the
     border colour, so the arrow stays correct in both themes. */
  content: '';
  position: absolute;
  top: -5px;
  left: var(--note-arrow-x, 50%);
  width: 8px;
  height: 8px;
  margin-left: -4px;
  background: var(--bg-elevated, var(--bg-card));
  border-left: 1px solid var(--border-default);
  border-top: 1px solid var(--border-default);
  transform: rotate(45deg);
}

/* The admin overview ring card hosts its label inside .kpi-content, a flex
   column that already supplies the gap below the label. .kpi-label's own
   margin-bottom would double it and push the ring off the card's centre. */
.kpi-content > .kpi-label { margin-bottom: 0; }

@media (max-width: 480px) {
  .kpi-note-bubble { max-width: calc(100vw - 24px); }
}

/* ── Agents grid: Archived folder + drawer archive/restore (2026-08-10) ──────
   Archive takes an agent out of its client folder without deleting it (a
   `status` write; deleted_at is untouched). The Archived card is the folder it
   lands in — rendered only while something is archived, last in the grid. */

/* Quiet and neutral BY INSTRUCTION: no wash, no dashed edge, no status colour.
   It is not a client and it is not the sandbox, so it borrows neither card's
   signal — the grey tile against four accent tiles is the whole distinction,
   with the count as the second. Tinting it would have made a third
   colour-coded card kind on a grid whose point is to be scanned. */
.agents-folder-card--archived .agents-folder-icon {
  background: var(--bg-elevated);
  color: var(--text-secondary);
}
/* The one number on this grid. Client cards are icon + name by decision (see
   _renderClientFolders), so this is deliberately the exception, not a stat line
   creeping back: an Archived folder with no count says nothing about whether
   it is worth opening. Mono + muted matches the drawer's role counts. */
.agents-folder-count {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Row actions. Both sit at 32px and above the stretched .drawer-agent-main
   link (which covers the row via ::after, inset 0) for the same reason the
   duplicate button does — without the z-index lift the link would swallow the
   click and navigate to the agent instead of archiving it. */
.client-agents-drawer .drawer-archive-btn,
.client-agents-drawer .drawer-restore-btn {
  position: relative;
  z-index: 1;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
}
/* lucide swaps the <i> for an <svg>, so an `i`-only rule stops matching the
   moment icons hydrate and the glyph falls back to its intrinsic 24px. */
.client-agents-drawer .drawer-archive-btn i,
.client-agents-drawer .drawer-archive-btn svg,
.client-agents-drawer .drawer-restore-btn i,
.client-agents-drawer .drawer-restore-btn svg {
  width: 16px;
  height: 16px;
}

/* Archived drawer search. .search-wrapper carries `flex: 1; min-width: 300px`
   for the filter bars it was written for; in the drawer's block flow the flex
   does nothing and the floor is a horizontal-overflow risk on a narrow sheet,
   so both are released. Height (36 desktop / 44 coarse) stays the shared
   .search-wrapper .search-input geometry — same field as every other page. */
.archived-agents-drawer .archived-search {
  flex: none;
  min-width: 0;
  margin-bottom: var(--space-4);
}

/* ── Overview aside: UP NEXT → UPCOMING is one list, not two blocks ─────────
   The generic `.today-ap-section + .today-ap-section` rule above draws a
   hairline between every pair of aside sections. That is right before
   FOLLOW-UPS, which is a genuinely different kind of thing, and wrong before
   UPCOMING, which is more of the same thing: on a day with no follow-ups the
   card is simply the next three appointments, and a line through its middle
   split them into two unrelated groups (Roman, 2026-08-10).
   Placed at the end of the file deliberately — it has the same specificity as
   both the base rule and the ≤767px one that re-sets padding-top, so source
   order is what has to win, in both viewports. */
.today-stathero-aside > .today-ap-section + .today-ap-section--upcoming {
  padding-top: 0;
  border-top: 0;
}
