/* ============================================================================
   POWER DIALER DESIGN SYSTEM — components.css
   Every component in the product. Load after tokens.css + core.css.

   CONTENTS
     01 Buttons            08 Banners & messages     15 Call bar & call state
     02 Fields & inputs    09 Tables & queue rows    16 Transport & keypad
     03 Toggle & checks    10 Stat tiles             17 Activity feed
     04 Segmented & tabs   11 Badges, dots, pills    18 Skeletons & spinner
     05 Dropdown menus     12 Chips (dispositions)   19 Progress
     06 Modals             13 Avatar                 20 Collapse & tooltip
     07 Toasts             14 Sidebar                21 Empty states, kbd, misc

   Legacy aliases from the current app (`.btn.danger`, `.btn.ghost`,
   `.pill.ok/.warn/.bad`, `.msg.ok/.msg.err`) are kept working so the later
   restyle of app.html / dialer.html is close to find-and-replace.
   ========================================================================== */


/* ===========================================================================
   00 ACCENT INK — the accents AS SMALL TEXT.

   tokens.css locks each accent to a call state, and Rule 2 says colour is
   state. That is right for a 6px dot or a 3px bar, where WCAG asks 3:1 of a
   graphic. It is not right for the 10-11px uppercase labels the same accents
   are used for, which have to clear 4.5:1 like any other text. Measured on
   --surface-sunk (#E9E7E2, the worst of the three light grounds because a
   dark ink loses contrast as the ground darkens):

       --ringing    #B8801E   2.77:1   FAIL   (the CALLBACKS / PRICE SENSITIVE
                                               badges, RINGING in the call bar)
       --connected  #2F8A5C   3.46:1   FAIL   (the Booked chip, LIVE)
       --failed     #B54430   4.43:1   FAIL   (marginally)
       --accent     #4557C9   4.91:1   pass   — used raw

   These take the SAME hue darkened toward the shadow ink, which is the recipe
   the pack already uses for pressed accent fills, so nothing new enters the
   palette. Measured after: ringing 4.79:1, connected 4.76:1, failed 4.86:1 on
   sunk, and better on base and raised.

   Dark theme lifts every accent ~14% lightness already and measures 7.50 /
   6.34 / 5.09:1 on its own sunk ground, so dark uses the raw tokens. Dots,
   bars and progress keep the raw token in BOTH themes — they are graphics and
   answer to 3:1, not 4.5:1. Fills do too, with ONE exception: --on-accent on a
   raw --connected fill measures 3.86:1 in light, so the two green fills that
   carry real words — .badge.fill-ok and .btn.success ("Resume") — take
   --connected-ink and read 5.31:1. Dark's --connected-ink IS the raw token, so
   dark's 6.49:1 is untouched by that swap.
   ========================================================================= */
:root {
  --ringing-ink:   color-mix(in srgb, var(--ringing)   66%, rgb(var(--sh-dark)));
  --connected-ink: color-mix(in srgb, var(--connected) 78%, rgb(var(--sh-dark)));
  --failed-ink:    color-mix(in srgb, var(--failed)    92%, rgb(var(--sh-dark)));
  /* The note above says --accent "does not need one". Re-measured on the
     ground a .banner.info actually composites to (accent-wash over the panel
     under it), raw --accent is 4.58:1 on base, 4.77:1 on raised and 4.30:1 on
     sunk — the sunk case misses. Same recipe as its three siblings; 4.72:1 at
     the worst of the three. */
  --accent-ink:    color-mix(in srgb, var(--accent)    92%, rgb(var(--sh-dark)));
}
[data-theme="dark"] {
  --ringing-ink:   var(--ringing);      /* 6.21:1 on raised — passes raw     */
  --connected-ink: var(--connected);    /* 5.25:1 on raised — passes raw     */
  /* The one dark accent that does not: --failed reads 4.22:1 on the RAISED
     ground a chip or a badge sits on (it is fine on base and sunk). Lifted
     toward --ink-1 rather than darkened, because on dark the ground is the
     dark side. 4.55:1 on raised, 5.01:1 on base. */
  --failed-ink:    color-mix(in srgb, var(--failed) 92%, var(--ink-1));
  /* Dark's raw --accent is the worst of the four on a banner wash: 3.80:1 on
     the raised ground. Lifted toward --ink-1 like --failed-ink, for the same
     reason — on dark, the ground is the dark side. 4.66:1 at the worst. */
  --accent-ink:    color-mix(in srgb, var(--accent) 80%, var(--ink-1));
}


/* ===========================================================================
   00b ICONOGRAPHY — masked SVG. Never a font glyph.

   The pack drew its whole icon set out of the Unicode symbol block — the
   transport's play, the caret, the collapse chevron, the modal close, the
   keypad's star, the Enter hint. Measured with canvas advance widths (a
   glyph a font does not contain measures identically to the same glyph in a
   font that does not exist), Manrope contains NONE of them:

       in Manrope      ·  —  ↑  ↓  …  ✓          -> safe, left as characters
       NOT in Manrope  ▶  ◀  ▮  ▾  ↵  →  ✳  ✕  ◐  ⇄

   Everything in the second row fell through the whole family stack to the
   OS last-resort font. On macOS that resolves to a symbol font and the
   glyph appears, which is why this shipped; on a machine without one it
   resolves to notdef and the owner sees a plain square where the play
   button should be. Loading Manrope over the network compounds it — every
   one of these is also wrong for the ~200ms before the webfont lands.

   A mask cannot fail this way. The shape is geometry in the stylesheet, it
   inherits `currentColor` so it is theme- and contrast-correct by
   construction, it scales, and it needs no font, no request and no library.
   ========================================================================= */
:root {
  --ico-phone: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M6.6 10.8a15.1 15.1 0 0 0 6.6 6.6l2.2-2.2a1 1 0 0 1 1-.25c1.15.38 2.36.58 3.6.58a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.24.2 2.45.58 3.6a1 1 0 0 1-.25 1z'/></svg>");
  --ico-caret: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='M6 8.7 1.4 3.9h9.2z'/></svg>");
  --ico-close: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M3.4 2.3 8 6.9l4.6-4.6 1.1 1.1L9.1 8l4.6 4.6-1.1 1.1L8 9.1l-4.6 4.6-1.1-1.1L6.9 8 2.3 3.4z'/></svg>");
  --ico-enter: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M13.5 3v4.6a1.6 1.6 0 0 1-1.6 1.6H3.2'/><path d='M6.2 6.2 3.2 9.2l3 3'/></svg>");
  --ico-star: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'><path d='M8 2.9v10.2M3.6 5.4l8.8 5.2M12.4 5.4 3.6 10.6'/></svg>");
  --ico-theme: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><circle cx='8' cy='8' r='6.3' fill='none' stroke='black' stroke-width='1.4'/><path d='M8 2.4a5.6 5.6 0 0 0 0 11.2z'/></svg>");
  --ico-prev: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11.2 5.6v12.8L2.6 12zM21.4 5.6v12.8L12.8 12z'/></svg>");
  --ico-next: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12.8 5.6v12.8L21.4 12zM2.6 5.6v12.8L11.2 12z'/></svg>");
}
.ico {
  display: inline-block;
  flex: none;
  width: 1em;
  height: 1em;
  background: currentColor;
  -webkit-mask: var(--ico) center / contain no-repeat;
  mask: var(--ico) center / contain no-repeat;
}
.ico-sm { width: 10px; height: 10px; }
.ico-md { width: 14px; height: 14px; }
.ico-lg { width: 17px; height: 17px; }

.ico-phone      { --ico: var(--ico-phone); }
/* Hang up is the same handset put down — one shape, two states, and the
   rotation is the whole difference the eye needs. */
.ico-phone-down { --ico: var(--ico-phone); transform: rotate(133deg); }
.ico-caret      { --ico: var(--ico-caret); }
.ico-close      { --ico: var(--ico-close); }
.ico-enter      { --ico: var(--ico-enter); }
.ico-star       { --ico: var(--ico-star); }
.ico-theme      { --ico: var(--ico-theme); }
.ico-prev       { --ico: var(--ico-prev); }
.ico-next       { --ico: var(--ico-next); }


/* ===========================================================================
   01 BUTTONS
   Raised = actionable. Hover lifts the dark shadow +.03 alpha. Press swaps
   to sunk over 90ms; release rebounds over 140ms. No scale, no translate.
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: var(--control-h);
  padding: 0 16px;
  border-radius: var(--r-control);
  background: var(--surface-raised);
  color: var(--ink-1);
  font: 600 12px/1 var(--font-ui);
  white-space: nowrap;
  user-select: none;
  -webkit-user-select: none;
  box-shadow: var(--shadow-raise-2);
  transition: box-shadow var(--t-state) var(--ease),
              background-color var(--t-state) var(--ease),
              color var(--t-state) var(--ease);
}
.btn:hover { box-shadow: var(--shadow-raise-2-hover); }
.btn:active,
.btn.is-pressed {
  background: var(--surface-sunk);
  color: var(--ink-2);
  box-shadow: var(--shadow-sink-1);
  transition-duration: var(--t-press);
}
.btn:focus-visible { box-shadow: var(--shadow-raise-2), var(--focus-ring); }

/* Accent fills — a colored surface can't borrow the bone sunk tone, so it
   presses into a deepened fill instead. */
.btn.primary, .btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-accent);
}
.btn.primary:hover, .btn-primary:hover { box-shadow: var(--shadow-accent-hover); }
.btn.primary:active, .btn-primary:active,
.btn.primary.is-pressed, .btn-primary.is-pressed {
  background: color-mix(in srgb, var(--accent) 84%, rgb(var(--sh-dark)));
  color: var(--on-accent);
  box-shadow: inset 2px 2px 6px rgb(var(--sh-dark) / .30);
}
.btn.primary:focus-visible, .btn-primary:focus-visible {
  box-shadow: var(--shadow-accent), inset 0 0 0 1.5px var(--on-accent);
}

.btn.danger, .btn-danger {
  background: var(--failed);
  color: var(--on-accent);
  box-shadow: var(--shadow-accent);
}
.btn.danger:hover, .btn-danger:hover { box-shadow: var(--shadow-accent-hover); }
.btn.danger:active, .btn-danger:active,
.btn.danger.is-pressed, .btn-danger.is-pressed {
  background: color-mix(in srgb, var(--failed) 84%, rgb(var(--sh-dark)));
  box-shadow: inset 2px 2px 6px rgb(var(--sh-dark) / .30);
}
.btn.danger:focus-visible, .btn-danger:focus-visible {
  box-shadow: var(--shadow-accent), inset 0 0 0 1.5px var(--on-accent);
}

/* --connected-ink, not --connected: "Resume" is real text and --on-accent on
   the raw fill is 3.86:1 in light. 5.31:1 after; dark resolves the ink variant
   back to the raw token, so dark's 6.49:1 is unchanged. */
.btn.success, .btn-success {
  background: var(--connected-ink);
  color: var(--on-accent);
  box-shadow: var(--shadow-accent);
}
.btn.success:hover, .btn-success:hover { box-shadow: var(--shadow-accent-hover); }
.btn.success:active, .btn-success:active,
.btn.success.is-pressed, .btn-success.is-pressed {
  background: color-mix(in srgb, var(--connected-ink) 84%, rgb(var(--sh-dark)));
  box-shadow: inset 2px 2px 6px rgb(var(--sh-dark) / .30);
}

/* Ghost — no surface until hovered. For tertiary actions only. */
.btn.ghost, .btn-ghost {
  background: transparent;
  color: var(--ink-2);
  box-shadow: none;
}
.btn.ghost:hover, .btn-ghost:hover {
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  color: var(--ink-1);
}
.btn.ghost:active, .btn-ghost:active,
.btn.ghost.is-pressed, .btn-ghost.is-pressed {
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
}
.btn.ghost:focus-visible, .btn-ghost:focus-visible { box-shadow: var(--focus-ring); }

/* Disabled — hairline outline, no shadow, no motion. */
.btn:disabled, .btn.is-disabled {
  background: var(--surface-base);
  color: var(--ink-4);
  box-shadow: inset 0 0 0 1px var(--hairline-soft);
  pointer-events: none;
}
/* …but a disabled control still has to be able to say WHY it is disabled, and
   `pointer-events: none` kills the :hover that [data-tip] fires on. A native
   disabled <button> dispatches no click, mouseup or mousedown of its own, so
   the lock buys nothing here except silencing the explanation — and on a
   glyph-only icon button the tip IS the label. Restored only for the controls
   that carry one, and only for the native :disabled state: `.is-disabled` is a
   class, so a click there WOULD fire and the lock has to stay. */
.btn:disabled[data-tip] { pointer-events: auto; cursor: default; }

/* Sizes. Default 28px. lg = the rep dialer's main Dial / Hang up. */
.btn-lg { height: 36px; padding: 0 20px; font-size: 13px; border-radius: 10px; }
.btn-sm { height: var(--chip-h); padding: 0 10px; font-size: 11px; border-radius: var(--r-chip); }
.btn-block { width: 100%; }

/* Icon button — square. */
.btn-icon { width: var(--control-h); padding: 0; flex: none; }
.btn-icon.btn-lg { width: 36px; }

/* Loading — spinner replaces the label; the button is inert while working. */
.btn.is-loading { pointer-events: none; color: transparent; position: relative; }
.btn.is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 14px;
  height: 14px;
  border-radius: var(--r-full);
  border: 2px solid color-mix(in srgb, currentColor 0%, var(--ink-3) 40%);
  border-top-color: var(--ink-2);
  animation: ds-spin 700ms linear infinite;
}
.btn.primary.is-loading::after, .btn.danger.is-loading::after, .btn.success.is-loading::after {
  border-color: rgba(255,255,255,.35);
  border-top-color: var(--on-accent);
}

/* Keyboard hint inside a button (e.g. "Save & next  ↵") */
.btn .btn-kbd {
  font: 500 9px/1 var(--font-num);
  opacity: .7;
  letter-spacing: .06em;
}


/* ===========================================================================
   02 FIELDS & INPUTS — sunk = input. Focus adds the inset accent ring
   INSIDE the sunk shadow. Never an outer glow.
   ========================================================================= */
.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.field-label {
  font: 600 11px/1.2 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-2);
}
/* A footnote is a footnote wherever it sits. Scoping this to `.field .hint`
   meant every .hint placed directly in a .panel — 14 of them across the owner
   pages — inherited panel body type and painted at 13px full-strength ink-1,
   i.e. louder than the table it was explaining. Unscoped, so the class means
   one thing everywhere. */
.hint { font: 500 11px/1.5 var(--font-ui); color: var(--ink-3); }

.input, .select, .textarea {
  height: var(--control-h);
  padding: 0 12px;
  border: 0;
  border-radius: var(--r-control);
  background: var(--surface-sunk);
  color: var(--ink-1);
  font: 500 12px/1 var(--font-ui);
  box-shadow: var(--shadow-sink-1);
  transition: box-shadow var(--t-state) var(--ease),
              background-color var(--t-state) var(--ease);
  width: 100%;
  min-width: 0;
  appearance: none;
  -webkit-appearance: none;
}
.input::placeholder, .textarea::placeholder { color: var(--ink-3); }
.input:focus-visible, .select:focus-visible, .textarea:focus-visible {
  box-shadow: var(--shadow-sink-1), var(--focus-ring);
}
.input:disabled, .select:disabled, .textarea:disabled {
  background: var(--surface-base);
  color: var(--ink-4);
  box-shadow: inset 0 0 0 1px var(--hairline-soft);
}
.input[readonly] { color: var(--ink-2); }
.input.mono, .input-mono { font-family: var(--font-num); letter-spacing: .01em; }

/* Validation — state carried by the ring color, message by .field-error. */
.input.is-invalid, .select.is-invalid, .textarea.is-invalid {
  box-shadow: var(--shadow-sink-1), inset 0 0 0 1.5px var(--failed);
}
.field-error {
  font: 500 11px/1.4 var(--font-ui);
  color: var(--failed);
}

.textarea {
  height: auto;
  min-height: 72px;
  padding: 9px 12px;
  line-height: 1.5;
  resize: vertical;
}

/* Native select keeps a drawn chevron. */
.select {
  padding-right: 28px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-3) 50%),
    linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
  background-position: calc(100% - 15px) 12px, calc(100% - 11px) 12px;
  background-size: 4px 4px;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* Input + button glued together (area-code search, generated links). */
.input-group { display: flex; gap: var(--pad); align-items: center; }
.input-group .input { flex: 1; }

/* Search field with a leading glyph. */
.input-search { padding-left: 30px; }
.field-search { position: relative; }
.field-search::before {
  content: "";
  position: absolute;
  left: 10px;
  top: 50%;
  width: 11px;
  height: 11px;
  margin-top: -7px;
  border: 1.5px solid var(--ink-3);
  border-radius: var(--r-full);
  pointer-events: none;
}
.field-search::after {
  content: "";
  position: absolute;
  left: 20px;
  top: 50%;
  width: 5px;
  height: 1.5px;
  margin-top: 3px;
  background: var(--ink-3);
  transform: rotate(45deg);
  pointer-events: none;
}


/* ===========================================================================
   03 TOGGLE, CHECKBOX, RADIO — sunk track, raised knob. The knob is one of
   only two objects allowed the spring ease.
   ========================================================================= */
.toggle {
  position: relative;
  display: inline-flex;
  width: 46px;
  height: 26px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-1);
  cursor: pointer;
  transition: background-color var(--t-state) var(--ease);
}
.toggle input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}
.toggle .knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: var(--r-full);
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  transition: transform var(--t-state) var(--ease-spring),
              background-color var(--t-state) var(--ease);
  pointer-events: none;
}
.toggle input:checked ~ .knob {
  transform: translateX(20px);
  background: var(--accent);
  box-shadow: var(--shadow-knob);
}
.toggle input:checked ~ .knob.knob-live { background: var(--connected); }
.toggle input:focus-visible ~ .knob { box-shadow: var(--shadow-knob), 0 0 0 1.5px var(--accent); }
.toggle input:disabled ~ .knob { background: var(--surface-base); box-shadow: inset 0 0 0 1px var(--hairline); }
.toggle:has(input:disabled) { opacity: .6; cursor: default; }

/* Toggle + its mono caps label ("AUTO-DIAL"). The label recolors with state. */
.toggle-row { display: inline-flex; align-items: center; gap: 10px; }
.toggle-row .toggle-label {
  font: 500 11px/1 var(--font-num);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  transition: color var(--t-state) var(--ease);
}
.toggle-row:has(input:checked) .toggle-label { color: var(--accent); }
.toggle-row:has(input:checked) .toggle-label.label-live { color: var(--connected); }

/* Checkbox — an 8px accent square pressed into a sunk well. */
.check { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.check input { position: absolute; opacity: 0; width: 16px; height: 16px; margin: 0; cursor: pointer; }
.check .box {
  width: 16px;
  height: 16px;
  flex: none;
  border-radius: 5px;
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  display: grid;
  place-items: center;
  transition: box-shadow var(--t-state) var(--ease);
  pointer-events: none;
}
.check .box::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 2.5px;
  background: var(--accent);
  transform: scale(0);
  transition: transform var(--t-state) var(--ease-spring);
}
.check input:checked ~ .box::after { transform: scale(1); }
.check input:focus-visible ~ .box { box-shadow: var(--shadow-sink-soft), var(--focus-ring); }
.check .check-label { font: 500 12px/1.3 var(--font-ui); color: var(--ink-1); }

.radio .box, .radio .box::after { border-radius: var(--r-full); }


/* ===========================================================================
   04 SEGMENTED CONTROL & TABS
   Segmented: sunk track, raised thumb that SLIDES (JS adds .seg-thumb and
   .has-thumb; without JS the active item is raised in place).
   Underline tabs: the 2px ink bar slides & stretches (JS adds .tabs-ink).
   ========================================================================= */
.seg {
  position: relative;
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  border-radius: 10px;
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
}
.seg-item {
  position: relative;
  z-index: 1;
  height: 24px;
  padding: 0 14px;
  border-radius: 7px;
  display: inline-flex;
  align-items: center;
  font: 500 12px/1 var(--font-ui);
  color: var(--ink-2);
  white-space: nowrap;
  transition: color var(--t-state) var(--ease);
}
.seg-item:hover { color: var(--ink-1); }
.seg-item.is-active { color: var(--ink-1); font-weight: 700; }
.seg-item:focus-visible { box-shadow: var(--focus-ring); border-radius: 7px; }
/* No-JS fallback: active item carries the raise itself. */
.seg:not(.has-thumb) .seg-item.is-active {
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
}
/* JS thumb: one raised element sliding under the labels. */
.seg-thumb {
  position: absolute;
  z-index: 0;
  top: 3px;
  left: 0;
  height: 24px;
  border-radius: 7px;
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  transition: transform var(--t-panel) var(--ease-spring),
              width var(--t-panel) var(--ease-spring);
  pointer-events: none;
}
/* THE HOVER GHOST — a soft highlight that glides under whichever label the
   cursor is over, before commit. Flat wash, no elevation: it signals
   "target", not "state". JS builds and positions it; it fades where the
   active thumb already sits.                                               */
.seg-ghost, .tabs-ghost {
  position: absolute;
  z-index: 0;
  border-radius: 7px;
  background: color-mix(in srgb, var(--ink-3) 14%, transparent);
  opacity: 0;
  transition: transform var(--t-state) var(--ease),
              width var(--t-state) var(--ease),
              opacity var(--t-press) var(--ease);
  pointer-events: none;
}
.seg-ghost { top: 3px; height: 24px; }
.tabs-ghost { top: 4px; bottom: 6px; border-radius: 6px; }
.seg.is-ghosting .seg-ghost, .tabs.is-ghosting .tabs-ghost { opacity: 1; }

/* Underline tabs. */
.tabs {
  position: relative;
  display: flex;
  gap: 18px;
  border-bottom: 1px solid var(--hairline);
}
.tabs-item {
  position: relative;
  padding: 8px 2px 9px;
  font: 500 12px/1 var(--font-ui);
  color: var(--ink-3);
  white-space: nowrap;
  transition: color var(--t-state) var(--ease);
}
.tabs-item:hover { color: var(--ink-2); }
.tabs-item.is-active { color: var(--ink-1); font-weight: 700; }
.tabs-item:focus-visible { box-shadow: var(--focus-ring); border-radius: 4px; }
.tabs:not(.has-ink) .tabs-item.is-active::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}
.tabs-ink {
  position: absolute;
  bottom: -1px;
  left: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  transition: transform var(--t-state) var(--ease-spring),
              width var(--t-state) var(--ease-spring);
  pointer-events: none;
}

/* Tab panes: outgoing fades 90ms, incoming rises 4px over 140ms. */
.tab-pane { display: none; }
.tab-pane.is-active { display: block; animation: ds-pane-in var(--t-state) var(--ease) both; }


/* ===========================================================================
   05 DROPDOWN MENUS — raised panel arriving from its trigger. Items may
   stagger 10ms apiece, capped at 60ms. Close is one tier faster.
   ========================================================================= */
.menu-wrap { position: relative; display: inline-block; }
.menu {
  position: absolute;
  z-index: var(--z-dropdown);
  min-width: 180px;
  padding: 5px;
  border-radius: var(--r-panel);
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-2), 0 0 0 1px var(--hairline-soft);
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
  transition: opacity var(--t-panel) var(--ease),
              transform var(--t-panel) var(--ease);
}
.menu.drop-up { transform: translateY(-6px); }
.menu.is-open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.menu.is-closing {
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  transition: opacity 120ms var(--ease-exit), transform 120ms var(--ease-exit);
}
.menu.drop-up.is-closing { transform: translateY(-4px); }

.menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  height: 26px;
  padding: 0 9px;
  border-radius: 7px;
  font: 500 12px/1 var(--font-ui);
  color: var(--ink-1);
  text-align: left;
  white-space: nowrap;
  transition: background-color var(--t-press) var(--ease);
}
.menu-item:hover, .menu-item.is-focused { background: var(--surface-sunk); }
.menu-item:active { box-shadow: var(--shadow-sink-soft); background: var(--surface-sunk); }
/* Raw --failed is 4.22:1 on the dark raised ground a menu sits on. --failed-ink
   (section 00) is 4.56:1 dark / 5.44:1 light. */
.menu-item.danger { color: var(--failed-ink); }
.menu-item:disabled { color: var(--ink-4); pointer-events: none; }
/* Same reasoning as .chip-key: a .menu sits on --surface-raised, where dark's
   --ink-3 is 4.22:1. 5.57:1 dark / 6.91:1 light after. */
.menu-item .menu-kbd {
  margin-left: auto;
  font: 500 10px/1 var(--font-num);
  color: var(--ink-2);
  letter-spacing: .06em;
}
.menu-sep { height: 1px; margin: 5px 4px; background: var(--hairline-soft); }
.menu-label {
  padding: 6px 9px 4px;
  font: 500 9px/1 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-2);
}
/* Item stagger on open (JS sets --i per item; capped in JS at 6). */
.menu.is-open .menu-item { animation: ds-item-in var(--t-state) var(--ease) both; animation-delay: calc(var(--i, 0) * 10ms); }

/* A select-shaped trigger for custom dropdowns. */
.menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 26px;
  padding: 0 12px;
  border-radius: var(--r-control);
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  font: 600 11px/1 var(--font-ui);
  color: var(--ink-1);
  cursor: pointer;
  transition: box-shadow var(--t-state) var(--ease);
}
.menu-trigger:focus-visible { box-shadow: var(--shadow-sink-soft), var(--focus-ring); }
/* The ▾ stays in the markup (it is on 20-odd pages and is not this file's to
   rewrite) but never paints: font-size:0 kills the glyph and the mask draws
   the caret instead, so the shape is identical whether or not the machine
   owns a symbol font. Same treatment for .chev and .modal-close below. */
.menu-trigger .caret {
  display: inline-block;
  flex: none;
  width: 9px;
  height: 9px;
  font-size: 0;
  color: var(--ink-3);
  background: currentColor;
  -webkit-mask: var(--ico-caret) center / contain no-repeat;
  mask: var(--ico-caret) center / contain no-repeat;
  transition: transform var(--t-state) var(--ease);
}
.menu-trigger[aria-expanded="true"] .caret { transform: rotate(180deg); }


/* ===========================================================================
   06 MODALS — the one elevation exception. Scrim leads, panel arrives 40ms
   later with an 8px rise and a 98.5% → 100% settle. Exit accelerates out.
   ========================================================================= */
.scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-overlay) var(--ease);
}
.scrim.is-open { opacity: 1; pointer-events: auto; }
.scrim.is-closing { opacity: 0; pointer-events: none; transition: opacity var(--t-panel) var(--ease-exit); }

.modal {
  width: 100%;
  max-width: 460px;
  max-height: calc(100vh - 48px);
  display: flex;
  flex-direction: column;
  border-radius: var(--r-shell);
  background: var(--surface-base);
  box-shadow: var(--shadow-overlay);
  opacity: 0;
  transform: translateY(8px) scale(.985);
  transition: opacity var(--t-overlay) var(--ease) 40ms,
              transform var(--t-overlay) var(--ease) 40ms;
}
.scrim.is-open .modal { opacity: 1; transform: translateY(0) scale(1); }
.scrim.is-closing .modal {
  opacity: 0;
  transform: translateY(6px) scale(.99);
  transition: opacity 160ms var(--ease-exit), transform 160ms var(--ease-exit);
}
.modal-sm { max-width: 380px; }
.modal-lg { max-width: 640px; }

.modal-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px 12px;
  border-bottom: 1px solid var(--hairline-soft);
  flex: none;
}
.modal-title { font: 700 15px/1.2 var(--font-ui); letter-spacing: -.01em; }
.modal-close { margin-left: auto; font-size: 0; }
.modal-close::before {
  content: "";
  width: 11px;
  height: 11px;
  background: currentColor;
  -webkit-mask: var(--ico-close) center / contain no-repeat;
  mask: var(--ico-close) center / contain no-repeat;
}
.modal-body {
  padding: 14px 16px;
  overflow-y: auto;
  font: 500 13px/1.6 var(--font-ui);
  color: var(--ink-2);
}
.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--pad);
  padding: 12px 16px 14px;
  border-top: 1px solid var(--hairline-soft);
  flex: none;
}
/* Danger modals: urgency is carried by the accent bar, not bigger motion. */
.modal.danger .modal-header { box-shadow: inset 3px 0 0 var(--failed); }


/* ===========================================================================
   07 TOASTS — bottom-right stack, max 4. Enter slides 12px from the right;
   a 3px bar drains linear over the toast's life; hover pauses it.
   ========================================================================= */
.toast-stack {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;
}
.toast {
  position: relative;
  width: 320px;
  max-width: calc(100vw - 32px);
  padding: 10px 12px 12px 14px;
  border-radius: var(--r-panel);
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-2), 0 0 0 1px var(--hairline-soft);
  pointer-events: auto;
  overflow: hidden;
  opacity: 0;
  transform: translateX(12px);
  transition: opacity var(--t-panel) var(--ease),
              transform var(--t-panel) var(--ease);
}
.toast.is-open { opacity: 1; transform: translateX(0); }
.toast.is-closing {
  opacity: 0;
  transform: translateX(12px);
  transition: opacity 160ms var(--ease-exit), transform 160ms var(--ease-exit);
}
/* 3px state bar on the left. Color = state only. */
.toast::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--ink-3);
}
.toast.accent::before    { background: var(--accent); }
.toast.ok::before        { background: var(--connected); }
.toast.warn::before      { background: var(--ringing); }
.toast.err::before       { background: var(--failed); }

.toast-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font: 700 12px/1.3 var(--font-ui);
  color: var(--ink-1);
  padding-right: 20px;
}
.toast-detail { margin-top: 3px; font: 500 12px/1.5 var(--font-ui); color: var(--ink-2); }
.toast-detail .mono { font-family: var(--font-num); font-size: 11px; }
.toast-x {
  position: absolute;
  top: 8px; right: 8px;
  width: 18px; height: 18px;
  border-radius: 6px;
  display: grid;
  place-items: center;
  color: var(--ink-3);
  font: 600 11px/1 var(--font-ui);
  transition: background-color var(--t-press) var(--ease), color var(--t-press) var(--ease);
}
.toast-x:hover { background: var(--surface-sunk); color: var(--ink-1); }
.toast-action {
  margin-top: 8px;
  display: inline-flex;
}
/* THE DECK — 3+ toasts collapse into a pile: older toasts tuck behind the
   newest with a peeking edge, scaled and dimmed. Hovering the stack fans
   the pile open; a dismiss re-settles it. JS (reflowDeck) sets the
   per-toast overlap margin since heights vary.                             */
.toast-stack .toast {
  transition: opacity var(--t-panel) var(--ease),
              transform var(--t-panel) var(--ease),
              margin-bottom var(--t-panel) var(--ease);
}
.toast-stack.is-deck:not(:hover) .toast.is-open:not(:last-child) {
  transform: scale(.95);
}
.toast-stack.is-deck:not(:hover) .toast.is-open:nth-last-child(n+3) {
  transform: scale(.90);
}
/* Buried cards stay OPAQUE (nothing bleeds through) — only their ink dims. */
.toast-stack.is-deck:not(:hover) .toast:not(:last-child) .toast-title,
.toast-stack.is-deck:not(:hover) .toast:not(:last-child) .toast-detail,
.toast-stack.is-deck:not(:hover) .toast:not(:last-child) .toast-action,
.toast-stack.is-deck:not(:hover) .toast:not(:last-child) .toast-x,
.toast-stack.is-deck:not(:hover) .toast:not(:last-child) .toast-life {
  opacity: .45;
  transition: opacity var(--t-panel) var(--ease);
}
/* While decked, buried toasts are display only. */
.toast-stack.is-deck:not(:hover) .toast:not(:last-child) .toast-x,
.toast-stack.is-deck:not(:hover) .toast:not(:last-child) .toast-action { pointer-events: none; }

/* Drain bar. JS sets transition-duration to the ttl and drops scaleX to 0. */
.toast-life {
  position: absolute;
  left: 3px; right: 0; bottom: 0;
  height: 3px;
  background: color-mix(in srgb, var(--ink-3) 28%, transparent);
  transform-origin: left;
  transform: scaleX(1);
  transition-property: transform;
  transition-timing-function: linear;
}


/* ===========================================================================
   08 BANNERS & MESSAGES — a block is a state, not an event. Banners collapse
   open in place with a wash + 3px bar. `.msg.ok` / `.msg.err` legacy aliases.
   ========================================================================= */
.banner {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--ringing-wash);
  box-shadow: inset 3px 0 0 var(--ringing);
  font: 500 12px/1.5 var(--font-ui);
  color: var(--ink-1);
}
.banner .banner-label {
  font: 500 10px/1.6 var(--font-num);
  letter-spacing: .08em;
  text-transform: uppercase;
  /* A banner label is 10px uppercase TEXT sitting on the banner's own wash, so
     it takes the ink variants for the same reason .badge.warn/.ok/.err do — the
     raw accents measured 2.63:1 (warn), 3.22:1 (ok) and 4.03:1 (err) on those
     composited grounds in light. --accent has no ink variant and does not need
     one. See section 00. */
  color: var(--ringing-ink);
  flex: none;
}
.banner.err, .msg.err { background: var(--failed-wash);    box-shadow: inset 3px 0 0 var(--failed); }
.banner.err .banner-label { color: var(--failed-ink); }
.banner.ok, .msg.ok   { background: var(--connected-wash); box-shadow: inset 3px 0 0 var(--connected); }
.banner.ok .banner-label { color: var(--connected-ink); }
.banner.info          { background: var(--accent-wash);    box-shadow: inset 3px 0 0 var(--accent); }
.banner.info .banner-label { color: var(--accent-ink); }
.msg { /* legacy shape */
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  font: 500 12px/1.5 var(--font-ui);
  color: var(--ink-1);
}
/* Banner entering = collapse-open + rise. JS adds .is-entering for one shot. */
.banner.is-entering, .msg.is-entering { animation: ds-banner-in var(--t-panel) var(--ease) both; }


/* ===========================================================================
   09 TABLES & QUEUE ROWS — flat = data. Hairlines, not cards. 30px rows,
   24px mono header. Selection = sunk + 6px radius (mandatory on dark).
   ========================================================================= */
.table-scroll { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; }
.table th {
  height: 24px;
  padding: 0 10px;
  text-align: left;
  font: 500 9px/1 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
}
.table td {
  height: var(--row-h);
  padding: 0 10px;
  font: 500 12px/1.3 var(--font-ui);
  color: var(--ink-1);
  border-bottom: 1px solid var(--hairline-soft);
  white-space: nowrap;
}
.table td.n, .table th.n { text-align: right; font-family: var(--font-num); font-variant-numeric: tabular-nums; }
.table td.mono { font-family: var(--font-num); font-size: 11px; color: var(--ink-2); }
/* Same scoped-selector defect as `.hint` and `.panel-title`: `.dim` existed
   only as `.table td.dim`, so a `.dim` on a span INSIDE a cell matched nothing
   and inherited `.table td`'s full-strength --ink-1. Unscoped so the class
   means one thing anywhere; the cell-level selector has to stay because
   `.table td` (0,1,1) outranks a bare `.dim` (0,1,0) on the cell itself. */
.dim { color: var(--ink-3); }
.table td.dim { color: var(--ink-3); }
.table tbody tr {
  transition: background-color var(--t-press) var(--ease);
}
.table.is-hoverable tbody tr:hover { background: color-mix(in srgb, var(--surface-sunk) 55%, transparent); }
.table tbody tr.is-selected {
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  border-radius: 6px;
}
.table tbody tr.is-selected td:first-child { border-radius: 6px 0 0 6px; }
.table tbody tr.is-selected td:last-child  { border-radius: 0 6px 6px 0; }

/* Row lifecycle in live lists. */
.table tr.row-enter, .queue-row.row-enter { animation: ds-row-in var(--t-panel) var(--ease) both; }
.table tr.row-leave, .queue-row.row-leave { animation: ds-row-out var(--t-state) var(--ease-exit) both; }

/* Queue rows (grid version for the rep dialer's list pane). */
.queue-head, .queue-row {
  display: grid;
  grid-template-columns: 26px minmax(0,1fr) 110px 40px 30px;
  align-items: center;
  padding: 0 12px;
}
.queue-head {
  height: 24px;
  border-bottom: 1px solid var(--hairline);
  font: 500 9px/1 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.queue-row {
  height: var(--row-h);
  border-bottom: 1px solid var(--hairline-soft);
  font: 500 12px/1.3 var(--font-ui);
  transition: background-color var(--t-press) var(--ease),
              box-shadow var(--t-state) var(--ease);
  cursor: default;
}
.queue-row:hover { background: color-mix(in srgb, var(--surface-sunk) 55%, transparent); }
.queue-row.is-active {
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  border-radius: 6px;
  border-bottom-color: transparent;
}
/* --ink-3, not --ink-4. tokens.css reserves ink-4 for "disabled /
   not-attempted only"; the dial index is neither — it is the number the rep
   reads to say where they are in the list. Measured 3.16:1 on base and
   3.47:1 on the selected row in dark; 4.64:1 and 5.10:1 after. */
.queue-row .q-n    { font: 500 10px/1 var(--font-num); color: var(--ink-3); }
.queue-row .q-lead { display: flex; align-items: center; gap: 7px; min-width: 0; }
.queue-row .q-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.queue-row.is-active .q-name { font-weight: 700; }
.queue-row .q-co   { color: var(--ink-3); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.queue-row .q-phone{ font: 500 11px/1 var(--font-num); color: var(--ink-2); }
.queue-row .q-tz   { font: 500 10px/1 var(--font-num); color: var(--ink-3); }
.queue-row .q-att  { font: 500 10px/1 var(--font-num); color: var(--ink-3); text-align: right; }


/* ===========================================================================
   10 STAT TILES — flat, hairline-separated, mono values. Values count up
   on first paint only (JS), ≤400ms.
   ========================================================================= */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}
.stat {
  padding: 14px 16px;
  border-right: 1px solid var(--hairline-soft);
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}
.stat:last-child { border-right: 0; }
.stat .k {
  font: 500 10px/1 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.stat .v {
  font: 600 22px/1.1 var(--font-num);
  letter-spacing: -.01em;
  color: var(--ink-1);
  font-variant-numeric: tabular-nums;
}
.stat .v.text { font-size: 15px; }
.stat .v small { font-size: 12px; color: var(--ink-4); font-weight: 500; }
.stat .delta { font: 500 10px/1 var(--font-num); letter-spacing: .04em; }
/* NOT on the sweep's list, but the same raw-accent-as-text pairing as the
   badges and banner labels, found by re-running the measurement across every
   page after those fixes landed: a delta is a 10px mono TEXT reading on the
   owner and rep dashboards, and raw --connected was 3.69:1 on base / 3.86:1
   raised / 3.46:1 sunk in light. The ink variants are 5.07 / 5.31 / 4.76:1,
   and take --failed from 4.43:1 to 4.88:1 on light sunk and 4.22:1 to 4.56:1
   on dark raised. Dark's --connected-ink is the raw token, so dark's up-delta
   (5.25-6.34:1) is unchanged. */
.stat .delta.up { color: var(--connected-ink); }
.stat .delta.down { color: var(--failed-ink); }


/* ===========================================================================
   11 BADGES, DOTS, PILLS — accents appear as 6px dots, 3px bars, or text.
   Never a filled surface larger than a button. Legacy: .pill.ok/.warn/.bad.
   ========================================================================= */
.dot {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--ink-4);
  transition: background-color var(--t-state) var(--ease);
}
.dot.ok, .dot.connected, .dot.available { background: var(--connected); }
.dot.warn, .dot.ringing, .dot.away      { background: var(--ringing); }
.dot.bad, .dot.failed, .dot.offline     { background: var(--failed); }
.dot.accent, .dot.on-call               { background: var(--accent); }
/* THE one permitted loop: pulses only while a call is in flight. */
.dot.is-live { animation: ds-live-pulse 1.8s var(--ease) infinite; }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: var(--badge-h);
  padding: 0 8px;
  border-radius: var(--r-badge);
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  font: 500 10px/1 var(--font-num);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
  transition: background-color var(--t-state) var(--ease), color var(--t-state) var(--ease);
}
.badge.raised { background: var(--surface-raised); box-shadow: var(--shadow-raise-1); }
/* Filled badges: reserved for hard states (DNC, LIVE, BLOCKED). */
.badge.fill-err { background: var(--failed);    color: var(--on-accent); box-shadow: none; }
/* fill-ok takes the ink variant for the same reason .btn.success does: the raw
   green under --on-accent is 3.86:1 in light. 5.31:1 after, dark unchanged. */
.badge.fill-ok  { background: var(--connected-ink); color: var(--on-accent); box-shadow: none; }
.badge.fill-accent { background: var(--accent); color: var(--on-accent); box-shadow: none; }
/* Tinted text badges for soft states. 10px uppercase on the badge's own sunk
   ground, so these are text and take the ink variants (see section 00), not
   the raw accents: warn was 2.77:1 and ok 3.46:1 in light. */
.badge.ok   { color: var(--connected-ink); }
.badge.warn { color: var(--ringing-ink); }
.badge.err  { color: var(--failed-ink); }
.badge.accent { color: var(--accent); }   /* 4.91:1 on sunk — passes raw */

/* Presence pill — dot + label, clickable (cycles available ⇄ away). */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 24px;
  padding: 0 11px;
  border-radius: var(--r-full);
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  font: 600 11px/1 var(--font-ui);
  color: var(--ink-1);
  white-space: nowrap;
  transition: box-shadow var(--t-state) var(--ease);
}
button.pill:hover { box-shadow: var(--shadow-raise-1-hover); }
button.pill:active { background: var(--surface-sunk); box-shadow: var(--shadow-sink-soft); }
button.pill:focus-visible { box-shadow: var(--shadow-raise-1), var(--focus-ring); }
/* Legacy alpha-wash pills from app.html keep working: */
.pill.ok   { background: var(--connected-wash); box-shadow: none; color: var(--connected); }
.pill.warn { background: var(--ringing-wash);   box-shadow: none; color: var(--ringing); }
.pill.bad  { background: var(--failed-wash);    box-shadow: none; color: var(--failed); }

/* One-shot attention wash behind a badge/dot when an async update lands. */
.status-flash { animation: ds-status-flash 300ms var(--ease) both; }

/* Mono caps status label with its dot (call bar, headers). */
.state-mark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: 500 10px/1 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}


/* ===========================================================================
   12 CHIPS — dispositions. 22px raised, keyboard digit in mono. Selected =
   pressed in. `.dnc` is red-text destructive.
   ========================================================================= */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: var(--chip-h);
  padding: 0 10px;
  border-radius: var(--r-chip);
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  font: 600 11px/1 var(--font-ui);
  color: var(--ink-1);
  white-space: nowrap;
  user-select: none;
  transition: box-shadow var(--t-state) var(--ease),
              background-color var(--t-state) var(--ease),
              color var(--t-state) var(--ease);
}
.chip:hover { box-shadow: var(--shadow-raise-1-hover); }
.chip:active, .chip.is-pressed, .chip.is-selected {
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  color: var(--ink-1);
}
.chip.is-selected { font-weight: 700; }
.chip:focus-visible { box-shadow: var(--shadow-raise-1), var(--focus-ring); }
.chip:disabled { background: var(--surface-base); color: var(--ink-4); box-shadow: inset 0 0 0 1px var(--hairline-soft); pointer-events: none; }
.chip:disabled[data-tip] { pointer-events: auto; cursor: default; }   /* see .btn:disabled[data-tip] */
/* --ink-2, not --ink-3. The key hint sits on --surface-raised unselected and
   --surface-sunk once picked, and --ink-3 is the AA floor on --surface-base
   only: measured 4.22:1 on dark's raised chip and 4.24:1 on light's selected
   one. This is the label of the key a rep hits two hundred times a day, so it
   takes the ink that clears AA on every ground it can land on (5.57-6.91:1).
   The size step (9px against the chip's 11px) carries the hierarchy. */
.chip .chip-key {
  font: 500 9px/1 var(--font-num);
  color: var(--ink-2);
  letter-spacing: .04em;
}
.chip.dnc { color: var(--failed-ink); }
.chip.ok  { color: var(--connected-ink); }   /* raw --connected was 3.86:1 on raised */

.chip-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }


/* ===========================================================================
   13 AVATAR
   ========================================================================= */
.avatar {
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  display: grid;
  place-items: center;
  font: 700 13px/1 var(--font-ui);
  color: var(--ink-2);
  text-transform: uppercase;
}
.avatar-sm { width: 24px; height: 24px; font-size: 10px; }
/* Brand mark — the one dark square. */
.avatar-brand {
  border-radius: 7px;
  background: var(--ink-1);
  color: var(--surface-base);
  box-shadow: none;
  font-weight: 800;
}


/* ===========================================================================
   14 SIDEBAR / RAIL — 220px ⇄ 56px. Labels lead the geometry: fade out
   before collapse, fade in after expand. Icons never move.
   ========================================================================= */
.sidebar {
  width: 220px;
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--pad);
  border-right: 1px solid var(--hairline);
  background: var(--surface-base);
  transition: width var(--t-panel) var(--ease);
  overflow: hidden;
}
.sidebar.is-collapsed { width: 56px; }
.sidebar-item {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 32px;
  padding: 0 10px;
  border-radius: var(--r-control);
  font: 600 12px/1 var(--font-ui);
  color: var(--ink-2);
  white-space: nowrap;
  transition: background-color var(--t-press) var(--ease),
              color var(--t-press) var(--ease),
              box-shadow var(--t-state) var(--ease);
}
.sidebar-item:hover { background: color-mix(in srgb, var(--surface-sunk) 55%, transparent); color: var(--ink-1); }
.sidebar-item.is-active {
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  color: var(--ink-1);
  font-weight: 700;
}
.sidebar-item:focus-visible { box-shadow: var(--focus-ring); }
.sidebar-item .icon {
  width: 16px;
  height: 16px;
  flex: none;
  display: grid;
  place-items: center;
  font-size: 13px;
}
.sidebar-item .label {
  opacity: 1;
  transition: opacity var(--t-state) var(--ease) 40ms;
}
.sidebar.is-collapsed .sidebar-item .label {
  opacity: 0;
  transition: opacity var(--t-press) var(--ease); /* labels lead on the way out */
}
.sidebar-item .count {
  margin-left: auto;
  font: 500 10px/1 var(--font-num);
  color: var(--ink-3);
  opacity: 1;
  transition: opacity var(--t-state) var(--ease) 40ms;
}
.sidebar.is-collapsed .sidebar-item .count { opacity: 0; transition: opacity var(--t-press) var(--ease); }
.sidebar-collapse-btn .chev { transition: transform var(--t-state) var(--ease); display: inline-block; }
.sidebar.is-collapsed .sidebar-collapse-btn .chev { transform: rotate(180deg); }


/* ===========================================================================
   15 CALL BAR & CALL STATE
   data-call-state on .callbar drives dot color, pulse, and label color.
   States: idle · dialing · ringing · answered · connected · voicemail ·
           wrap · held · failed · blocked
   The label itself rolls via JS (DialerUI.roll) on change.
   ========================================================================= */
.callbar {
  min-height: var(--chrome-h);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 var(--gutter);
  border-bottom: 1px solid var(--hairline);
  background: var(--surface-base);
  position: relative;
  z-index: var(--z-callbar);
}
.callbar .callbar-state {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.callbar .callbar-dot {
  width: 6px; height: 6px; flex: none;
  border-radius: var(--r-full);
  background: var(--ink-4);
  transition: background-color var(--t-state) var(--ease);
}
.callbar .callbar-label {
  font: 500 10px/1.2 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
  transition: color var(--t-state) var(--ease);
  overflow: hidden;
}
.callbar .callbar-meta {
  font: 500 10px/1.2 var(--font-num);
  letter-spacing: .06em;
  color: var(--ink-3);
  white-space: nowrap;
}
.callbar .callbar-timer {
  font: 600 20px/1 var(--font-num);
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-1);
}

/* State → color map. The DOT is a 6px graphic and wears the raw accent; the
   LABEL is 10px uppercase text and wears the ink variant (section 00). On
   --surface-base in light, raw --connected read 3.69:1 and raw --ringing
   2.95:1 — the two states a rep glances at most were the two least legible
   words on the screen. Ink variants measure 5.07:1 and 5.11:1. Same hue, so
   the dot and its label still read as one state. */
.callbar[data-call-state="idle"]      .callbar-dot { background: var(--ink-4); }
.callbar[data-call-state="idle"]      .callbar-label { color: var(--ink-3); }
.callbar[data-call-state="dialing"]   .callbar-dot { background: var(--accent); }
.callbar[data-call-state="dialing"]   .callbar-label { color: var(--accent); }
.callbar[data-call-state="ringing"]   .callbar-dot { background: var(--ringing); }
.callbar[data-call-state="ringing"]   .callbar-label { color: var(--ringing-ink); }
.callbar[data-call-state="answered"]  .callbar-dot { background: var(--connected); }
.callbar[data-call-state="answered"]  .callbar-label { color: var(--connected-ink); }
.callbar[data-call-state="connected"] .callbar-dot { background: var(--connected); }
.callbar[data-call-state="connected"] .callbar-label { color: var(--connected-ink); }
.callbar[data-call-state="voicemail"] .callbar-dot { background: var(--ringing); }
.callbar[data-call-state="voicemail"] .callbar-label { color: var(--ringing-ink); }
.callbar[data-call-state="wrap"]      .callbar-dot { background: var(--accent); }
.callbar[data-call-state="wrap"]      .callbar-label { color: var(--accent); }
.callbar[data-call-state="held"]      .callbar-dot { background: var(--ringing); }
.callbar[data-call-state="held"]      .callbar-label { color: var(--ringing-ink); }
.callbar[data-call-state="failed"]    .callbar-dot { background: var(--failed); }
.callbar[data-call-state="failed"]    .callbar-label { color: var(--failed-ink); }
.callbar[data-call-state="blocked"]   .callbar-dot { background: var(--failed); }
.callbar[data-call-state="blocked"]   .callbar-label { color: var(--failed-ink); }

/* The ONE loop: in-flight states pulse the dot. */
.callbar[data-call-state="dialing"]   .callbar-dot,
.callbar[data-call-state="ringing"]   .callbar-dot,
.callbar[data-call-state="answered"]  .callbar-dot,
.callbar[data-call-state="connected"] .callbar-dot {
  animation: ds-live-pulse 1.8s var(--ease) infinite;
}

/* THE ODOMETER — per-character cells; only a changed digit rolls (with a
   streak of motion blur), the rest hold perfectly still. Built by
   DialerUI.odometer; used by the call timer and live counters. Digit cells
   are fixed-width so the layout never moves.                               */
.odo { display: inline-flex; align-items: baseline; }
.odo .odo-cell {
  position: relative;
  display: inline-block;
  overflow: hidden;
  white-space: pre;
}
.odo .odo-cell.odo-digit { min-width: 1ch; text-align: center; }
.odo .odo-out {
  position: absolute;
  inset: 0;
  animation: ds-odo-out var(--t-state) var(--ease) both;
}
.odo .odo-in {
  display: inline-block;
  animation: ds-odo-in var(--t-state) var(--ease) both;
}

/* Lead identity block (the only display-scale type in the product). */
.lead-name {
  font: 800 30px/1.1 var(--font-ui);
  letter-spacing: var(--track-display);
  color: var(--ink-1);
}
.lead-name.sm { font-size: 22px; }
.lead-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  font: 500 12px/1.3 var(--font-ui);
  color: var(--ink-2);
  flex-wrap: wrap;
}
/* The one remaining sub-AA pairing on this screen was this separator, at
   1.99:1 light / 3.16:1 dark. It is arguably decoration — the fields either
   side are also separated by space — but "arguably exempt" is not a standard,
   and --ink-3 still lands it below the --ink-2 fields it divides, so the
   hierarchy is unchanged and the measurement is unambiguous. 4.52:1 light,
   4.64:1 dark. */
.lead-meta .sep { color: var(--ink-3); }
.lead-meta .mono { font-family: var(--font-num); }

/* Contact advance stage — JS animates .contact-leave / .contact-enter. */
.contact-stage { position: relative; overflow: hidden; }
.contact-leave { animation: ds-contact-out var(--t-state) var(--ease-exit) both; }
.contact-enter { animation: ds-contact-in var(--t-overlay) var(--ease) 60ms both; }

/* Call-window progress (e.g. % of calling hours elapsed). */
.call-progress {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  overflow: hidden;
}
.call-progress .fill {
  height: 100%;
  border-radius: 3px;
  background: var(--connected);
  width: 0%;
  transition: width var(--t-slow) var(--ease);
}


/* ===========================================================================
   16 TRANSPORT & KEYPAD — the only controls that exceed 28px, because a rep
   hits them without looking. Keys are 28px raised keys, mono digits.

   ONE keypad. There were two: a 138px popover with 34x28 keys hanging off
   the call controls, and a 267px pane collapse whose inline
   `grid-template-columns:repeat(3,1fr)` stretched the same .keypad class to
   85x28 keys — same class, 2.5x the key width, which is why the owner said
   the two "look a little different". The geometry now lives here and only
   here, so a keypad is the same object wherever it is mounted.
   ========================================================================= */
.btn-transport {
  width: 36px;
  height: 36px;
  border-radius: var(--r-full);
  background: var(--surface-raised);
  color: var(--ink-2);
  font: 700 11px/1 var(--font-ui);
  display: grid;
  place-items: center;
  box-shadow: var(--shadow-raise-2);
  transition: box-shadow var(--t-state) var(--ease), background-color var(--t-state) var(--ease);
}
.btn-transport:hover { box-shadow: var(--shadow-raise-2-hover); }
.btn-transport:active, .btn-transport.is-pressed {
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-1);
  transition-duration: var(--t-press);
}
.btn-transport:focus-visible { box-shadow: var(--shadow-raise-2), var(--focus-ring); }
.btn-transport-main {
  width: 44px;
  height: 44px;
  background: var(--connected);
  color: var(--on-accent);
  font-size: 15px;
  box-shadow: var(--shadow-accent);
}
.btn-transport-main:hover { box-shadow: var(--shadow-accent-hover); }
.btn-transport-main:active, .btn-transport-main.is-pressed {
  background: color-mix(in srgb, var(--connected) 84%, rgb(var(--sh-dark)));
  box-shadow: inset 2px 2px 6px rgb(var(--sh-dark) / .30);
}
.btn-transport-main.danger { background: var(--failed); }
.btn-transport-main.danger:active {
  background: color-mix(in srgb, var(--failed) 84%, rgb(var(--sh-dark)));
}

.keypad {
  display: grid;
  grid-template-columns: repeat(3, 52px);
  gap: 6px;
  justify-content: center;
}
.btn-key {
  height: var(--control-h);
  border-radius: var(--r-control);
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  display: grid;
  place-items: center;
  font: 500 12px/1 var(--font-num);
  color: var(--ink-1);
  transition: box-shadow var(--t-press) var(--ease), background-color var(--t-press) var(--ease);
}
.btn-key:hover { box-shadow: var(--shadow-raise-1-hover); }
.btn-key:active, .btn-key.is-pressed {
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  color: var(--ink-2);
}
.btn-key:focus-visible { box-shadow: var(--shadow-raise-1), var(--focus-ring); }
/* --ink-2, not --ink-3: the letter groups sit on --surface-raised, where
   ink-3 measures 4.22:1 in dark. ink-2 is 5.57:1 dark / 6.91:1 light, and at
   7px on a 52px key they are legible for the first time. */
.btn-key sub {
  font-size: 7px;
  letter-spacing: .08em;
  color: var(--ink-2);
  vertical-align: baseline;
  margin-left: 2px;
}
/* DTMF echo line under the pad. */
.keypad-echo {
  font: 500 12px/1 var(--font-num);
  letter-spacing: .14em;
  color: var(--ink-2);
  min-height: 14px;
}


/* ===========================================================================
   17 ACTIVITY FEED — 26px hairline rows, newest on top, entering with a
   4px rise.
   ========================================================================= */
.feed { display: flex; flex-direction: column; }
.feed-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 26px;
  padding: 2px 0;
  border-bottom: 1px solid var(--hairline-soft);
  font: 500 11px/1.4 var(--font-ui);
}
.feed-row .dot { flex: none; }
.feed-row .feed-event { font-weight: 600; color: var(--ink-1); flex: none; }
.feed-row .feed-event.mono { font-family: var(--font-num); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }
.feed-row .feed-detail { color: var(--ink-3); flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.feed-row .feed-time { font: 500 10px/1 var(--font-num); color: var(--ink-3); flex: none; }
.feed-row.is-new { animation: ds-row-in var(--t-panel) var(--ease) both; }


/* ===========================================================================
   18 SKELETONS & SPINNER — owner panel only, never the live session view,
   nothing under 400ms. One low-contrast shimmer sweep.
   ========================================================================= */
.skel {
  position: relative;
  border-radius: var(--r-chip);
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  overflow: hidden;
  color: transparent !important;
  pointer-events: none;
  user-select: none;
}
.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    transparent 30%,
    color-mix(in srgb, var(--surface-raised) 65%, transparent) 50%,
    transparent 70%);
  transform: translateX(-100%);
  animation: ds-shimmer 1.6s linear infinite;
}
.skel-text  { height: 12px; width: 100%; }
.skel-title { height: 18px; width: 40%; }
.skel-row   { height: var(--row-h); border-radius: var(--r-chip); }
.skel-stat  { height: 58px; border-radius: var(--r-chip); }
.skel-stack { display: flex; flex-direction: column; gap: 8px; }
/* Skeleton → content: hide skeleton, then rows cascade (JS caps stagger at 8). */
.skel-done { animation: ds-fade-out var(--t-panel) var(--ease) both; }
.cascade-in { animation: ds-row-in var(--t-panel) var(--ease) both; animation-delay: calc(var(--i, 0) * 20ms); }

.spinner {
  width: 16px;
  height: 16px;
  flex: none;
  border-radius: var(--r-full);
  border: 2px solid color-mix(in srgb, var(--ink-3) 35%, transparent);
  border-top-color: var(--accent);
  animation: ds-spin 700ms linear infinite;
}


/* ===========================================================================
   19 PROGRESS — a quantity, not a movement. Fill moves at --t-slow; use
   .linear when time itself drives it (countdowns, wrap-up windows).
   ========================================================================= */
.progress {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-sunk);
  box-shadow: var(--shadow-sink-soft);
  overflow: hidden;
}
.progress .fill {
  height: 100%;
  border-radius: 3px;
  background: var(--accent);
  width: 0%;
  transition: width var(--t-slow) var(--ease);
}
.progress .fill.ok   { background: var(--connected); }
.progress .fill.warn { background: var(--ringing); }
.progress .fill.err  { background: var(--failed); }
.progress.linear .fill { transition-timing-function: linear; }
.progress.thin { height: 3px; border-radius: 2px; box-shadow: none; }


/* ===========================================================================
   20 COLLAPSE & TOOLTIP
   Collapse: CSS grid 0fr → 1fr; geometry first, ink 40ms later.
   Tooltip: mono, dark chip, 140ms in after a 500ms hover intent delay.
   ========================================================================= */
.collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--t-panel) var(--ease);
}
.collapse > .collapse-inner {
  overflow: hidden;
  min-height: 0;
  opacity: 0;
  transition: opacity var(--t-state) var(--ease);
}
.collapse.is-open { grid-template-rows: 1fr; }
.collapse.is-open > .collapse-inner { opacity: 1; transition-delay: 40ms; }
.collapse-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  font: 600 12px/1 var(--font-ui);
  color: var(--ink-1);
  padding: 8px 0;
}
/* Scoped to .collapse-trigger on purpose: the sidebar's collapse button also
   carries .chev but its glyph is « (U+00AB, Latin-1 — Manrope has it), and a
   down-caret would be the wrong shape there. */
.collapse-trigger .chev {
  display: inline-block;
  flex: none;
  width: 9px;
  height: 9px;
  font-size: 0;
  color: var(--ink-3);
  background: currentColor;
  -webkit-mask: var(--ico-caret) center / contain no-repeat;
  mask: var(--ico-caret) center / contain no-repeat;
  transition: transform var(--t-state) var(--ease);
}
.collapse-trigger[aria-expanded="true"] .chev { transform: rotate(180deg); }
.collapse-trigger:focus-visible { box-shadow: var(--focus-ring); border-radius: 4px; }

[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 7px);
  left: 50%;
  transform: translateX(-50%) translateY(2px);
  padding: 5px 8px;
  border-radius: 6px;
  background: var(--ink-1);
  color: var(--surface-base);
  font: 500 10px/1.3 var(--font-num);
  letter-spacing: .04em;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-state) var(--ease), transform var(--t-state) var(--ease);
  transition-delay: 0ms;
  z-index: var(--z-dropdown);
}
[data-tip]:hover::after, [data-tip]:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  transition-delay: 500ms; /* hover intent — tooltips never flicker in passing */
}


/* ===========================================================================
   21 EMPTY STATES, KBD, PANELS, MISC
   ========================================================================= */
.empty {
  padding: 28px 16px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
}
.empty .empty-label {
  font: 500 11px/1 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}
/* --ink-3, not --ink-4: this is 12px prose explaining an empty state, not a
   disabled control, so --ink-4's "disabled / not-attempted only" contract does
   not cover it. 1.99:1 -> 4.52:1 light, 3.16:1 -> 4.64:1 dark. */
.empty .empty-hint { font: 500 12px/1.5 var(--font-ui); color: var(--ink-3); max-width: 300px; }

.kbd {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 5px;
  background: var(--surface-raised);
  box-shadow: var(--shadow-raise-1);
  font: 500 10px/1 var(--font-num);
  color: var(--ink-2);
}

/* Panels: flat surface + hairline. The owner panel's content blocks. */
.panel {
  border: 1px solid var(--hairline);
  border-radius: var(--r-panel);
  background: var(--surface-base);
  padding: 14px 16px;
}
/* Not `.panel > .panel-title`: the moment a title is wrapped in a `.row-flex`
   to sit beside a button — which four panels already do — the child combinator
   silently drops the rule and the title renders as 13px sentence-case body
   copy, next to a sibling panel whose title is 11px mono caps. Same class, one
   appearance. Instances that need to kill the bottom margin already carry
   `style="margin:0"`, which still wins. */
.panel-title {
  font: 600 11px/1.2 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-2);
  margin: 0 0 12px;
}
/* Legacy `.card` and `.section-label` aliases. */
.card { border: 1px solid var(--hairline); border-radius: var(--r-panel); background: var(--surface-base); padding: 14px 16px; }
.section-label {
  font: 500 10px/1.2 var(--font-num);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Top chrome strip (44px). */
.topbar {
  height: var(--chrome-h);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 var(--gutter);
  border-bottom: 1px solid var(--hairline);
  background: var(--surface-base);
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}
.topbar .top-stats {
  display: flex;
  align-items: center;
  gap: 14px;
  font: 500 11px/1 var(--font-num);
  color: var(--ink-2);
}
.topbar .top-stats .k { color: var(--ink-3); }
/* Same call as .q-n above: "/240" is the campaign's size, which is data the
   rep acts on, not a disabled control. 3.16:1 -> 4.64:1 dark, 4.52:1 light.
   It still reads as the quieter half of the pair — that job is done by the
   ink step and the tabular numeral beside it, not by dropping under AA. */
.topbar .top-stats .dim { color: var(--ink-3); }
