/* Transcript_pro web — shares the cpanel design language (slate + black accent,
   Inter, Bootstrap 5). Thai UI. Extends the cpanel token set. */

:root {
  --bg: #f6f8fb;
  --surface: #ffffff;
  --text: #0a0a0a;
  --text-muted: #64748b;
  --border: #e6ebf1;
  --border-strong: #cbd5e1;
  /* A step darker than --border, for rules that DIVIDE content rather than close
     a box: the cell rules in a metric strip, a summary band's closing rule. On a
     tinted band --border all but disappears and the columns stop reading as
     columns; --border-strong is loud enough to look like a table gridline. */
  --divider: #d3dae4;
  --accent: #000000;
  --ink: #0f172a;          /* primary dark surface: buttons, brand, dark chips */
  --ink-hover: #1e293b;
  --radius-lg: 14px;
  --radius: 10px;
  --radius-sm: 7px;
  --radius-xs: 5px;
  --transition: .18s cubic-bezier(.4, 0, .2, 1);

  /* elevation — soft, layered (global SaaS feel) */
  --shadow-xs: 0 1px 2px rgba(15,23,42,.05);
  --shadow-sm: 0 1px 2px rgba(15,23,42,.04), 0 2px 6px rgba(15,23,42,.05);
  --shadow-md: 0 2px 4px rgba(15,23,42,.04), 0 6px 16px rgba(15,23,42,.08);
  --shadow-lg: 0 10px 30px rgba(15,23,42,.12);
  --shadow-xl: 0 24px 60px rgba(15,23,42,.22);
  /* focus ring */
  --ring: 0 0 0 3px rgba(15,23,42,.12);

  /* semantic */
  --green: #16a34a;
  --green-bg: #dcfce7;
  --amber: #d97706;
  --amber-bg: #fef3c7;
  --red: #dc2626;
  --red-bg: #fee2e2;
  --blue: #2563eb;
  --blue-bg: #dbeafe;
  --slate-bg: #f1f5f9;

  /* Table surfaces — the grid's greys in one place so the whole scale moves
     together (they were hardcoded per rule, which made "a bit darker" a hunt
     through five selectors). Two steps down from the near-white they used to be
     (white rows, #fafbfc header): the grid now sits as its own tinted surface
     instead of bleeding into the card, and a hovered row is unmistakable on a
     bright screen.

     The four fills are ONE ordered scale — row < hover < pin-hover < header —
     and every rule below picks from it. Keep that order when re-tuning: the
     header has to stay the darkest (it is the table's chrome) and the pinned
     cell one step past the row it sits on, or the frozen column stops reading
     as a layer above the scrolling ones. --tbl-border must stay darker than
     all four. */
  --tbl-row-bg:    #f3f5f9;
  --tbl-row-hover: #e9eef5;
  --tbl-pin-hover: #e2e8f1;
  --tbl-head-bg:   #e0e7f0;
  --tbl-border:    #d3dae4;

  /* ── App shell (dark topbar + collapsible sidebar) ──────────────────────
     Shared Call On Cloud chrome — tokens are the SSOT from
     docs/COC_APP_SHELL_GUIDE.md §3. Do not fork these values per service. */
  --chrome:          #0b0d0f;              /* near-black top bar */
  /* Dark command chrome frames a white working surface. Keep the dark regions
     compact; hierarchy comes from control treatment, not oversized black bands.
     At :root, not on `.dt`: the pager and the table-card bars wear it too, and
     when it lived on the grid a pager anywhere else painted no background. */
  --dt-black:        #050505;
  --chrome-ink:      #e9eaec;
  --chrome-ink-dim:  #9aa1a9;              /* 4.9:1 on --chrome */
  --chrome-border:   rgba(255,255,255,.09);
  --chrome-hover:    rgba(255,255,255,.08);

  /* one accent: blue — nav icons, active pill, focus */
  --brand:         #2563eb;
  --brand-on-dark: #4f9bff;
  --nav-active-bg: #dde8fd;
  --nav-hover-bg:  rgba(37,99,235,.07);

  /* layout metrics */
  --topbar-h:      56px;
  --sidebar-w:     248px;
  --sidebar-w-min: 68px;                   /* collapsed icon rail */
  --proj-menu-w:   320px;                  /* project switcher panel (fixed, see .proj-menu) */
  /* Horizontal padding of the content pane. A token because anything that has
     to bleed to the pane's edges (the sticky .dtabs strip) needs the same
     number — see main.app-content. */
  --content-pad:   clamp(1rem, 3vw, 2.5rem);

  /* semantic z-index scale (Bootstrap dropdowns = 1000, above the chrome) */
  --z-backdrop: 190;
  --z-sidebar:  195;
  --z-topbar:   210;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  background: var(--bg);
  color: var(--text);
  /* Latin from Inter, Thai from IBM Plex Sans Thai Looped — a LOOPED (มีหัว) Thai
     face, which reads far easier at UI sizes than the loopless one we had. The
     two share x-height and stroke weight closely enough that a mixed Thai/English
     line (the common case here: English chrome around Thai model output) sits on
     one optical baseline instead of looking like two pasted-together fonts. */
  font-family: 'Inter', 'IBM Plex Sans Thai Looped', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-feature-settings: 'cv05', 'ss01';
}

[hidden] { display: none !important; }

/* Consistent focus ring across native + custom controls (replaces Bootstrap's
   blue glow so the whole app shares the neutral ink accent). */
.form-control:focus, .form-select:focus, .form-check-input:focus {
  border-color: var(--border-strong);
  box-shadow: var(--ring);
}
.form-check-input:checked { background-color: var(--ink); border-color: var(--ink); }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Top page-load progress bar — see chrome.js pageProgress() */
#pageProgress {
  position: fixed; top: 0; left: 0; height: 3px; width: 0%; z-index: 500;
  background: linear-gradient(90deg, #334155, #0f172a);
  box-shadow: 0 0 8px rgba(15,23,42,.4);
  transition: width .3s ease, opacity .3s ease;
}

/* ── App shell — dark topbar + collapsible sidebar ────────────────────────
   Spec: docs/COC_APP_SHELL_GUIDE.md. Built by chrome.js mountChrome():
   #chrome.app-shell > .app-topbar + .app-body(.app-sidebar + main.app-content)
   + .chrome-backdrop. */
.app-shell { min-height: 100vh; }
.app-body { display: flex; align-items: flex-start; }

/* ── Top bar (dark) ── */
.app-topbar {
  position: sticky; top: 0; z-index: var(--z-topbar);
  display: flex; align-items: center; gap: .5rem;
  height: var(--topbar-h); padding: 0 clamp(.75rem, 2vw, 1.25rem);
  background: var(--chrome); color: var(--chrome-ink);
  border-bottom: 1px solid var(--chrome-border);
}
.chrome-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex-shrink: 0; border: 0;
  border-radius: var(--radius-sm); background: transparent;
  color: var(--chrome-ink); font-size: 1.15rem; cursor: pointer;
  text-decoration: none;                 /* .chrome-back is an <a> */
  transition: background-color var(--transition);
}
.chrome-toggle:hover { background: var(--chrome-hover); color: #fff; }
.chrome-toggle:focus-visible { outline: 2px solid var(--chrome-ink); outline-offset: 2px; }

/* The drawer toggle is the only way to the nav under 992px; on desktop the rail
   is always open, so the slot belongs to the back link instead. Re-show this
   (and set DESKTOP_COLLAPSE = true in chrome.js) to bring the icon rail back. */
@media (min-width: 992px) {
  .chrome-drawer-toggle { display: none; }
}

.chrome-brand {
  display: inline-flex; align-items: center; gap: .55rem; flex-shrink: 0;
  text-decoration: none; color: var(--chrome-ink);
  padding: .25rem .35rem; border-radius: var(--radius-sm);
}
.chrome-brand:hover { color: #fff; }
.chrome-brand:focus-visible { outline: 2px solid var(--chrome-ink); outline-offset: 2px; }
/* coc_transcript_pro.svg = the dark-bar product lockup (yellow mark + white
   "TRANSCRIPT PRO"). 34px is the box height the guide (§9) fixes for every COC
   service — do not "correct" it for the artwork's padding: this lockup carries
   the SAME ink-to-viewBox ratio as the portal's logo_coc.svg (~68%, 109/160 vs
   61.5/90), so an equal box paints an equal mark. Sizing the ink to 34px instead
   (a 50px box) made this bar's logo ~1.5x every other service's. */
.chrome-brand { padding-block: 0; }
.chrome-brand .brand-logo { height: 34px; width: auto; display: block; flex-shrink: 0; }

.chrome-divider { width: 1px; height: 24px; background: var(--chrome-border); margin: 0 .35rem; flex-shrink: 0; }
@media (max-width: 575.98px) { .chrome-divider { display: none; } }

/* The project switcher lives at the top of the RAIL, not on the bar — see
   .app-sidebar-head / .proj-switch below. Its panel styles (.proj-menu and
   everything under it) are shared and stay here, next to the other menus. */

/* The panel: one fixed width in every tenant (see the comment on
   renderProjectSwitcher in chrome.js for why it is not content-sized).
   Only the row list scrolls — the search field and "All projects" stay put. */
.proj-menu { width: var(--proj-menu-w); max-width: calc(100vw - 1.5rem); }
.proj-search { position: relative; padding: .15rem .15rem .35rem; }
.proj-search .bi-search {
  position: absolute; left: .7rem; top: 50%; transform: translateY(-60%);
  font-size: .8rem; color: var(--text-muted); pointer-events: none;
}
.proj-search-input {
  width: 100%; padding: .4rem .6rem .4rem 1.9rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: .8438rem; color: var(--text); background: var(--surface);
}
.proj-search-input::placeholder { color: var(--text-muted); }
.proj-search-input:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px var(--nav-active-bg); }

/* Only this region scrolls, so a long list never pushes "All projects" off the
   panel. The rem cap lands mid-row on purpose (rows are ~2.1rem): a half-visible
   row is the affordance that says "keep scrolling" on platforms with overlay
   scrollbars. The vh cap is for short windows — without it a 25-project panel
   runs past the bottom of a laptop-landscape viewport. */
.proj-list {
  max-height: min(15.75rem, 45vh); overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.proj-list .proj-item { width: 100%; }
/* .dropdown-item is display:flex (author style) and would beat the UA's
   [hidden] { display: none } — the search filter needs this to actually hide. */
.proj-list .proj-item[hidden], .proj-empty[hidden] { display: none; }
.proj-list .proj-item-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proj-list .proj-item.is-current { font-weight: 600; background: var(--nav-active-bg); }
.proj-list .proj-item-check { flex-shrink: 0; color: var(--brand); }
.proj-empty { padding: .9rem .65rem; text-align: center; font-size: .8125rem; color: var(--text-muted); }

.chrome-spacer { flex: 1 1 auto; }

/* Organization badge — right end of the bar. Outlined, not filled: it is context
   (which org this session is scoped to), never a control. One org = one tenant
   here, so there is nothing to switch; the tenant itself is internal and unshown. */
.chrome-org {
  display: inline-flex; align-items: center; gap: .4rem;
  flex-shrink: 1; min-width: 0;
  max-width: 46vw;                        /* ellipsize instead of pushing the logo */
  padding: .26rem .65rem;
  border: 1px solid var(--chrome-border); border-radius: 999px;
  color: var(--chrome-ink); font-size: .8125rem; font-weight: 600;
}
.chrome-org .bi { flex-shrink: 0; font-size: .875rem; color: var(--chrome-ink-dim); }
.chrome-org .org-static { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Phones: the product lockup does not shrink, so the badge would ellipsize down
   to a building icon and two letters. It is context, not a control, and the
   account menu at the foot of the rail spells the org out in full. */
@media (max-width: 575.98px) { .chrome-org { display: none; } }

/* Standalone brand lockup for pages without the app shell (login) */
.topbar-brand {
  display: inline-flex; align-items: center; gap: .5rem;
  color: var(--text); font-weight: 800; letter-spacing: -0.02em;
}
.topbar-brand .brand-logo { height: 30px; width: auto; display: block; flex-shrink: 0; }

/* ── Sidebar ── */
.app-sidebar {
  position: sticky; top: var(--topbar-h); z-index: var(--z-sidebar);
  flex-shrink: 0; width: var(--sidebar-w); height: calc(100vh - var(--topbar-h));
  display: flex; flex-direction: column; background: var(--surface);
  border-right: 1px solid var(--border);
  /* overflow: visible on purpose — the footer's dropup is an absolutely
     positioned menu, and any clipping context here (auto/hidden) cuts it off.
     Scrolling belongs to .app-sidebar-nav. position: fixed on the menu is not a
     workaround: under 992px the sidebar is translateX'd, which contains it. */
  overflow: visible;
  transition: width var(--transition);
}
/* Scope header — the "Project" / "Workspace" heading, plus the switcher beneath it
   inside a project. The heading is the same `.nav-section-label` at both levels, so
   the rail opens with the same word in the same place whichever scope you are in;
   only the control under it is conditional. */
.app-sidebar-head {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: .4rem;
  padding: .875rem .625rem .5rem;
}
.app-sidebar-nav {
  flex: 1 1 auto;                 /* takes the free space → the footer is pinned */
  padding: .25rem .625rem 1rem; display: flex; flex-direction: column; gap: .125rem;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}

/* ── Project switcher (rail head) ──
   A bordered field, not a ghost button: it is the only control in the rail that
   changes what every item below it means, so it reads as a distinct object rather
   than a heavier nav row. Name + chevron only — no icon (see
   renderProjectSwitcher in chrome.js). */
.sidebar-proj { width: 100%; min-width: 0; }
.proj-switch {
  position: relative;             /* anchor for the collapsed-rail tooltip */
  display: flex; align-items: center; gap: .5rem; width: 100%;
  padding: .45rem .55rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); text-align: left; cursor: pointer;
  transition: border-color var(--transition), background-color var(--transition), box-shadow var(--transition);
}
.proj-switch:hover { border-color: var(--border-strong); background: #fcfdfe; }
.proj-switch[aria-expanded="true"] { border-color: var(--brand); box-shadow: 0 0 0 3px var(--nav-hover-bg); }
.proj-switch:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.proj-switch::after { display: none; }                /* no Bootstrap caret */
.proj-switch-name {
  flex: 1 1 auto; min-width: 0; font-size: .875rem; font-weight: 600; color: #111827;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.proj-switch .bi-chevron-expand { flex-shrink: 0; font-size: .8rem; color: var(--text-muted); }

/* ── Account chip, pinned to the bottom of the rail (menu opens upward) ── */
.app-sidebar-foot {
  flex: 0 0 auto; padding: .5rem .625rem .625rem;
  border-top: 1px solid var(--border); background: var(--surface);
}
.user-chip {
  position: relative;             /* anchor for the collapsed-rail tooltip */
  display: flex; align-items: center; gap: .55rem; width: 100%;
  padding: .375rem .5rem; border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--text); font-size: .875rem;
  text-align: left; cursor: pointer; transition: background-color var(--transition);
}
.user-chip:hover, .user-chip[aria-expanded="true"] { background: var(--nav-hover-bg); }
.user-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.user-chip::after { display: none; }                  /* no Bootstrap caret */
.user-chip .bi-chevron-expand { flex-shrink: 0; font-size: .8rem; color: #64748b; }

.user-meta { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
.user-name { font-size: .8125rem; font-weight: 600; color: #1f2937; }
.user-mail { font-size: .72rem; color: #64748b; }
.user-name, .user-mail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Line the menu up with the chip instead of Bootstrap's own min-width. */
.portal-menu { min-width: 240px; }
.app-sidebar-foot .dropdown-menu.portal-menu { min-width: calc(var(--sidebar-w) - 1.25rem); }
.menu-identity { padding: .4rem .65rem .3rem; }
.menu-org {
  display: flex; align-items: center; gap: .45rem; min-width: 0;
  font-size: .78rem; font-weight: 600; color: var(--text);
}
.menu-org .bi { flex-shrink: 0; color: var(--text-muted); }
.menu-org span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Guide §5 puts this inside .app-sidebar-nav; here it sits in .app-sidebar-head
   (which owns the padding) as the non-switchable twin of .proj-switch, so it
   carries only the small inset that lines its text up with the nav items below. */
.nav-section-label {
  padding: 0 .5rem; font-size: .6875rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: #64748b;
  white-space: nowrap; overflow: hidden;
}

.nav-item {
  position: relative; display: flex; align-items: center; gap: .625rem;
  padding: .375rem .625rem; border-radius: var(--radius-sm); color: #1f2937;
  font-size: .9rem; font-weight: 500; text-decoration: none; white-space: nowrap;
  transition: background-color var(--transition), color var(--transition);
}
.nav-item:hover { color: #0f172a; background: var(--nav-hover-bg); }
.nav-item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.nav-item.is-active { color: #111827; background: var(--nav-active-bg); font-weight: 600; }
.nav-item .nav-icon {
  flex-shrink: 0; width: 20px; font-size: 1rem; line-height: 1; text-align: center;
  color: var(--brand); -webkit-text-stroke: 0.5px currentColor;   /* thicken outline */
}
.nav-item.is-active .nav-icon { color: var(--brand); }
.nav-item .nav-text { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; }
/* An expanded parent is a group heading, not a link: it drops the pointer, the
   hover feedback AND the active pill. The pill moves down to the child that is
   actually open — two filled rows stacked would read as two current pages. */
.nav-item.is-parent { cursor: default; background: none; }
.nav-item.is-parent:hover { background: none; }

/* ── Submenu (rail children of Settings / Defaults) ──
   No box around the group and no gaps between the rows: what makes these read as
   children is that they sit TIGHT under their parent, indented to its text, with
   a 2px tick in its icon column. A container (border or tint) would only draw a
   second edge inside a rail that already has one, and any gap between the rows
   breaks the run that ties them together. Only hover and the open row are
   filled — same two blues as the rail items above. */
.nav-sub { display: flex; flex-direction: column; margin: 0 0 .125rem; }
.nav-subitem {
  position: relative;                       /* anchor for the tick */
  padding: .3rem .5rem .3rem 2.5rem;        /* 2.5rem = where the parent's label starts */
  border-radius: var(--radius-sm);
  color: #334155; font-size: .8125rem; font-weight: 500;
  text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background-color var(--transition), color var(--transition);
}
.nav-subitem::before {
  content: ''; position: absolute; left: calc(1.25rem - 1px); top: 50%;
  width: 2px; height: 1.05rem; margin-top: -.525rem;
  border-radius: 1px; background: var(--brand); opacity: .35;
  transition: opacity var(--transition);
}
.nav-subitem:hover { color: #0f172a; background: var(--nav-hover-bg); }
.nav-subitem:hover::before { opacity: .6; }
.nav-subitem:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.nav-subitem.is-active { color: #111827; background: var(--nav-active-bg); font-weight: 600; }
.nav-subitem.is-active::before { opacity: 1; }

/* Views under Conversations: the same row, plus a count pushed to the right edge.
   The label truncates and the count never does — a queue whose name is too long
   is still readable as "…, 47", while a clipped number would be a lie. */
.nav-view { display: flex; align-items: center; gap: .5rem; padding-right: .4rem; }
.nav-view-label { overflow: hidden; text-overflow: ellipsis; }
.nav-view-count {
  margin-left: auto; flex: none;
  padding: 0 .35rem; min-width: 1.25rem;
  border-radius: 999px;
  background: #f1f5f9; color: #334155;
  font-size: .6875rem; font-weight: 600; font-variant-numeric: tabular-nums;
  line-height: 1.35rem; text-align: center;
}
.nav-subitem.is-active .nav-view-count { background: #fff; color: #1f2937; }
.nav-view-count.is-alert { background: var(--red-bg); color: #991b1b; }

/* ── Collapsed (icon-rail) — desktop only. Dormant while DESKTOP_COLLAPSE is
   false in chrome.js; kept intact so re-enabling is a one-line change. ── */
@media (min-width: 992px) {
  .app-shell.is-collapsed .app-sidebar { width: var(--sidebar-w-min); }
  .app-shell.is-collapsed .app-sidebar-nav { overflow: visible; }   /* let tooltips escape */
  .app-shell.is-collapsed .app-sidebar .nav-text,
  .app-shell.is-collapsed .app-sidebar .nav-section-label { display: none; }
  .app-shell.is-collapsed .app-sidebar .nav-item { justify-content: center; padding-left: 0; padding-right: 0; }
  .app-shell.is-collapsed .app-sidebar .nav-icon { width: auto; }
  /* Children are text-only, so 68px has nothing to show them with. The page's own
     heading still names the open section; re-enabling DESKTOP_COLLAPSE would need
     a flyout here to reach the others. */
  .app-shell.is-collapsed .app-sidebar .nav-sub { display: none; }

  /* The whole scope header goes: a switcher whose only content is a project name
     has nothing to show in 68px. Re-enabling DESKTOP_COLLAPSE therefore needs a
     collapsed affordance for it (an initial? the kind glyph back?) — the rail is
     otherwise the only route to another project. */
  .app-shell.is-collapsed .app-sidebar-head { display: none; }

  /* Footer chip collapses to the avatar alone. */
  .app-shell.is-collapsed .app-sidebar-foot { padding-left: .35rem; padding-right: .35rem; }
  .app-shell.is-collapsed .user-chip { justify-content: center; padding-left: 0; padding-right: 0; }
  .app-shell.is-collapsed .user-meta,
  .app-shell.is-collapsed .user-chip .bi-chevron-expand { display: none; }

  /* Custom tooltip for the collapsed rail (not the browser's title attribute).
     display: block is required: .user-chip::after is set to none to kill the
     Bootstrap caret, and that would otherwise win over this rule. */
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::after {
    content: attr(data-tip); display: block;
    position: absolute; left: calc(100% + 10px); top: 50%;
    transform: translateY(-50%) scale(.96); transform-origin: left center;
    background: #111827; color: #fff; font-size: .8rem; font-weight: 500; line-height: 1;
    padding: .45rem .6rem; border-radius: 7px; white-space: nowrap;
    box-shadow: 0 8px 24px rgba(2,6,23,.3); opacity: 0; pointer-events: none;
    transition: opacity .13s ease, transform .13s cubic-bezier(.4,0,.2,1); z-index: var(--z-topbar);
  }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::before {
    content: ""; position: absolute; left: calc(100% + 4px); top: 50%;
    transform: translateY(-50%); border: 5px solid transparent; border-right-color: #111827;
    opacity: 0; pointer-events: none; transition: opacity .13s ease; z-index: var(--z-topbar);
  }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):hover::after,
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):focus-visible::after { opacity: 1; transform: translateY(-50%) scale(1); }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):hover::before,
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip):focus-visible::before { opacity: 1; }
}

/* ── Content pane — fills the width next to the rail (and widens when it
   collapses), so override Bootstrap's .container max-width/centering. ── */
main.app-content {
  flex: 1 1 auto; min-width: 0; width: 100%; max-width: none; margin: 0;
  padding: 0 var(--content-pad) 3rem;
}

/* ── Mobile: off-canvas drawer ── */
.chrome-backdrop {
  position: fixed; inset: var(--topbar-h) 0 0; z-index: var(--z-backdrop);
  background: rgba(2,6,23,.45); opacity: 0; visibility: hidden;
  transition: opacity var(--transition), visibility var(--transition);
}
@media (max-width: 991.98px) {
  .app-sidebar {
    position: fixed; top: var(--topbar-h); left: 0; height: calc(100vh - var(--topbar-h));
    width: var(--sidebar-w); transform: translateX(-100%);
    box-shadow: var(--shadow-xl); transition: transform .22s cubic-bezier(.4,0,.2,1);
  }
  .app-shell.drawer-open .app-sidebar { transform: translateX(0); }
  .app-shell.drawer-open .chrome-backdrop { opacity: 1; visibility: visible; }
  :root { --content-pad: clamp(1rem, 4vw, 1.5rem); }
}

/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
  .app-sidebar, .chrome-backdrop, .chrome-toggle, .nav-item, .user-chip, .proj-switch { transition: none; }
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::after,
  .app-shell.is-collapsed .app-sidebar :is(.nav-item, .user-chip)::before { transition: none; }
}

/* General-purpose link style, reused for in-page "back to …" links (see .page-back) */
.topbar-link {
  display: inline-flex; align-items: center; gap: .4rem; padding: .4rem .75rem;
  border-radius: var(--radius-sm); font-size: .875rem; color: #334155; text-decoration: none;
  transition: color var(--transition), background-color var(--transition); white-space: nowrap; font-weight: 500;
}
.topbar-link:hover { color: #000; background: rgba(15,23,42,.05); }

/* In-page "back to …" link, sits above the page-head */
.page-back { margin-bottom: .5rem; padding-left: 0 !important; }

/* (The per-project context row + underline tabs that used to live here are gone:
   Dashboard/Conversations/Settings are rail items now, and which project you are
   inside is answered by the switcher at the head of the rail — see .proj-switch.) */

/* Project picker cards */
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); gap: 1rem; }
.project-card {
  text-align: left; display: flex; flex-direction: column; min-height: 130px; padding: 1.15rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  cursor: pointer; font: inherit; color: inherit; box-shadow: var(--shadow-sm);
  transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition);
}
.project-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-lg); transform: translateY(-3px); }
.project-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.project-card.is-archived { cursor: default; background: #f8fafc; }
.project-card.is-archived:hover { border-color: var(--border); box-shadow: none; transform: none; }
.project-card-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: .9rem; }
.project-icon { width: 44px; height: 44px; border-radius: var(--radius); background: #0f172a; color: #fff; display: grid; place-items: center; font-size: 1.2rem; }
.project-card.is-archived .project-icon { background: #e2e8f0; color: #94a3b8; }
.project-name { font-weight: 700; font-size: 1.0625rem; letter-spacing: -0.01em; margin-bottom: .25rem; }
.project-meta { font-size: .8rem; color: var(--text-muted); margin-top: auto; }

/* Per-card overflow (three-dot) action menu, top-right of the card */
.project-menu { position: relative; }
.project-menu-btn {
  width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent; padding: 0;
  color: var(--text-muted); display: inline-flex; align-items: center; justify-content: center; font-size: 1rem;
  transition: background-color var(--transition), color var(--transition);
}
.project-menu-btn:hover, .project-menu-btn[aria-expanded="true"] { background: rgba(15,23,42,.07); color: #0f172a; }

/* Account avatar (initials disc inside the sidebar footer chip) */
.user-avatar {
  width: 28px; height: 28px; border-radius: 50%; background: #334155; color: #fff;
  font-size: .72rem; font-weight: 600; display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; text-transform: uppercase; border: none; padding: 0;
}

/* Shared dropdown-menu look (bootstrap dropdowns used for the account + project menus) */
.dropdown-menu { border-radius: var(--radius); border-color: var(--border); box-shadow: var(--shadow-lg); font-size: .8438rem; padding: .35rem; }
.dropdown-item { display: flex; align-items: center; gap: .55rem; border-radius: var(--radius-sm); padding: .45rem .65rem; }
.dropdown-item:hover, .dropdown-item:focus { background: rgba(15,23,42,.06); }
.dropdown-item.text-danger:hover, .dropdown-item.text-danger:focus { background: var(--red-bg); }

/* ── Page header ──────────────────────────────────────────── */
.page-head { padding: 1rem 0 1.25rem; display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.page-head h1 { font-size: 1.375rem; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 .2rem; }
.page-head .page-sub { font-size: .875rem; color: #475569; margin: 0; max-width: 60ch; }
/* Its slot, where the link is rendered rather than written into the markup
   (Reports: only the person scorecard has a way back). The slot takes over the
   heading's top padding while it is showing, so the link occupies the space the
   heading already had instead of pushing the whole page down a line. */
.page-back-slot { padding-top: 1.25rem; }
.rp-head { padding-top: 1.5rem; }
.page-back-slot:not([hidden]) + .rp-head { padding-top: 0; }
/* Right-hand action cluster of a page header (one primary + any icon buttons). */
.page-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* ── Cards ────────────────────────────────────────────────── */
.card-x {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.card-x + .card-x { margin-top: 1rem; }
/* grid rows already space cards via `gap`; cancel the flow-stack margin so the
   2nd card in a row doesn't drop below the first (e.g. dashboard .chart-grid).
   EVERY container that spaces cards itself has to be listed here — one that
   forgets shows it twice over: in a grid row the second card sits 1rem low and
   the first is stretched by that 1rem into a band of empty card below its own
   last row; in a flex column the two just drift 1rem further apart than the
   neighbours either side of them. */
.chart-grid > .card-x + .card-x,
.rp-split > .card-x + .card-x,
.db-stack > .card-x + .card-x { margin-top: 0; }
.card-x-head { padding: .9rem 1.15rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.card-x-head h2 { font-size: .95rem; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
.card-x-body { padding: 1.15rem; }

/* ── Stat cards ───────────────────────────────────────────── */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: 1rem; }
/* Read-only tiles — nothing to click, so no hover lift: motion on hover promises
   an affordance the card doesn't have. Flat surface, no wash; the only weight is
   a 2px ink rail on the top edge, the same on every card (the metric is named by
   its label, so the rail is structure rather than a colour code).
   (The dashboard does NOT use these — it leads with one hero figure and a
   divided metric strip instead; see .db-lead / .db-metrics.) */
.stat-card {
  position: relative; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.15rem 1.25rem; box-shadow: var(--shadow-sm);
}
.stat-card::before { content: ''; position: absolute; inset: 0 0 auto; height: 2px; background: var(--ink); }
.stat-card .stat-label { font-size: .8125rem; color: var(--text-muted); font-weight: 500; display: flex; align-items: center; gap: .4rem; }
.stat-card .stat-value { font-size: 1.9rem; font-weight: 800; letter-spacing: -0.02em; margin-top: .35rem; line-height: 1; }
.stat-card .stat-value small { font-size: .95rem; font-weight: 600; color: var(--text-muted); }
.stat-card .stat-foot { font-size: .78rem; color: var(--text-muted); margin-top: .5rem; }

/* ── Badges ───────────────────────────────────────────────── */
.badge-x { display: inline-flex; align-items: center; gap: .3rem; padding: .2rem .55rem; border-radius: 999px; font-size: .75rem; font-weight: 600; line-height: 1.4; white-space: nowrap; }
.badge-topic { background: var(--slate-bg); color: #334155; border: 1px solid var(--border); }
.badge-green { background: var(--green-bg); color: #15803d; }
.badge-amber { background: var(--amber-bg); color: #b45309; }
.badge-red { background: var(--red-bg); color: #b91c1c; }
.badge-blue { background: var(--blue-bg); color: #1d4ed8; }
.badge-slate { background: var(--slate-bg); color: #475569; border: 1px solid var(--border); }

/* Call direction. Deliberately the quietest chip in the table, and the reason is
   the table: sentiment, QA and status already spend the colour budget, and every
   one of them is a JUDGEMENT — good, warning, bad. Direction is a plain fact about
   the call, so a saturated fill (this used to be blue vs violet) made a
   non-signal shout over the signals, and put a second accent hue in an app whose
   token set commits to one (see --brand).
   So: outlined, near-neutral ink, and the meaning carried by a directional arrow —
   inbound leans on the single blue accent, outbound stays slate. Readable at a
   glance down a column, invisible when you're scanning for a red score. */
.dir-badge {
  display: inline-flex; align-items: center; gap: .28rem;
  font-size: .72rem; font-weight: 600; line-height: 1.4; white-space: nowrap;
  padding: .12rem .45rem .12rem .38rem;
  border: 1px solid var(--border); border-radius: var(--radius-xs);
  background: var(--surface);
}
.dir-badge .bi { font-size: .78rem; }
.dir-inbound { border-color: #dbe6fb; background: #f2f6fe; color: #1e40af; }
.dir-inbound .bi { color: var(--brand); }
.dir-outbound { border-color: var(--border); background: #f8fafc; color: #475569; }
.dir-outbound .bi { color: #94a3b8; }

/* status dots */
.status-badge { display: inline-flex; align-items: center; gap: .35rem; font-size: .78rem; font-weight: 600; }
.status-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.st-done .status-dot { background: var(--green); }
.st-done { color: #15803d; }
.st-analyzing .status-dot, .st-transcribing .status-dot, .st-ingested .status-dot, .st-transcribed .status-dot { background: var(--amber); animation: pulse 1.4s ease-in-out infinite; }
.st-analyzing, .st-transcribing, .st-ingested, .st-transcribed { color: #b45309; }
.st-error .status-dot { background: var(--red); }
.st-error { color: #b91c1c; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .status-dot { animation: none; } }

/* ── Processing strip (conversations) ─────────────────────────
   A status line, not a card: it sits between the page title and the grid and
   has to read as a caption on the grid rather than compete with it. Hence the
   plain surface, one row, and numbers carried by weight instead of color —
   only the two states that need action (stuck, failed) are allowed a hue. */
.pipe {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .75rem;
  margin-bottom: .75rem; padding: .45rem .75rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); font-size: .78rem; color: var(--text-muted);
}
.pipe-head { display: inline-flex; align-items: center; gap: .35rem; font-weight: 600; color: #475569; }
.pipe-head .bi { font-size: .85rem; color: #94a3b8; }
.pipe-head.is-busy .bi { color: var(--amber); }
.pipe-head b { color: var(--text); }
.pipe-chips { display: inline-flex; align-items: center; flex-wrap: wrap; gap: .35rem; }
.pipe-chip {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .16rem .5rem; font-size: .74rem; font-weight: 500; line-height: 1.5;
  border: 1px solid var(--border); border-radius: 999px;
  background: #f8fafc; color: #475569; cursor: pointer;
}
.pipe-chip:hover { border-color: #cbd5e1; background: #f1f5f9; color: var(--text); }
.pipe-chip .status-dot { background: var(--amber); animation: pulse 1.4s ease-in-out infinite; }
.pipe-chip b { color: var(--text); font-weight: 700; }
.pipe-chip.is-err { border-color: #fecaca; background: #fef2f2; color: #b91c1c; }
.pipe-chip.is-err:hover { border-color: #fca5a5; background: #fee2e2; }
.pipe-chip.is-err .status-dot { background: var(--red); animation: none; }
.pipe-chip.is-err b { color: #b91c1c; }
.pipe-notes { display: inline-flex; align-items: center; flex-wrap: wrap; gap: .4rem; margin-left: auto; }
.pipe-notes b { color: var(--text); font-weight: 600; }
.pipe-sep { color: #cbd5e1; }
.pipe-flag { display: inline-flex; align-items: center; gap: .3rem; font-weight: 600; cursor: help; }
.pipe-flag.err { color: #b91c1c; }
.pipe-flag.warn { color: #b45309; }

/* QA color helpers */
.qa-good { color: var(--green); }
.qa-mid { color: var(--amber); }
.qa-bad { color: var(--red); }

/* ── Table ────────────────────────────────────────────────── */
.tx-table-wrap { overflow-x: auto; }
table.tx { width: 100%; border-collapse: collapse; font-size: .8438rem; }
/* Header text is a step darker than --text-muted: on the darker header fill the
   muted grey fell under 4.5:1, and these labels are 12px uppercase. */
table.tx thead th { text-align: left; font-weight: 600; color: #475569; font-size: .75rem; text-transform: uppercase; letter-spacing: .03em; padding: .6rem .9rem; border-bottom: 1px solid var(--tbl-border); white-space: nowrap; background: var(--tbl-head-bg); }
table.tx tbody td { padding: .75rem .9rem; border-bottom: 1px solid var(--tbl-border); vertical-align: middle; }
/* Fill on the ROW, not the cell: a cell fill would sit on top of the :hover rule
   below and swallow it (the pinned cell is the one exception — being sticky, it
   has to paint its own, see .dt-pin). */
table.tx tbody tr { cursor: pointer; background: var(--tbl-row-bg); transition: background-color var(--transition); }
table.tx tbody tr:hover { background: var(--tbl-row-hover); }
table.tx tbody tr:last-child td { border-bottom: 0; }
/* Summary row under a paged table (Reports > People: the team median). It lives
   in <tfoot> rather than at the end of <tbody> — what every row is read against
   has to stay under whichever page you are on — so it needs the cell padding
   <tbody> gets, plus a rule of its own to separate it. */
table.tx tfoot td { padding: .75rem .9rem; border-top: 1px solid var(--tbl-border); background: var(--tbl-head-bg); vertical-align: middle; }
.td-summary { color: var(--text-muted); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.td-num { font-variant-numeric: tabular-nums; }

/* Share bar — proportion of a total inside a table cell (Usage: minutes per
   project). Scaled to the LARGEST row, not to the total, so the smaller rows stay
   visible; the exact share of the total is the percentage next to it. */
/* Share-of-minutes meter. Same measure as the minutes chart, so the same hue —
   and the empty track is a lighter step of that hue, not a neutral grey. */
.usage-share { height: 6px; border-radius: 999px; background: #dbeafe; overflow: hidden; margin-bottom: .28rem; }
.usage-share > span { display: block; height: 100%; border-radius: 999px; background: #2563eb; min-width: 2px; }

/* ── Data table (`.dt`) ───────────────────────────────────────────────────────
   The Conversations grid. A card that owns its own viewport instead of growing
   down the page: search + tools, filter pills, a scroll area with a sticky header
   and a pinned first column, then the pager. Everything that says "which rows and
   which columns" stays on screen while you scroll through them, which is the whole
   difference between a list and a grid you work in.

   `.tx` (above) is still the plain table used elsewhere; `.dt-table` adds the grid
   behaviour on top of it. */
.dt {
  /* The height of the rows' viewport, named once: the first-load spinner below
     stands in a box of exactly this size, so a full page of rows replaces it
     without moving anything. */
  --dt-viewport: clamp(320px, 62vh, 940px);
  --tbl-row-bg: #ffffff;
  --tbl-row-hover: #f8fafc;
  --tbl-pin-hover: #f1f5f9;
  --tbl-head-bg: #eef2f6;
  --tbl-border: #dbe1e8;
  display: flex; flex-direction: column; overflow: hidden;
}

/* Row A — free-text search, then the view controls, hard right. */
.dt-bar {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .6rem .75rem; border-bottom: 1px solid var(--chrome-border);
  background: var(--dt-black); color: var(--chrome-ink);
}
.dt-search { position: relative; flex: 1 1 280px; max-width: 440px; min-width: 170px; }
.dt-search > .bi-search { position: absolute; left: .6rem; top: 50%; transform: translateY(-50%); font-size: .8rem; color: var(--text-muted); pointer-events: none; }
.dt-search input {
  width: 100%; padding: .4rem 1.7rem .4rem 1.85rem; font-size: .8438rem;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--text);
}
.dt-search input::placeholder { color: var(--text-muted); }
.dt-search input:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px var(--nav-hover-bg); }
.dt-search input::-webkit-search-cancel-button { display: none; }   /* we draw our own */
.dt-x {
  position: absolute; right: .3rem; top: 50%; transform: translateY(-50%);
  display: inline-flex; border: 0; background: none; color: var(--text-muted);
  padding: .15rem .25rem; border-radius: var(--radius-xs); line-height: 1; cursor: pointer;
}
.dt-x:hover { background: var(--slate-bg); color: var(--text); }
.dt-bar-spacer { flex: 1 1 auto; }

.dt-tool {
  display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap;
  padding: .35rem .6rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: #334155; font-size: .8125rem; font-weight: 600;
  cursor: pointer; transition: all var(--transition);
}
.dt-tool:hover { border-color: var(--border-strong); background: #f8fafc; color: var(--text); }
.dt-tool:disabled { opacity: .55; cursor: wait; }
.dt-tool[aria-expanded="true"] { border-color: var(--brand); background: var(--nav-hover-bg); color: var(--brand); }
.dt-tool:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.dt-tool::after { display: none; }                     /* no Bootstrap caret */
.dt-tool .dt-num-badge {
  min-width: 1.15rem; padding: 0 .25rem; border-radius: 999px;
  background: var(--brand); color: #fff; font-size: .68rem; font-variant-numeric: tabular-nums;
}
.dt-date-tool.is-on { border-color: #bfd3fb; background: var(--nav-active-bg); color: #1e3a8a; }
.dt-date-tool { border-radius: 999px; padding-inline: .8rem; }
.dt-date-tool.is-on > .bi:first-child { color: var(--brand); }
.dt-date-tool.is-on .dt-pill-label::after { content: ':'; }
.dt-date-tool .dt-pill-val { max-width: 18ch; }

/* Dark command controls stay visually inside the bar instead of becoming a row
   of bright boxes. The page-level date filter keeps the light base style. */
.dt-bar .dt-search input {
  border-color: rgba(255,255,255,.18); background: rgba(255,255,255,.08);
  color: #fff; caret-color: #fff;
}
.dt-bar .dt-search > .bi-search { color: var(--chrome-ink-dim); }
.dt-bar .dt-search input::placeholder { color: #fff; opacity: 1; }
.dt-bar .dt-search input:focus { border-color: rgba(255,255,255,.52); box-shadow: 0 0 0 3px rgba(255,255,255,.1); }
.dt-bar .dt-x { color: var(--chrome-ink-dim); }
.dt-bar .dt-x:hover { background: var(--chrome-hover); color: #fff; }
.dt-bar .dt-tool { border-color: rgba(255,255,255,.14); background: rgba(255,255,255,.07); color: var(--chrome-ink); }
.dt-bar .dt-tool:hover { border-color: rgba(255,255,255,.28); background: rgba(255,255,255,.13); color: #fff; }
.dt-bar .dt-tool[aria-expanded="true"] { border-color: rgba(255,255,255,.36); background: rgba(255,255,255,.16); color: #fff; }
.dt-bar .dt-tool:focus-visible { outline-color: #fff; outline-offset: 2px; }

/* Row B — one pill per filter. A pill is the filter AND its value: closed it
   reads "Sentiment: Negative", so the active query is legible without a separate
   chips row echoing it back. Set pills tint; the rest stay quiet. */
.dt-filters {
  display: flex; justify-content: flex-start; align-items: center; flex-wrap: wrap;
  gap: .35rem; padding: .45rem .75rem;
  /* One pill's worth of height, held from the first paint: the pills only exist
     after the project's kind is known, and a strip that grows a row later shoves
     the whole grid down while you are already looking at it. */
  min-height: 2.5rem;
  border-bottom: 1px solid var(--chrome-border); background: var(--dt-black);
}
.dt-pill {
  display: inline-flex; align-items: center; gap: .35rem; max-width: 100%;
  padding: .25rem .55rem; border: 1px solid #fff; border-radius: 999px;
  background: #fff; color: #111827; font-size: .8125rem; white-space: nowrap;
  cursor: pointer; transition: all var(--transition);
}
.dt-pill::after { display: none; }
.dt-pill:hover { border-color: #e2e8f0; background: #f1f5f9; color: #050505; }
.dt-pill:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.dt-pill[aria-expanded="true"] { border-color: #bfdbfe; background: #fff; color: #1d4ed8; box-shadow: none; }
.dt-pill > .bi:first-child { flex-shrink: 0; font-size: .8rem; color: #475569; }
.dt-pill > .bi-chevron-down { flex-shrink: 0; font-size: .58rem; opacity: .55; }
.dt-pill-label { font-weight: 500; }
.dt-pill-val { font-weight: 600; overflow: hidden; text-overflow: ellipsis; max-width: 16ch; }
.dt-pill.is-on { border-color: #bfdbfe; background: #fff; color: #1d4ed8; }
.dt-pill.is-on > .bi:first-child { color: var(--brand); }
.dt-pill.is-on .dt-pill-label::after { content: ':'; }
/* Clearing one filter must not open its menu on the way — the button sits inside
   the toggle, so its handler stops the event. */
.dt-pill-x { display: inline-flex; border: 0; background: none; color: inherit; padding: 0 0 0 .1rem; font-size: .85rem; line-height: 1; opacity: .6; cursor: pointer; }
.dt-pill-x:hover { opacity: 1; }

/* Menus with a footer (a custom range, a reset) are a flex column: only the
   option list scrolls, so the footer can't slide out of reach — which is exactly
   what a plain max-height + overflow does to it. `.show` is on the selector
   because Bootstrap's own `.dropdown-menu.show { display: block }` would
   otherwise beat a bare class here, and a bare `display: flex` would beat
   `.dropdown-menu { display: none }` and leave the menu permanently open. */
.dt-pill-menu.show, .dt-cols-menu.show { display: flex; flex-direction: column; }
.dt-menu-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
.dt-menu-foot { flex: 0 0 auto; }

.dt-pill-menu { min-width: 216px; max-height: min(26rem, 72vh); }
.dt-pill-menu .dropdown-item.is-on { background: var(--nav-active-bg); font-weight: 600; }
.dt-pill-menu .dropdown-item .bi-check2 { margin-left: auto; color: var(--brand); }
.dt-menu-label { padding: .4rem .65rem .2rem; font-size: .68rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--text-muted); }
.dt-menu-row { display: flex; align-items: center; gap: .35rem; padding: .1rem .65rem .45rem; }
.dt-menu-row input { flex: 1 1 0; min-width: 0; font-size: .8125rem; padding: .3rem .45rem; border: 1px solid var(--border); border-radius: var(--radius-xs); color: var(--text); background: var(--surface); }
.dt-menu-row input:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px var(--nav-hover-bg); }
.dt-menu-row span { font-size: .75rem; color: var(--text-muted); }
.dt-menu-error { flex-basis: 100%; color: var(--red); font-size: .75rem; line-height: 1.35; }

@media (max-width: 767.98px) {
  .page-head .page-actions { width: 100%; }
  #headerDateFilter { margin-left: auto; }
}
@media (max-width: 479.98px) {
  .page-head .page-actions { display: grid; grid-template-columns: 1fr 1fr; }
  #headerDateFilter { margin-left: 0; }
  #headerDateFilter .dt-tool, #csvBtn { width: 100%; justify-content: center; }
  .page-head .page-actions #uploadBtn { grid-column: 1 / -1; justify-content: center; }
}

/* Column picker — a checklist in definition order, so toggling a column never
   moves the others around. */
.dt-cols-menu { min-width: 234px; max-height: min(26rem, 72vh); }
.dt-col-row { display: flex; align-items: center; gap: .5rem; margin: 0; padding: .3rem .65rem; border-radius: var(--radius-sm); font-size: .8438rem; cursor: pointer; }
.dt-col-row:hover { background: rgba(15,23,42,.06); }
.dt-col-row input { margin: 0; flex-shrink: 0; }
.dt-col-row.is-locked { color: var(--text-muted); cursor: default; }
.dt-col-row.is-locked:hover { background: none; }
.dt-cols-foot { display: flex; justify-content: flex-end; padding: .3rem .4rem .1rem; }

/* The grid viewport. Both axes scroll here so the header can stick: a wrapper
   that scrolls only horizontally still creates a scroll container, and the header
   would then stick to a box with no vertical overflow — i.e. not at all. */
.dt-scroll {
  overflow: auto; max-height: var(--dt-viewport);
  scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;
  /* Same fill as the rows, so a short page (or the last row of a long one) ends
     in the grid's own surface rather than a white band the rows appear to float on. */
  background: var(--tbl-row-bg);
}
/* Phones: drop the internal viewport entirely. A short scroll box inside a
   scrolling page is two nested gestures for six visible rows, and the sticky
   header it buys is worth less than that costs. Horizontal scroll stays. */
@media (max-width: 767.98px) {
  .dt-scroll { max-height: none; }
}
/* Tighter gutters than the plain `.tx` table: a dozen columns is a lot of
   horizontal budget, and .15rem a side buys back most of a column. */
table.dt-table { min-width: 100%; }
table.dt-table thead th { padding-inline: .75rem; }
table.dt-table tbody td { padding-inline: .75rem; }
table.dt-table thead th { position: sticky; top: 0; z-index: 3; }
/* A sticky cell's own border-bottom scrolls away with the row it was painted for
   in some engines; this shim is drawn by the sticky element itself. */
table.dt-table thead th::after { content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px; background: var(--tbl-border); }
table.dt-table tbody tr:hover { background: var(--tbl-row-hover); }
table.dt-table tbody tr:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* First column is pinned: with a dozen columns the timestamp is what tells you
   which conversation a row is, and it has to survive scrolling right. Pinned
   cells need their own background (the row's would scroll under them) — hence the
   hover/head duplicates below. */
table.dt-table :is(th, td).dt-pin { position: sticky; left: 0; z-index: 2; background: var(--tbl-row-bg); }
table.dt-table thead th.dt-pin { z-index: 4; background: var(--tbl-head-bg); }
table.dt-table tbody tr:hover td.dt-pin { background: var(--tbl-pin-hover); }
table.dt-table :is(th, td).dt-pin::before { content: ''; position: absolute; top: 0; bottom: 0; right: -1px; width: 1px; background: var(--tbl-border); }
/* Lifted only once you have actually scrolled right: the shadow is the cue that
   something is hidden behind this column, so it would be a lie at rest. */
.dt.is-scrolled-x table.dt-table :is(th, td).dt-pin { box-shadow: 6px 0 10px -6px rgba(15,23,42,.22); }

/* Sortable header: the label is the button. The arrow is latent (half-lit on
   hover) and solid on the sorted column, so exactly one arrow is ever loud. */
.dt-th { display: inline-flex; align-items: center; gap: .3rem; padding: 0; border: 0; margin: 0; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
.dt-th:hover { color: var(--text); }
.dt-th:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.dt-th .dt-sort { font-size: .68rem; opacity: 0; transition: opacity var(--transition); }
.dt-th:hover .dt-sort { opacity: .45; }
.dt-th.is-sorted { color: var(--brand); }
.dt-th.is-sorted .dt-sort { opacity: 1; }
table.dt-table th.td-num, table.dt-table td.td-num { text-align: right; }
table.dt-table th.td-num .dt-th { flex-direction: row-reverse; }
/* Trailing affordance column: says the row opens, without spending a label on it. */
table.dt-table :is(th, td).dt-go { width: 1%; padding-left: 0; text-align: right; }
table.dt-table td.dt-go { color: var(--border-strong); }
table.dt-table tbody tr:hover td.dt-go { color: var(--brand); }

/* Compact density — same grid, ~30% more rows in the viewport. */
.dt.is-compact table.dt-table thead th { padding-top: .4rem; padding-bottom: .4rem; }
.dt.is-compact table.dt-table tbody td { padding-top: .4rem; padding-bottom: .4rem; }

/* Pager — page size + range on the left, the page walk on the right. */
.dt-pager {
  justify-content: space-between; padding: .65rem .85rem; border-top-color: var(--chrome-border);
  background: var(--dt-black); color: var(--chrome-ink-dim);
}
.dt-pager .muted { color: var(--chrome-ink-dim); }
.dt-pager-left { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.dt-size { padding: .2rem 1.35rem .2rem .45rem; font-size: .8125rem; color: #fff; border: 1px solid rgba(255,255,255,.2); border-radius: var(--radius-xs); background: rgba(255,255,255,.09); }
.dt-size option { color: var(--text); background: #fff; }
.dt-range { color: #fff; font-variant-numeric: tabular-nums; }
.dt-pages { display: flex; align-items: center; gap: .15rem; }
.dt-page {
  min-width: 30px; height: 30px; padding: 0 .4rem;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--radius-sm); background: none;
  color: var(--chrome-ink); font-size: .8125rem; font-weight: 600; font-variant-numeric: tabular-nums; cursor: pointer;
  transition: background-color var(--transition), color var(--transition);
}
.dt-page:hover:not(:disabled):not(.is-on) { background: var(--chrome-hover); color: #fff; }
.dt-page.is-on { background: #fff; border-color: #fff; color: var(--dt-black); cursor: default; }
.dt-page:disabled { opacity: .35; cursor: default; }
.dt-page:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
.dt-gap { padding: 0 .1rem; color: var(--chrome-ink-dim); }
@media (max-width: 575.98px) {
  .dt-pager { justify-content: center; }
  .dt-page-num { display: none; }        /* keep only the step arrows + range */
}

/* Skeleton rows: the real column count, so the header doesn't jump when data
   lands. */
.dt-skel { height: 11px; border-radius: 999px; background: linear-gradient(90deg, #eef2f7 25%, #f7fafc 37%, #eef2f7 63%); background-size: 400% 100%; animation: dtShimmer 1.4s ease infinite; }
@keyframes dtShimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { .dt-skel { animation: none; } }

/* First paint: the grid's own loading state ─────────────────────────────────
   Rows are three requests deep (project → sources/topics → calls), so the card
   used to paint its toolbar, its column header and its pager around an empty
   hole and then drop the body in. Instead the body IS one spinner until the
   first response lands, in a box the size of the viewport the rows will fill —
   so nothing arrives late and nothing jumps when it does. `is-first-load` sits
   in the served markup, not on a class JS adds, or the hole would flash first. */
.dt-loading { display: none; }
.dt.is-first-load > .dt-scroll,
.dt.is-first-load > .dt-pager { display: none; }
.dt.is-first-load > .dt-loading {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .75rem; min-height: var(--dt-viewport);
  background: var(--tbl-row-bg); color: var(--text-muted);
}
.dt-loading-spin {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2.5px solid var(--border); border-top-color: var(--brand);
  animation: spin .7s linear infinite;
}
.dt-loading-text { font-size: .8125rem; font-weight: 500; }
/* Placeholders for the two pill strips, which are rendered by JS one request
   later than the toolbar around them. Same size as the real pills, so nothing
   moves when they land; quiet enough not to look clickable. */
.dt-pill-ghost {
  display: inline-block; height: 26px; border-radius: 999px;
  background: rgba(255, 255, 255, .13); animation: dtGhost 1.5s ease-in-out infinite;
}
.dt-pill-ghost.is-head { width: 11rem; height: 30px; background: var(--border); }
@keyframes dtGhost { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .dt-pill-ghost { animation: none; } }
@media (max-width: 767.98px) { .dt.is-first-load > .dt-loading { min-height: 260px; } }
/* The swap itself: a fade, so the rows read as arriving rather than snapping in.
   No fill mode — once it has run the element is back to its plain state. */
.dt.is-ready > .dt-scroll, .dt.is-ready > .dt-pager { animation: dtReveal .22s ease; }
@keyframes dtReveal { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .dt.is-ready > .dt-scroll, .dt.is-ready > .dt-pager { animation: none; }
}

/* ── Filter bar ───────────────────────────────────────────── */
.filter-bar { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; }
.filter-bar .form-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted); font-weight: 600; margin-bottom: .2rem; }
.filter-bar .form-select, .filter-bar .form-control { font-size: .8438rem; border-radius: var(--radius-sm); border-color: var(--border); }
.filter-field { display: flex; flex-direction: column; min-width: 130px; }

/* ── Period picker ────────────────────────────────────────── */
/* One dark pill: ‹ step back · the month · step forward ›. It wears the SHELL
   tokens (--chrome, the near-black top bar), not --ink: --ink is slate-tinted
   and reads as a different, muddier black next to the chrome above it.
   No chevron: the label is a button, and the arrows already say it moves.
   No overflow:hidden either — the popover has to escape the pill — so the end
   caps carry their own rounding instead. */
.period { display: inline-flex; align-items: stretch; border-radius: 999px;
  background: var(--chrome); box-shadow: 0 1px 2px rgba(11,13,15,.3); }
.period-step { border: 0; background: none; color: var(--chrome-ink-dim); width: 32px; display: grid;
  place-items: center; cursor: pointer; font-size: .9rem; transition: background-color var(--transition), color var(--transition); }
.period-step.prev { border-radius: 999px 0 0 999px; }
.period-step.next { border-radius: 0 999px 999px 0; }
.period-step:hover:not(:disabled) { background: var(--chrome-hover); color: #fff; }
.period-step:disabled { opacity: .3; cursor: default; }
.period-now { border: 0; border-left: 1px solid var(--chrome-border); border-right: 1px solid var(--chrome-border);
  background: none; display: flex; align-items: center; justify-content: center;
  padding: .42rem .8rem; min-width: 140px; font-size: .8438rem; font-weight: 600; color: var(--chrome-ink);
  cursor: pointer; transition: background-color var(--transition); }
.period-now:hover, .period-now[aria-expanded="true"] { background: var(--chrome-hover); color: #fff; }

/* Month popover — a chip grid per year, not a 12-row list: a year of months is
   a shape you scan, and four-up chips fit the whole window in two blocks. */
.month-pop { padding: .55rem; min-width: 244px; }
.month-year { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted); padding: .2rem .2rem .35rem; }
.month-year:not(:first-child) { margin-top: .5rem; border-top: 1px solid var(--border); padding-top: .55rem; }
.month-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: .3rem; }
.month-chip { border: 1px solid var(--border); background: #fff; border-radius: 999px; padding: .32rem 0;
  font-size: .78rem; font-weight: 600; color: var(--text); cursor: pointer; transition: all var(--transition); }
.month-chip:hover { border-color: #94a3b8; background: #f8fafc; }
.month-chip.is-current { background: var(--chrome); border-color: var(--chrome); color: #fff; }

/* ── Trend chip ───────────────────────────────────────────── */
/* Month-over-month movement in a stat card footer. Up is green / down is red —
   read as "more or less than last month", the same way every other metric tile
   in the product does. */
.trend { display: inline-flex; align-items: center; gap: .1rem; padding: .1rem .45rem .1rem .3rem;
  border-radius: 999px; font-size: .72rem; font-weight: 700; line-height: 1.5; }
.trend .bi { font-size: .95rem; }
.trend.up { background: var(--green-bg); color: #15803d; }
.trend.down { background: var(--red-bg); color: #b91c1c; }
.trend.flat { background: var(--slate-bg); color: #475569; padding-inline: .45rem; }

/* ── Buttons ──────────────────────────────────────────────── */
.btn-x { display: inline-flex; align-items: center; gap: .4rem; padding: .45rem .85rem; border-radius: var(--radius-sm); font-size: .8438rem; font-weight: 600; border: 1px solid var(--border-strong); background: #fff; color: var(--text); cursor: pointer; transition: all var(--transition); text-decoration: none; box-shadow: var(--shadow-xs); }
.btn-x:hover { border-color: #94a3b8; background: #f8fafc; color: var(--text); box-shadow: var(--shadow-sm); }
.btn-x:active { transform: translateY(1px); box-shadow: none; }
.btn-x:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.btn-x.primary { background: var(--ink); color: #fff; border-color: var(--ink); box-shadow: 0 1px 2px rgba(15,23,42,.25); }
.btn-x.primary:hover { background: var(--ink-hover); color: #fff; border-color: var(--ink-hover); box-shadow: var(--shadow-md); }
.btn-x.sm { padding: .3rem .6rem; font-size: .78rem; }
/* A button that can't be pressed has to look like one — and keeps its tooltip,
   which is where the reason lives (so no pointer-events: none here). */
.btn-x:disabled, .btn-x[disabled] { opacity: .45; cursor: not-allowed; box-shadow: none; }
.btn-x:disabled:hover, .btn-x[disabled]:hover { background: #fff; border-color: var(--border-strong); color: var(--text); box-shadow: none; }
.btn-x.primary:disabled:hover { background: var(--ink); border-color: var(--ink); color: #fff; }
/* Export PDF. Red because of what comes out — every PDF affordance a person has
   ever clicked is this colour, and the icon plus the word carry the rest. It is
   NOT the danger red: destructive actions in this app are text-on-white inside a
   menu (.dropdown-item.text-danger), never a filled button, so the two cannot be
   confused for one another. */
.btn-x.pdf { background: #d0342c; border-color: #d0342c; color: #fff; box-shadow: 0 1px 2px rgba(208,52,44,.28); }
.btn-x.pdf:hover { background: #b02a23; border-color: #b02a23; color: #fff; box-shadow: var(--shadow-md); }
.btn-x.pdf:disabled, .btn-x.pdf[disabled] { box-shadow: none; }
.btn-x.pdf:disabled:hover, .btn-x.pdf[disabled]:hover { background: #d0342c; border-color: #d0342c; color: #fff; }

.btn-x.danger { color: var(--red); border-color: #fecaca; }
.btn-x.danger:hover { background: var(--red-bg); }
.btn-x.danger-solid { background: var(--red); color: #fff; border-color: var(--red); }
.btn-x.danger-solid:hover { background: #b91c1c; color: #fff; border-color: #b91c1c; }
/* Pill variant — compact, fully rounded. Used for onboarding empty-state CTAs
   so they read as light actions, not heavy blocks. Every button inside a
   .cfg-empty-act row gets it without asking: those rows are all the same
   "start here" moment, and one of them opting out is what made them look like
   two different components. Placed after .sm on purpose — an empty-state CTA
   written as `btn-x sm` still lands on the pill metrics. */
.btn-x.pill, .cfg-empty-act .btn-x {
  border-radius: 999px; padding: .38rem .95rem; font-size: .8125rem; box-shadow: none;
}
.btn-x.pill:hover, .cfg-empty-act .btn-x:hover { box-shadow: var(--shadow-xs); }
.btn-x.pill.primary, .cfg-empty-act .btn-x.primary { box-shadow: 0 1px 2px rgba(15,23,42,.2); }
.cfg-empty-act .btn-x .bi { font-size: .8rem; }

/* ── Confirm dialog ───────────────────────────────────────── */
.cx-overlay { position: fixed; inset: 0; z-index: 400; display: grid; place-items: center; padding: 1rem;
  background: rgba(15,23,42,.4); opacity: 0; transition: opacity .22s ease;
  -webkit-backdrop-filter: blur(0); backdrop-filter: blur(0); }
.cx-overlay.show { opacity: 1; -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.cx-dialog { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  max-width: 400px; width: 100%; padding: 1.25rem 1.35rem; box-shadow: var(--shadow-xl);
  opacity: 0; transform: translateY(14px) scale(.97);
  transition: transform .32s cubic-bezier(.16,1,.3,1), opacity .22s ease;
  /* tall dialogs (e.g. the yeastar source form) must fit the viewport and let
     the body scroll — title + actions stay pinned. */
  display: flex; flex-direction: column; max-height: calc(100vh - 2rem); }
.cx-overlay.show .cx-dialog { opacity: 1; transform: translateY(0) scale(1); }
@media (prefers-reduced-motion: reduce) {
  .cx-overlay, .cx-dialog { transition-duration: .01ms; }
  .cx-dialog { transform: none; }
}
.cx-dialog-title { font-weight: 700; font-size: 1rem; margin-bottom: .5rem; display: flex; align-items: center; gap: .45rem; flex: 0 0 auto; }
.cx-dialog-msg { font-size: .875rem; color: #475569; line-height: 1.5; margin-bottom: 1.15rem;
  flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.cx-dialog-actions { display: flex; justify-content: flex-end; gap: .5rem; flex: 0 0 auto; }

/* Selectable radio cards inside a form dialog (e.g. project data type) */
.fd-radio-group { display: flex; flex-direction: column; gap: .5rem; }
.fd-radio-card { position: relative; display: block; cursor: pointer; margin: 0; }
.fd-radio-card input { position: absolute; opacity: 0; width: 0; height: 0; }
.fd-radio-body {
  display: flex; align-items: center; gap: .75rem; padding: .7rem .85rem;
  border: 1.5px solid var(--border); border-radius: var(--radius); background: var(--surface);
  transition: border-color var(--transition), background-color var(--transition), box-shadow var(--transition);
}
.fd-radio-card:hover .fd-radio-body { border-color: var(--border-strong); }
.fd-radio-ic {
  width: 38px; height: 38px; flex-shrink: 0; border-radius: 10px; background: #f1f5f9; color: #475569;
  display: grid; place-items: center; font-size: 1.1rem;
  transition: background-color var(--transition), color var(--transition);
}
.fd-radio-text { display: flex; flex-direction: column; min-width: 0; }
.fd-radio-title { font-weight: 600; font-size: .9rem; color: var(--text); line-height: 1.25; }
.fd-radio-desc { font-size: .78rem; color: var(--text-muted); }
.fd-radio-check {
  margin-left: auto; flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%;
  border: 1.5px solid var(--border-strong); color: #fff; display: grid; place-items: center;
  font-size: .8rem; opacity: 0; transform: scale(.6);
  transition: opacity var(--transition), transform var(--transition), background-color var(--transition), border-color var(--transition);
}
.fd-radio-card input:checked + .fd-radio-body {
  border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, var(--surface));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
.fd-radio-card input:checked + .fd-radio-body .fd-radio-ic { background: var(--accent); color: #fff; }
.fd-radio-card input:checked + .fd-radio-body .fd-radio-check {
  opacity: 1; transform: scale(1); background: var(--accent); border-color: var(--accent);
}
.fd-radio-card input:focus-visible + .fd-radio-body { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Pagination ───────────────────────────────────────────── */
.pager { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .85rem 1.15rem; border-top: 1px solid var(--border); font-size: .8125rem; color: var(--text-muted); flex-wrap: wrap; }
.pager-btns { display: flex; gap: .4rem; }

/* ── Call detail layout ───────────────────────────────────── */
/* Overview: two columns that keep their own height. Left is the narrative you
   read — what the call was about, then the moments that evidence it. Right is
   what you scan — what to do next, what was extracted, how the customer felt.
   Panels stack INSIDE a column, so a long left column no longer leaves a hole
   beside a short one. (The previous shape put five panels through two grids —
   a 1.7fr/1fr row, then an auto-fit three-up — which forced Custom fields, Key
   moments and Sentiment into a single row height. Key moments is routinely 3×
   the height of the other two, and the row emptied out beside it.) */
.detail-overview { display: grid; grid-template-columns: 1.7fr 1fr; gap: 1.15rem; margin-top: 1.15rem; align-items: start; }
.ov-col { display: flex; flex-direction: column; gap: 1.15rem; min-width: 0; }
/* inside a column `gap` owns the rhythm — cancel the flow-stack margin */
.ov-col > .panel + .panel { margin-top: 0; }

@media (max-width: 900px) {
  /* One column. The columns dissolve rather than stack, so panels interleave by
     priority instead of by which column they happened to live in: action items
     must not land below a long list of key moments on a phone. */
  .detail-overview { grid-template-columns: 1fr; }
  .ov-col { display: contents; }
  .ov-summary { order: 1; }
  .ov-actions { order: 2; }
  .ov-custom { order: 3; }
  .ov-moments { order: 4; }
  .ov-sentiment { order: 5; }
  .detail-overview > .panel + .panel { margin-top: 0; }
}

/* Transcript/QA tabs still use the plain two-up grid. */
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.15rem; margin-top: 1.15rem; align-items: stretch; }
@media (max-width: 900px) {
  .detail-grid { grid-template-columns: 1fr; align-items: start; }
}

/* transcript bubbles */
.transcript { display: flex; flex-direction: column; gap: .7rem; max-height: 60vh; overflow-y: auto; padding: .25rem; }
/* The transcript owns a whole tab now: the page scrolls it instead of an inner
   box (so a QA quote can jump straight to a line), and the column is measured so
   bubbles don't stretch across a wide screen. */
.transcript.is-full { max-height: none; overflow: visible; max-width: 900px; margin-inline: auto; }
.bubble { max-width: 82%; padding: .6rem .8rem; border-radius: 12px; font-size: .8438rem; line-height: 1.5; position: relative; cursor: pointer; transition: box-shadow var(--transition), transform var(--transition); }
.bubble:hover { box-shadow: 0 2px 10px rgba(15,23,42,.1); transform: translateY(-1px); }
.bubble .b-time { font-size: .68rem; color: var(--text-muted); margin-bottom: .2rem; font-variant-numeric: tabular-nums; }
.bubble.agent { align-self: flex-start; background: #f1f5f9; border: 1px solid var(--border); border-bottom-left-radius: 3px; }
.bubble.customer { align-self: flex-end; background: var(--blue-bg); border: 1px solid #bfdbfe; border-bottom-right-radius: 3px; }
.bubble .b-role { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; opacity: .6; margin-bottom: .15rem; }

/* analysis panels */
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; box-shadow: var(--shadow-sm); }
.panel + .panel { margin-top: 1.15rem; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted); margin-bottom: .6rem; display: flex; align-items: center; gap: .45rem; }
/* sectioned call summary (client-configured topics) */
.summary-sections { display: flex; flex-direction: column; gap: .75rem; }
.sum-sec { padding-left: .7rem; border-left: 2px solid var(--border-strong); }
.sum-sec-label { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted); margin-bottom: .2rem; }
.sum-sec-text { font-size: .875rem; line-height: 1.55; }

.action-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .5rem; }
.action-list li { display: flex; gap: .55rem; align-items: flex-start; font-size: .8438rem; line-height: 1.45; }
.action-list li .bi { color: var(--green); margin-top: .1rem; flex-shrink: 0; }

/* qa ring */
.qa-ring { display: grid; place-items: center; width: 120px; height: 120px; border-radius: 50%; position: relative; margin: 0 auto; }
.qa-ring .qa-ring-inner { position: absolute; inset: 12px; background: #fff; border-radius: 50%; display: grid; place-items: center; }
.qa-ring .qa-num { font-size: 1.7rem; font-weight: 800; letter-spacing: -0.02em; }
.qa-ring .qa-max { font-size: .7rem; color: var(--text-muted); }
.qa-crit { border-top: 1px solid var(--border); padding-top: .7rem; margin-top: .8rem; }
.qa-crit-row { padding: .5rem 0; border-bottom: 1px dashed var(--border); }
.qa-crit-row:last-child { border-bottom: 0; }
.qa-crit-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; font-size: .8125rem; }
.qa-bar { height: 6px; background: var(--slate-bg); border-radius: 999px; margin-top: .4rem; overflow: hidden; }
.qa-bar > span { display: block; height: 100%; border-radius: 999px; background: #0f172a; }
.qa-reason { font-size: .78rem; color: var(--text-muted); margin-top: .35rem; }

/* qa scorecard — full-width: ring header + responsive 4Cs sections grid */
.qa-scorecard { margin-top: 1.15rem; }
.qa-score-head { display: flex; align-items: center; gap: 1.3rem; flex-wrap: wrap; }
.qa-score-head .qa-ring { margin: 0; flex-shrink: 0; }
.qa-score-headmain { display: flex; flex-direction: column; gap: .35rem; min-width: 200px; }
.qa-sections-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.3rem 2rem; margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 1.1rem; }
@media (max-width: 720px) { .qa-sections-grid { grid-template-columns: 1fr; } }
.qa-section-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted);
  padding-bottom: .35rem; margin-bottom: .1rem; border-bottom: 1px solid var(--border); }
.qa-section-pct { font-variant-numeric: tabular-nums; }
.qa-pf { display: inline-flex; align-items: center; gap: .25rem; font-size: .72rem; font-weight: 700;
  padding: .1rem .45rem; border-radius: 999px; white-space: nowrap; }
.qa-pf.ok { background: var(--green-bg, #dcfce7); color: #15803d; }
.qa-pf.bad { background: var(--red-bg, #fee2e2); color: #b91c1c; }
.qa-pf.lg { font-size: .82rem; padding: .25rem .7rem; align-self: flex-start; }
.qa-pf.na { background: var(--muted-bg, #f1f5f9); color: var(--text-muted, #64748b); }
.qa-crit-row.qa-na, .qa-section.qa-na { opacity: .6; }
.qa-critical { font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .02em;
  color: #b45309; background: #fef3c7; padding: .05rem .3rem; border-radius: 4px; vertical-align: middle; }
.qa-ev { display: inline-flex; align-items: flex-start; gap: .3rem; margin-top: .4rem; max-width: 100%;
  font-size: .74rem; line-height: 1.35; text-align: left; color: var(--text-muted);
  background: var(--slate-bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .3rem .5rem; cursor: pointer; transition: all var(--transition); }
.qa-ev:hover { color: #0f172a; border-color: #cbd5e1; background: #f1f5f9; }
.qa-ev .bi { flex-shrink: 0; margin-top: .1rem; opacity: .7; }

/* ── Conversation detail header ──────────────────────────────────────────────
   One block where the page used to open with four stacked bands: title, meta,
   a row of three buttons, and a bordered "insights" card holding one sentence
   under two uppercase labels. Two things went:

   - the button row. Reanalyze / Export / Delete are rare and one is
     destructive, so they collapse into the ⋯ menu the rest of the app already
     uses for row actions (see sources in settings.html). The call's identity
     deserves the width, not its verbs.
   - the card around the lead. A border and two labels around a sentence and
     four tag chips was chrome around content; in the header, directly under
     the numbers, the sentence needs no label to say what it is.

   Meta keeps no max-width: .page-sub clamps to 60ch, which wrapped this line
   in two while the space beside it sat empty. */
.conv-head { padding: .25rem 0 .35rem; }
.conv-head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.conv-actions { display: flex; align-items: center; gap: .4rem; flex-shrink: 0; }
.conv-versions { width: auto; font-size: .78rem; }
.conv-meta { font-size: .8125rem; color: #475569; margin: .4rem 0 0; }

/* The call route, as the h1: (dir) counterparty → who took it. Direction is a
   round icon chip rather than the table's text pill — at title size the word
   competed with the number for the first thing your eye lands on, and the arrow
   says it already (down-left came to us, up-right we placed it, the same
   vocabulary as .dir-badge). The two legs are deliberately unequal: the
   counterparty identifies the call, the internal side only qualifies it. */
.conv-head-row h1 { margin: 0; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; min-width: 0; }
.conv-dir {
  display: inline-grid; place-items: center; flex-shrink: 0;
  width: 30px; height: 30px; border-radius: 50%; font-size: .9rem;
}
.conv-dir.in { background: var(--blue-bg); color: var(--blue); }
.conv-dir.out { background: var(--slate-bg); color: #475569; }
.conv-leg-party { font-size: 1.375rem; font-weight: 700; letter-spacing: -0.01em; }
.conv-leg-inside { font-size: 1rem; font-weight: 600; color: #475569; }
.conv-arrow { font-size: .85rem; color: var(--border-strong); }
.conv-lead { margin-top: .85rem; }
.conv-lead-reason { font-size: 1rem; font-weight: 500; line-height: 1.5; max-width: 82ch; }
.conv-lead .tag-chips { margin-top: .55rem; }
/* The header's bottom padding is tuned for the tab strip that normally follows
   and brings its own margin; a waiting/error panel has none. */
.conv-head + #content > .state-panel { margin-top: 1.15rem; }

.tag-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.tag-chip { display: inline-flex; align-items: center; font-size: .78rem; font-weight: 600;
  padding: .2rem .6rem; border-radius: 999px; background: var(--blue-bg); color: var(--blue); }

/* Custom fields — label above value, not a two-column table. The values are
   whatever the customer configured (a name, three product chips, a number),
   and a fixed label column either starves them or wastes half the panel. */
.cf-list { display: flex; flex-direction: column; gap: .65rem; }
.cf-item + .cf-item { border-top: 1px dashed var(--border); padding-top: .65rem; }
.cf-key { font-size: .72rem; font-weight: 600; letter-spacing: .02em; color: var(--text-muted); margin-bottom: .22rem; }
.cf-val { font-size: .875rem; line-height: 1.5; display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
.cf-val.is-num { font-variant-numeric: tabular-nums; font-weight: 600; }

/* key moments */
.moments-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .55rem; }
.moments-list li { display: flex; flex-direction: column; gap: .25rem; padding-bottom: .5rem;
  border-bottom: 1px dashed var(--border); }
.moments-list li:last-child { border-bottom: 0; padding-bottom: 0; }
.moment-label { font-size: .85rem; font-weight: 600; }

/* QA scorecard — section chips as the glance summary above the per-criterion
   grid. The grid used to sit inside a <details> to keep the one-column page
   short; the scorecard has its own tab now, so it opens flat. */
.qa-section-chips { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem;
  border-top: 1px solid var(--border); padding-top: 1rem; }
.qa-sec-chip { display: inline-flex; align-items: center; gap: .4rem; font-size: .8rem;
  padding: .28rem .7rem; border-radius: 999px; background: var(--slate-bg); }
.qa-sec-chip b { font-variant-numeric: tabular-nums; }

/* sentiment gauge */
.gauge { height: 8px; border-radius: 999px; background: linear-gradient(90deg, #ef4444, #f59e0b, #22c55e); position: relative; margin: 1rem 0 .5rem; }
.gauge .needle { position: absolute; top: -4px; width: 3px; height: 16px; background: #0f172a; border-radius: 2px; transform: translateX(-50%); }
.gauge-scale { display: flex; justify-content: space-between; font-size: .7rem; color: var(--text-muted); }

/* audio bar */
.audio-box { background: #f8fafc; border: 1px solid var(--border); border-radius: var(--radius); padding: .85rem 1rem; }
#waveform { margin-bottom: .6rem; }
.audio-controls { display: flex; align-items: center; gap: .7rem; }
.play-btn { width: 40px; height: 40px; border-radius: 50%; border: 0; background: #0f172a; color: #fff; display: grid; place-items: center; font-size: 1rem; cursor: pointer; flex-shrink: 0; }
.audio-time { font-size: .78rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* transcript-first (no audio) — non-clickable bubbles */
.bubble.no-audio { cursor: default; }
.bubble.no-audio:hover { box-shadow: none; transform: none; }
.bubble.no-audio .b-time { opacity: .75; }
/* evidence jump target flash */
.bubble.flash { animation: bubbleFlash 1.6s ease; }
@keyframes bubbleFlash { 0%, 30% { box-shadow: 0 0 0 2px var(--amber, #f59e0b); } 100% { box-shadow: 0 0 0 2px transparent; } }

/* ── Upload dropzone ──────────────────────────────────────── */
.dropzone { border: 2px dashed #cbd5e1; border-radius: var(--radius); padding: 2rem 1.5rem; text-align: center; transition: all var(--transition); cursor: pointer; background: #fafbfc; }
.dropzone.drag { border-color: #0f172a; background: #f1f5f9; }
.dropzone .bi { font-size: 1.8rem; color: #94a3b8; display: block; margin-bottom: .5rem; }
.upload-row { display: flex; align-items: center; gap: .7rem; padding: .6rem .8rem; border: 1px solid var(--border); border-radius: var(--radius-sm); margin-top: .6rem; font-size: .8125rem; }
.upload-progress { flex: 1; height: 6px; background: var(--slate-bg); border-radius: 999px; overflow: hidden; }
.upload-progress > span { display: block; height: 100%; background: #0f172a; transition: width .3s ease; }

/* ── Charts ───────────────────────────────────────────────── */
.chart-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 1rem; }
/* Grid items default to min-width:auto, so a Chart.js canvas (which carries an
   inline pixel width) would pin the columns at their widest size — the pane then
   can't shrink back when the sidebar re-expands and the whole page overflows.
   min-width:0 + an out-of-flow canvas let the box shrink; Chart.js's
   ResizeObserver on .chart-box then redraws at the new size. */
.chart-grid > * { min-width: 0; }
@media (max-width: 860px) { .chart-grid { grid-template-columns: 1fr; } }
.chart-box { position: relative; height: 240px; min-width: 0; }
.chart-box > canvas { position: absolute; inset: 0; }
.chart-box.sm { height: 190px; }
.chart-box.lg { height: 260px; }

/* ── Dashboard ────────────────────────────────────────────────────────────────
   The dashboard does NOT use .stat-card / .card-x. Four equal bordered tiles over
   two rows of equal bordered chart boxes gave every number on the page the same
   weight, so the page had no reading order and every panel competed for the eye.
   What replaces it is a hierarchy in three steps:

     1. .db-lead    — ONE hero figure (today's volume) with its own 30-day chart
                      inside the same panel. The headline number and the shape it
                      came from belong together; split across two cards the reader
                      has to join them up.
     2. .db-metrics — the four secondary numbers as one divided strip, not four
                      floating cards. Hairlines instead of four borders: they are
                      one row of related facts, and the shared surface says so.
     3. .db-row     — three supporting panels, deliberately the quietest things
                      here.

   Colour budget for the whole page: ONE accent (--brand) on the data marks, and
   semantic green/red as *text* on a movement or a state — never as a filled chip
   or a tinted icon tile. Everything structural is ink, gray and hairlines. The
   only place four colours appear at once is the sentiment bar, where polarity is
   the actual subject. */
/* ── Segmented control ────────────────────────────────────────────────────────
   The dashboard's date range. One control above everything it scopes — never a
   filter per panel, or two panels end up showing different windows and looking
   like they disagree.

   The dark marker is ONE element that slides, not a background swapped on each
   button: sliding is what makes the control read as "the same selection moving"
   rather than "two separate things blinking", and it is a single transform so it
   stays cheap. Its geometry is measured in JS (see `moveThumb`) because the
   buttons are text-width, and honest label widths beat equal columns here.

   `inset` ring instead of `border`: with a real border, offsetLeft is measured
   from the border box while the marker's `left:0` starts at the padding box, and
   the marker lands 1px off. */
.seg {
  position: relative; display: inline-flex; padding: 4px; gap: 2px;
  background: var(--surface); border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--border), var(--shadow-xs);
}
.seg-thumb {
  position: absolute; top: 4px; bottom: 4px; left: 0; width: 0;
  border-radius: 999px; background: var(--ink); pointer-events: none;
}
/* Enabled only once the marker has been placed, so it doesn't slide in from the
   left edge on first paint. */
.seg.is-ready .seg-thumb {
  transition: transform .28s cubic-bezier(.4, 0, .2, 1), width .28s cubic-bezier(.4, 0, .2, 1);
}
.seg-btn {
  position: relative; z-index: 1; border: 0; background: transparent;
  padding: .4rem .85rem; border-radius: 999px;
  font: inherit; font-size: .8125rem; font-weight: 600; line-height: 1.4;
  color: var(--text-muted); cursor: pointer; white-space: nowrap;
  transition: color var(--transition);
}
.seg-btn:hover { color: var(--text); }
.seg-btn[aria-pressed="true"] { color: #fff; }
.seg-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The marker is decoration; the label colour is what states the selection, so it
   must not animate away from a reader who asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .seg.is-ready .seg-thumb { transition: none; }
}

.db-stack { display: flex; flex-direction: column; gap: .9rem; }
/* Refetching holds the previous render, dimmed, instead of tearing it down for a
   skeleton: the panels keep their size so nothing on the page jumps, and the
   numbers you were reading stay readable while the new ones arrive. */
.db-stack.is-busy { opacity: .5; transition: opacity .16s ease; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .db-stack.is-busy { transition: none; } }

.db-panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); min-width: 0;
}
/* No hairline under the title: the panel border already closes the box, and a
   second rule 40px inside it chopped every card into two. */
.db-panel-head { padding: 1.15rem 1.3rem .2rem; }
.db-panel-head h2 { font-size: .875rem; font-weight: 600; letter-spacing: -0.01em; margin: 0; }
.db-panel-head p { margin: .25rem 0 0; font-size: .78rem; color: var(--text-muted); }
.db-panel-body { padding: 1rem 1.3rem 1.3rem; }

/* Micro label — the same one on the hero, the strip and the panels, so all four
   levels of the page are visibly one family. */
.db-label { display: flex; align-items: center; gap: .45rem; font-size: .6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); }

/* The lead panel's summary band. It carries a faint plane and closes with a rule,
   which is NOT the hairline-under-a-title that .db-panel-head deliberately drops:
   that one cut a 40px sliver off the top of a card for no reason, this one divides
   two substantial zones — the numbers you read and the chart you scan. Without it
   four figures floated in the same white as the plot and the panel had no
   structure at all, which is the whole reason it read as flat.
   Rounded to match the panel it caps, or the tint squares off its top corners. */
/* `flex-start`, not `stretch`. Stretched, the facts group inherited the hero
   block's height, so its cell rules ran a good 15px past the bottom of "981" into
   empty band. At flex-start the group is only as tall as its own content — the
   rules end where the numbers do — and every label in the row still shares one
   top edge with the hero's. (`.db-facts` keeps `stretch` internally, so the three
   cells match each other however tall "Busiest day" gets.) */
.db-lead-top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: clamp(1rem, 2.5vw, 1.75rem); flex-wrap: wrap;
  padding: 1.25rem 1.3rem;
  background: #fbfcfe; border-bottom: 1px solid var(--divider);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.db-hero-block { min-width: 0; }
/* Delta on the hero's baseline rather than on its own line below: stacked, it left
   the tallest thing on the page with a third short line hanging off it. */
.db-hero-row { display: flex; align-items: baseline; gap: .85rem; flex-wrap: wrap; margin-top: .45rem; }
/* Proportional figures (tabular-nums would make a number like 121 look loose at
   this size). Exactly one hero per view. */
.db-hero { font-size: clamp(2.5rem, 6vw, 3.25rem); font-weight: 700; letter-spacing: -0.04em; line-height: 1; color: var(--ink); }
.db-lead-meta { font-size: .8125rem; color: var(--text-muted); display: inline-flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
/* The right-hand block of the hero row: three facts about the same series, so the
   row has two masses instead of one big number and one stranded small one.
   Divided by hairlines, the same device the metric strip uses — whitespace alone
   left them reading as three orphans rather than one group, and the first rule
   also does the work of separating the group from the hero.
   A step lighter than the hero in BOTH size and ink: four figures in the same
   near-black gave the row no reading order, which was most of why it looked
   flat. */
.db-facts { display: flex; align-items: stretch; justify-content: flex-end; flex-wrap: wrap; }
/* Right-aligned, because a cell is only as wide as its widest line — always the
   label — and left-aligning left every value stranded at the left edge of a box
   sized by something else ("981" under a 130px "LAST 30 DAYS"). Sharing a right
   edge locks each value to its own label, and the group ends flush with the
   panel. */
.db-fact { min-width: 0; text-align: right; padding: 0 clamp(.9rem, 2vw, 1.6rem); border-left: 1px solid var(--divider); }
.db-fact:last-child { padding-right: 0; }
/* .db-label is a flex row (it carries the hero's series mark), so text-align on
   the cell can't move it — the flex axis has to. */
.db-fact .db-label { justify-content: flex-end; }
.db-fact .v { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.025em; line-height: 1; margin-top: .55rem; color: #1e293b; }
.db-fact-sub { margin-top: .35rem; font-size: .75rem; color: var(--text-muted); }
/* Once it has wrapped under the hero it is no longer the right-hand side of
   anything: pushed right it detaches from the column it now sits in, and its
   leading rule divides nothing. */
@media (max-width: 720px) {
  .db-facts { justify-content: flex-start; }
  .db-fact { text-align: left; }
  .db-fact .db-label { justify-content: flex-start; }
  .db-fact:first-child { padding-left: 0; border-left: 0; }
}

/* Movement / state, as text with a caret. Not a filled pill: four coloured pills
   in a row read as buttons and spend the page's whole colour budget on four
   numbers that are already the smallest thing in their card. */
.db-delta { display: inline-flex; align-items: center; gap: .05rem; font-size: .8125rem; font-weight: 700; }
.db-delta.up { color: #15803d; }
.db-delta.down { color: #b91c1c; }
.db-delta.flat { color: var(--text-muted); }
.db-delta .bi { font-size: 1rem; }

/* The secondary numbers: one surface, vertical hairlines. */
.db-metrics { display: grid; grid-template-columns: repeat(4, 1fr); }
.db-metric { padding: 1.15rem 1.3rem; border-left: 1px solid var(--divider); min-width: 0; }
.db-metric:first-child { border-left: 0; }
/* One step below the hero and on the same ink as the facts beside it, so the page
   has three legible levels rather than one near-black for every figure. */
.db-metric .v { font-size: 1.7rem; font-weight: 700; letter-spacing: -0.03em; line-height: 1; margin-top: .65rem; color: #1e293b; }
.db-metric .v small { font-size: .8rem; font-weight: 600; color: var(--text-muted); letter-spacing: 0; }
.db-metric .s { margin-top: .55rem; font-size: .78rem; color: var(--text-muted); display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; min-height: 1.25rem; }
/* Reflowing has to move the dividers too, or a wrapped strip grows hairlines
   that separate nothing and loses the ones that did. */
@media (max-width: 940px) {
  .db-metrics { grid-template-columns: repeat(2, 1fr); }
  .db-metric:nth-child(odd) { border-left: 0; }
  .db-metric:nth-child(n+3) { border-top: 1px solid var(--divider); }
}
@media (max-width: 520px) {
  .db-metrics { grid-template-columns: 1fr; }
  .db-metric { border-left: 0; }
  .db-metric + .db-metric { border-top: 1px solid var(--divider); }
}

/* Chart key — the legend for the lead chart's two series, as marks that look like
   what they label (a bar and a line) rather than two identical dots. The line
   swatch carries the trend's own wash under it, so the key explains the shaded
   band too without needing a third entry. */
.db-key { display: flex; justify-content: flex-end; gap: 1.1rem; margin-bottom: .35rem; font-size: .75rem; color: var(--text-muted); }
.db-key span { display: inline-flex; align-items: center; gap: .4rem; }
/* The two marks are their own display:inline-block — the hero's label reuses
   .db-key-bar outside .db-key, where an <i> would otherwise stay inline and drop
   the width/height. */
.db-key-bar, .db-key-line { display: inline-block; flex-shrink: 0; }
.db-key-bar { width: 5px; height: 12px; border-radius: 1px; background: var(--brand); }
.db-key-line { width: 14px; height: 8px; border-top: 2px solid #1e3a8a; background: rgba(37,99,235,.14); }

.db-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: .9rem; }
.db-row > * { min-width: 0; }
@media (max-width: 1180px) { .db-row { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 860px)  { .db-row { grid-template-columns: 1fr; } }

/* ── Ranked bar list ──────────────────────────────────────────────────────────
   "Top N of a nominal dimension" (topics), as HTML rather than a chart.
   It replaced a doughnut, for three reasons that are all about the data:
   the categories are customer-defined so their count is unbounded (the doughnut
   cycled its 8-hue palette and started repeating colours); rank is what the
   reader wants and angles are the worst encoding for comparing near-equal
   values; and a list can carry the count AND the share as direct labels, so no
   value is trapped in a tooltip.
   One hue for every bar on purpose — the categories have no order of their own,
   so colouring them by value would re-encode bar length in the only channel that
   could have carried identity. */
.rank-list { display: flex; flex-direction: column; gap: .8rem; }
.rank-row { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: .35rem .6rem; border-radius: var(--radius-xs); }
a.rank-row { text-decoration: none; color: inherit; padding: .15rem .3rem; margin: -.15rem -.3rem; transition: background-color var(--transition); }
a.rank-row:hover { background: var(--nav-hover-bg); }
a.rank-row:hover .rank-name { color: var(--brand); }
a.rank-row:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.rank-name { font-size: .8125rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rank-val { font-size: .8125rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.rank-val small { font-weight: 500; color: var(--text-muted); margin-left: .35rem; }
.rank-track { grid-column: 1 / -1; height: 7px; border-radius: 999px; background: var(--slate-bg); }
.rank-fill { display: block; height: 100%; border-radius: 999px; background: var(--brand); min-width: 3px; }
/* The tail bucket is not a topic — it must not look like the biggest one. */
.rank-row.is-other .rank-name { color: var(--text-muted); }
.rank-row.is-other .rank-fill { background: #94a3b8; }

/* ── Proportion bar + labelled rows ───────────────────────────────────────────
   Part-to-whole on an ORDERED scale (sentiment: positive → neutral → mixed →
   negative). Deliberately a bar and not a doughnut: on a ring the first and last
   segments touch, which would put the green and the red next to each other —
   the one adjacency that collapses under red-green colour blindness (ΔE 5.0
   simulated deuteranopia, well under the ΔE 8 floor). Laid out flat they sit at
   opposite ends and never meet, and every remaining neighbour clears the floor.
   Each row names its segment in words, so the colour is never the only thing
   saying which is which — and a 9px dot rather than a coloured glyph, to keep the
   tinted area on this page down to the bar itself. */
.prop-bar { display: flex; gap: 2px; height: 10px; }
.prop-seg { min-width: 3px; border-radius: 3px; }
.prop-legend { display: flex; flex-direction: column; margin-top: 1.1rem; }
.prop-item { display: flex; align-items: center; gap: .6rem; padding: .42rem .3rem; font-size: .8125rem; border-radius: var(--radius-xs); text-decoration: none; color: inherit; }
a.prop-item { transition: background-color var(--transition); }
a.prop-item:hover { background: var(--nav-hover-bg); }
a.prop-item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.prop-dot { flex-shrink: 0; width: 9px; height: 9px; border-radius: 999px; }
.prop-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prop-count { font-weight: 700; font-variant-numeric: tabular-nums; }
.prop-pct { color: var(--text-muted); font-variant-numeric: tabular-nums; width: 3rem; text-align: right; }

/* Panel-sized empty note — for a panel whose data just isn't there yet, where a
   full .state-panel would be louder than the panel it sits in. */
.db-note { display: flex; align-items: center; justify-content: center; gap: .45rem; min-height: 120px; font-size: .8125rem; color: var(--text-muted); text-align: center; }

/* ── Empty / state panels ─────────────────────────────────── */
.state-panel { background: var(--surface); border: 1px dashed #cbd5e1; border-radius: var(--radius); text-align: center; padding: 3rem 1.5rem; }
.state-panel .bi { font-size: 1.9rem; color: #94a3b8; display: block; margin-bottom: .6rem; }
.state-panel .state-title { font-weight: 600; margin-bottom: .25rem; }
.state-panel .state-text { font-size: .8125rem; color: var(--text-muted); margin: 0 auto; max-width: 42ch; }
.state-panel .state-actions { display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; margin-top: 1.15rem; }
.state-panel .state-actions:empty { display: none; }

/* ── In-page section tabs (`.dtabs`) ─────────────────────────────────────────
   Settings and Defaults dropped their tab strip when those sections became the
   rail's submenu (.nav-sub); this is the "if a page ever needs in-page tabs
   again" case the old note left room for. Conversation detail is four readings
   of one call (insight, summary, transcript, QA scorecard) and as one column the
   fourth was three screens down.

   The standing rule from that note holds: these must not read as navigation.
   The rail owns blue — pill background, left marker, brand text. The strip owns
   ink — a 2px underline and nothing else, no fill. It sticks under the topbar so
   switching sections never means scrolling back up a long transcript, and each
   tab carries its own count/score so a hidden section still reports itself. */
.dtabs {
  position: sticky; top: var(--topbar-h); z-index: 20;
  display: flex; gap: .15rem; margin: 1.15rem 0 0;
  background: var(--bg); border-bottom: 1px solid var(--border);
  overflow-x: auto; scrollbar-width: none;
  /* Bleed to the content pane's edges: without it, content scrolling under the
     strip shows through the pane's horizontal padding. */
  margin-inline: calc(-1 * var(--content-pad)); padding-inline: var(--content-pad);
}
.dtabs::-webkit-scrollbar { display: none; }
.dtab {
  appearance: none; background: none; border: 0; border-bottom: 2px solid transparent;
  display: inline-flex; align-items: center; gap: .45rem; white-space: nowrap;
  padding: .65rem .8rem; margin-bottom: -1px; cursor: pointer;
  font: inherit; font-size: .8438rem; font-weight: 600; color: var(--text-muted);
  transition: color var(--transition), border-color var(--transition);
}
.dtab .bi { font-size: .9rem; opacity: .7; }
.dtab:hover { color: var(--ink); border-bottom-color: var(--border-strong); }
.dtab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); }
.dtab[aria-selected="true"] .bi { opacity: 1; }
/* The count is what makes a closed section legible from the strip. */
.dtab-count {
  font-size: .7rem; font-weight: 700; font-variant-numeric: tabular-nums;
  padding: .05rem .4rem; border-radius: 999px;
  background: var(--slate-bg); color: var(--text-muted);
}
.dtab[aria-selected="true"] .dtab-count { background: #e2e8f0; color: var(--ink); }
.dtab-panel { padding-top: 1.15rem; }
/* Panels open with their own top margin (.panel / .detail-grid / .qa-scorecard);
   the strip already spaced them. */
.dtab-panel > :first-child { margin-top: 0; }

.banner { display: flex; gap: .6rem; align-items: flex-start; padding: .75rem .9rem; border-radius: var(--radius-sm); font-size: .8125rem; line-height: 1.45; }
.banner.warn { background: var(--amber-bg); color: #92400e; border: 1px solid #fde68a; }
.banner.err { background: var(--red-bg, #fee2e2); color: #991b1b; border: 1px solid #fecaca; }
/* Neutral/informational variant — used by the settings scope strip (workspace
   default vs project-specific), which states context rather than warning. */
.banner.info { background: var(--blue-bg); color: #1e40af; border: 1px solid #bfdbfe; align-items: center; }
.banner .bi { flex-shrink: 0; margin-top: .1rem; }

/* ── Settings help affordances ────────────────────────────────
   The config editors (rubric / summary / custom fields / topics) used to stack
   up to three tinted banners and a prose card around one table, which read as
   noise and buried the thing being edited. They now spend at most one line each
   on three quiet primitives:
     .scope-row    — a state chip (Inherited / Custom) + the one move
     .card-x-foot  — the one operational caveat ("applies to new calls")
     .help-note    — the rest of the prose, collapsed
   Colour is reserved for STATE, never for prose. */

/* No box of its own: the chip carries the colour and the row carries nothing,
   so the card below stays the loudest thing on the page. */
.scope-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
.scope-row .spacer { flex: 1 1 auto; }
.scope-row .badge-x { padding: .28rem .6rem; }
.scope-row .badge-x .bi { font-size: .72rem; }

/* Footnote row inside a card, under its table. */
.card-x-foot {
  display: flex; align-items: center; gap: .4rem;
  padding: .55rem 1.15rem; border-top: 1px solid var(--border);
  font-size: .78rem; color: var(--text-muted);
}

/* Two cards side by side (Reports: the lowest- and highest-scored calls).
   `align-items: start` is the other half of the fix above: stretched, the shorter
   of the two pads itself out to the taller one's height, and that padding is card
   surface below the pager — a white strip with nothing in it. Each card ends
   where its own table does. */
.rp-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; }
@media (max-width: 767.98px) { .rp-split { grid-template-columns: 1fr; } }

/* Footnote BELOW a card, not inside it (Reports). `.card-x-foot` above is a row
   of the card's own chrome and shares its fill — under a table card that fill is
   black, and a paragraph of prose sitting in the same band as the pager reads as
   another control rather than as an aside. This is the aside: page ground, muted
   voice, hugging the card it belongs to.

   The negative margin only cancels the stack's flex gap, so the note ends up the
   same short distance below its card wherever it is used; the plain margin is
   what applies outside a stack. */
.rp-note {
  display: flex; align-items: flex-start; gap: .4rem;
  margin: .45rem 0 0; padding: 0 .15rem; max-width: 92ch;
  font-size: .78rem; line-height: 1.55; color: var(--text-muted);
}
.db-stack > .rp-note { margin-top: -.45rem; }
.rp-note > .bi { flex: none; margin-top: .15rem; font-size: .8rem; opacity: .75; }

/* Collapsed prose — closed by default: the table is the content, this is the
   footnote someone opens once and never again. */
.help-note { margin-top: 1rem; font-size: .8125rem; }
.help-note > summary {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .2rem 0; cursor: pointer; list-style: none;
  color: var(--text-muted); font-weight: 500;
}
.help-note > summary::-webkit-details-marker { display: none; }
.help-note > summary:hover { color: var(--text); }
.help-note > summary .bi { font-size: .7rem; transition: transform var(--transition); }
.help-note[open] > summary .bi { transform: rotate(90deg); }
.help-note .help-body {
  padding: .35rem 0 0 1.05rem; max-width: 78ch;
  color: var(--text-muted); line-height: 1.55;
}
.help-note .help-body p { margin: 0 0 .4rem; }
.help-note .help-body p:last-child { margin-bottom: 0; }

/* ── Config editors (Settings / Defaults) ─────────────────────────────────────
   These four panels are list editors, not data grids: every cell is an input and
   nothing in a row is clickable. `.tx` on its own says the opposite (pointer
   cursor, hover tint), so the editors take `.tx-edit` on top of it, which trades
   the row-hover affordance for a focus-within tint — feedback about where the
   caret is instead of a click that does nothing. */
table.tx.tx-edit tbody tr { cursor: default; transition: background-color var(--transition); }
/* Stays on the row fill instead of going transparent — transparent used to mean
   "the card's white", which since the rows are tinted would read as a highlight,
   i.e. exactly the affordance this rule exists to remove. */
table.tx.tx-edit tbody tr:hover { background: var(--tbl-row-bg); }
table.tx.tx-edit tbody tr:focus-within { background: var(--tbl-row-hover); }
table.tx.tx-edit thead th,
table.tx.tx-edit tbody td { padding: .5rem .45rem; }
/* Line the outer columns up with the card head above them. */
table.tx.tx-edit thead th:first-child,
table.tx.tx-edit tbody td:first-child { padding-left: 1.15rem; }
table.tx.tx-edit thead th:last-child,
table.tx.tx-edit tbody td:last-child { padding-right: 1.15rem; }
table.tx.tx-edit td.col-toggle, table.tx.tx-edit th.col-toggle { width: 1%; white-space: nowrap; text-align: center; }
table.tx.tx-edit td.col-act, table.tx.tx-edit th.col-act { width: 1%; white-space: nowrap; }

/* Table-card chrome — the Conversations grid's palette on a card: black command
   bars top and bottom framing a quiet grey column header and a white row surface.

   Scoped to the pages that opt in, and it has to stay that way even though the
   block classes below would read as opt-in enough on their own: config-tabs.js
   renders the same `.settings-table-card` markup on BOTH Settings and Workspace
   Defaults, and Defaults deliberately keeps the plain light treatment. So the
   opt-in is the page. Reports joins it here — its tables already ship with these
   class names, and until they were in scope the page rendered as flat white
   cards and read as a different product from the grid it reports on.

   The ONE thing that stays scoped to Settings alone is the row hover, below.
   There a row is an editing surface and lighting it up on the way to a field is
   noise; on Reports a row is a link, and hover is what says so. */
:root {
  --settings-table-black: var(--dt-black);
}
:is(.settings-page, .reports-page) :is(.settings-table-card, .settings-table-dialog, .settings-table) {
  --tbl-row-bg: #fff;
  --tbl-row-hover: #f8fafc;
  --tbl-head-bg: #eef2f6;
  --tbl-border: #dbe1e8;
}
:is(.settings-page, .reports-page) .settings-table-card {
  overflow: hidden;
  border-color: #dbe1e8;
}
:is(.settings-page, .reports-page) .settings-table-card > .card-x-head,
:is(.settings-page, .reports-page) .settings-table-bar {
  background: var(--settings-table-black);
  color: #fff;
  border-color: var(--chrome-border);
}
:is(.settings-page, .reports-page) .settings-table-card > .card-x-head .muted,
:is(.settings-page, .reports-page) .settings-table-card > .card-x-head > div > .bi {
  color: var(--chrome-ink-dim) !important;
}
:is(.settings-page, .reports-page) .settings-table-card > .card-x-head .badge-x {
  border-color: #fff;
  background: #fff;
  color: #111827;
}
:is(.settings-page, .reports-page) .settings-table-card > .card-x-head .btn-x,
:is(.settings-page, .reports-page) .settings-table-bar .btn-x,
:is(.settings-page, .reports-page) .settings-table-foot .btn-x {
  border-color: #fff;
  background: #fff;
  color: #111827;
  box-shadow: none;
}
:is(.settings-page, .reports-page) .settings-table-card > .card-x-head .btn-x:hover,
:is(.settings-page, .reports-page) .settings-table-bar .btn-x:hover,
:is(.settings-page, .reports-page) .settings-table-foot .btn-x:hover {
  border-color: #e2e8f0;
  background: #f1f5f9;
  color: #050505;
}
:is(.settings-page, .reports-page) .settings-table table.tx thead th {
  background: #eef2f6;
  color: #475569;
}
:is(.settings-page, .reports-page) .settings-table table.tx tbody tr {
  background: #fff;
}
:is(.settings-page, .reports-page) .settings-table table.tx tbody tr:focus-within {
  background: #f8fafc;
}
/* Spelled out rather than left to `table.tx tbody tr:hover` further up: the rule
   above pins the row fill at a higher specificity than that one, so without this
   the opt-in pages would have no hover at all. */
:is(.settings-page, .reports-page) .settings-table table.tx tbody tr:hover {
  background: #f8fafc;
}
/* Settings only: an edit row is not a link, so it does not light up under the
   pointer. Reports keeps the grid's hover, because there a row does open. */
.settings-page .settings-table table.tx tbody tr:hover,
.settings-page .settings-table table.tx.tx-edit tbody tr:hover {
  background: #fff;
}
:is(.settings-page, .reports-page) .settings-table-card > .card-x-foot,
:is(.settings-page, .reports-page) .settings-table-foot,
:is(.settings-page, .reports-page) .settings-table-dialog > .cx-dialog-actions {
  background: var(--settings-table-black);
  color: #fff;
  border-color: var(--chrome-border);
}
:is(.settings-page, .reports-page) .settings-table-card > .card-x-foot {
  color: #fff;
}
:is(.settings-page, .reports-page) .settings-table-dialog {
  overflow: hidden;
}
:is(.settings-page, .reports-page) .settings-table-dialog > .cx-dialog-msg {
  padding-block: .8rem;
}
/* The pager between the table and that black foot is the Conversations pager
   itself (`.dt-pager`), unmodified — which is the point: one pager, one look,
   wherever a table pages. */

/* A row's own error, under the field that caused it — the message names the
   field it belongs to instead of floating away in a toast. */
.field-err { display: block; margin-top: .28rem; font-size: .72rem; color: var(--red); font-weight: 500; }
.form-control.is-bad, .form-select.is-bad { border-color: var(--red); background: #fffafa; }
.form-control.is-bad:focus, .form-select.is-bad:focus { border-color: var(--red); box-shadow: 0 0 0 3px rgba(220,38,38,.13); }

/* Same rejection, for a group that isn't one input: a checkbox list ("pick at
   least one extension"), a set of radios. `!important` is deliberate — these
   boxes carry their border as an inline style in the dialog markup, and a
   rejection that can't outrank it would silently show no border at all. */
.is-bad-box { border: 1px solid var(--red) !important; background: #fffafa; border-radius: 7px; }

/* Quiet square icon button. Row-level destructive actions use this instead of a
   red-outlined button: a delete control repeated down every row is chrome, and a
   red one competes with the content for attention until it's actually hovered. */
.btn-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0; flex-shrink: 0;
  border: 1px solid transparent; border-radius: var(--radius-xs);
  background: transparent; color: var(--text-muted); cursor: pointer;
  transition: background-color var(--transition), color var(--transition), border-color var(--transition), opacity var(--transition);
}
.btn-icon:hover { background: var(--slate-bg); color: var(--text); }
.btn-icon.danger:hover { background: var(--red-bg); color: var(--red); border-color: #fecaca; }
.btn-icon:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; opacity: 1; }
/* Recede until the row is in play — never to invisible, which would hide it from
   anyone scanning rather than pointing. */
tr .btn-icon { opacity: .45; }
tr:hover .btn-icon, tr:focus-within .btn-icon { opacity: 1; }

/* The "On" column. One switch vocabulary across the whole app — Sources already
   used Bootstrap's, so the editors adopt it rather than inventing a second. */
.sw { display: inline-flex; align-items: center; justify-content: center; margin: 0; min-height: 0; padding-left: 0; }
.sw .form-check-input { margin: 0; float: none; cursor: pointer; }
.sw .form-check-input:disabled { cursor: not-allowed; }

/* Compact empty state inside a card — teaches the control that fills it. */
.cfg-empty { padding: 2.4rem 1.15rem; text-align: center; }
/* Direct child only: this is the big grey glyph above the title. Unscoped, it
   also caught the icon inside the CTA below — 1.5rem and display:block turned
   a small button into a tall block with a grey square in it. */
.cfg-empty > .bi { font-size: 1.5rem; color: var(--border-strong); display: block; margin-bottom: .55rem; }
.cfg-empty .cfg-empty-title { font-weight: 600; font-size: .875rem; margin-bottom: .2rem; }
.cfg-empty .cfg-empty-text { font-size: .8125rem; color: var(--text-muted); max-width: 46ch; margin: 0 auto; }
.cfg-empty .cfg-empty-act {
  margin-top: .9rem;
  display: flex; gap: .45rem; flex-wrap: wrap; justify-content: center;
}

/* Unsaved-changes bar. Sticky rather than fixed so it rides the content column
   and needs no sidebar arithmetic; ink rather than a card so it reads as a layer
   over the editor instead of one more panel in it. Rendered last in the panel and
   only while there is something to save — a Save button with nothing to save is
   the reason people stop trusting one. */
.cfg-bar {
  position: sticky; bottom: 1rem; z-index: 50;
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-top: 1rem; padding: .55rem .6rem .55rem 1rem;
  background: var(--ink); color: #fff;
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  animation: cfgBarIn .22s cubic-bezier(.16, 1, .3, 1);
}
.cfg-bar-msg { display: inline-flex; align-items: center; gap: .45rem; font-size: .8125rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.cfg-bar-msg .bi { color: #94a3b8; }
.cfg-bar .spacer { flex: 1 1 auto; }
.cfg-bar .btn-x { background: rgba(255,255,255,.1); border-color: rgba(255,255,255,.2); color: #fff; box-shadow: none; }
.cfg-bar .btn-x:hover { background: rgba(255,255,255,.2); border-color: rgba(255,255,255,.34); color: #fff; }
.cfg-bar .btn-x.primary { background: #fff; border-color: #fff; color: var(--ink); }
.cfg-bar .btn-x.primary:hover { background: #e2e8f0; border-color: #e2e8f0; color: var(--ink); }
.cfg-bar kbd {
  font: inherit; font-size: .7rem; padding: .05rem .3rem; border-radius: 4px;
  background: rgba(255,255,255,.12); color: #cbd5e1; border: 1px solid rgba(255,255,255,.16);
}
@keyframes cfgBarIn { from { opacity: 0; transform: translateY(10px); } }

/* ── Sources & endpoints (Settings) ──────────────────────────────────────────
   A source is one row inside a single list card, not a card of its own: three
   sources used to be three floating panels, each with a grey strip stapled
   underneath holding one long middot sentence and a stray button. That read as
   three unrelated widgets. Now it is one card with hairline-divided rows.

   The row is split by WHAT CHANGES, not by what it is called. Left of centre is
   configuration — the name, the connector, what it is set to pull — and it only
   ever changes when someone edits it. Right of centre is live: the controls that
   act on the source, and under them one line of health (last call, volume today,
   anything queued or failed). Reading down the list the eye tracks two columns,
   name and health, rather than a four-line paragraph per source.

   Every step of type here is a real step: the name at .9375/600, the connector
   and config at .78 muted, the health line at .75 with a colour-carrying dot.
   Three sizes, not one repeated four times, which is what made the old row read
   as a wall. */
.src-head { display: flex; align-items: center; gap: .85rem; padding: .85rem 1.15rem; }
.src-list > .src-row + .src-row { border-top: 1px solid var(--border); }
/* The identity column keeps a floor so a row with four controls (Move, Edit,
   Poll, delete) squeezes the CONTROLS into wrapping, not the source's name. */
.src-row {
  display: grid; grid-template-columns: auto minmax(12rem, 1fr) auto;
  align-items: center; gap: .15rem 1.1rem; padding: 1rem 1.15rem;
}
.src-glyph {
  width: 38px; height: 38px; flex-shrink: 0; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--slate-bg); color: #475569; font-size: 1.05rem;
}
/* Paused, not gone — the row stays readable, it just stops claiming attention. */
.src-row.is-off .src-glyph { opacity: .5; }
.src-row.is-off .src-name { color: var(--text-muted); }
.src-id { min-width: 0; }
.src-name {
  font-size: .9375rem; font-weight: 600; line-height: 1.35;
  display: flex; align-items: baseline; gap: .45rem; flex-wrap: wrap;
}
/* The connector, sitting with the name rather than heading its own line: it
   qualifies the name ("ALL, the Yeastar one") and is never the thing you scan
   for. Baseline-aligned so it hangs off the name instead of floating. */
.src-type { font-size: .78rem; font-weight: 400; color: var(--text-muted); }
.src-name .badge-x { align-self: center; }
/* What this source is set to pull. Muted and directly under the name: it is the
   answer to "is this configured right?", not part of the identity. */
.src-facts { font-size: .78rem; color: var(--text-muted); line-height: 1.55; margin-top: .2rem; }
.src-facts .warn { color: var(--red); font-weight: 500; }
.src-facts code { font-size: .95em; }
/* Right column: controls on top, one health line under them, both right-aligned
   so the row has a second edge to read down. */
.src-side { display: flex; flex-direction: column; align-items: flex-end; gap: .4rem; }
/* Two controls and a menu, in that order. On/off and Poll are the two things
   done to a source over and over, so they stay on the row; edit, move and
   delete are once-in-a-lifetime and live behind the ⋯ — which also keeps a
   destructive action from sitting one stray click away from Poll. */
.src-actions { display: flex; align-items: center; justify-content: flex-end; gap: .45rem; flex-wrap: wrap; }
/* Health. The dot carries the state so the words stay the same length and the
   column does not jitter as sources change; colour is spent only here and on
   the queue chips, which is why either one is visible from across the row. */
.src-state {
  display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap;
  gap: .3rem .5rem; font-size: .75rem; color: var(--text-muted); text-align: right;
}
.src-state-line { display: inline-flex; align-items: center; gap: .4rem; }
.src-state b { color: var(--text); font-weight: 600; }
.src-state .status-dot { background: #cbd5e1; }
.src-state.is-live .status-dot { background: var(--green); }
.src-state.is-busy .status-dot { background: var(--amber); animation: pulse 1.4s ease-in-out infinite; }
.src-state.is-err { color: #b91c1c; }
.src-state.is-err .status-dot { background: var(--red); }
.src-state.is-err b { color: #b91c1c; }
/* Queue chips: present only while something is actually waiting or failing. An
   always-there "clear" chip is one you learn to skip, and the warning would
   then arrive in the place you learned to skip. */
.sq-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .1rem .45rem; font-size: .72rem; line-height: 1.5;
  border: 1px solid #fde68a; border-radius: 999px;
  background: #fffbeb; color: #b45309;
}
.sq-chip .status-dot { background: var(--amber); animation: pulse 1.4s ease-in-out infinite; }
.sq-chip b { font-weight: 700; }
.sq-chip.is-err { border-color: #fecaca; background: #fef2f2; color: #b91c1c; cursor: help; }
/* The switch states itself in words. A lone switch is only readable if you
   already know which way is on, and "is this source pulling?" is the first
   question this page answers. */
.src-toggle {
  display: inline-flex; align-items: center; gap: .4rem; cursor: pointer;
  padding: .25rem .5rem .25rem .35rem; border-radius: 999px;
  transition: background var(--transition);
}
.src-toggle:hover { background: var(--slate-bg); }
.src-toggle-label {
  font-size: .75rem; font-weight: 600; color: var(--text-muted);
  min-width: 1.6rem;                        /* On/Off swap must not shift the row */
}
.src-row:not(.is-off) .src-toggle-label { color: #15803d; }
/* A poll is a request to a worker, not an instant result — the button says it
   is in flight instead of pretending the work already finished. */
.btn-x .bi.spin { animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn-x .bi.spin { animation: none; } }
/* Full-width block under a row (the API-push snippet), aligned to the text
   column so it hangs off the name rather than the card edge. */
.src-extra { grid-column: 2 / -1; margin-top: .75rem; }
.src-extra .muted-line { font-size: .78rem; color: var(--text-muted); margin-bottom: .4rem; }

/* Narrow: the two columns become two stacked bands, and the health line leads
   its band — on a phone "is it working?" is the whole reason you opened this. */
@media (max-width: 760px) {
  .src-row { grid-template-columns: auto minmax(0, 1fr); align-items: start; }
  .src-side { grid-column: 2 / -1; align-items: stretch; margin-top: .6rem; gap: .5rem; }
  .src-state { justify-content: flex-start; text-align: left; order: -1; }
  .src-actions { justify-content: flex-start; }
  .src-extra { grid-column: 1 / -1; }
}
/* Endpoint / payload sample. Ink block, wrapped rather than clipped. */
.code-block {
  margin: 0; background: var(--ink); color: #e2e8f0;
  padding: .7rem .85rem; border-radius: var(--radius-sm);
  font-size: .78rem; line-height: 1.55; overflow-x: auto;
}
.hook-deliveries { margin-top: .85rem; border-top: 1px solid var(--border); padding-top: .7rem; }
.hook-delivery { display: flex; gap: .7rem; align-items: center; font-size: .8rem; padding: .25rem 0; }
.hook-delivery .when { margin-left: auto; color: var(--text-muted); }

@media (prefers-reduced-motion: reduce) {
  .cfg-bar { animation: none; }
}

/* skeleton */
.sk { background: #e2e8f0; border-radius: 6px; animation: sk 1.4s ease-in-out infinite; }
@keyframes sk { 0%,100% { opacity: 1; } 50% { opacity: .5; } }

.fade-in-up { animation: fadeInUp .22s ease; }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .fade-in-up { animation: none; } }

.muted { color: var(--text-muted); }
.mono { font-variant-numeric: tabular-nums; }
.nowrap { white-space: nowrap; }

/* mock-mode ribbon */
.mock-ribbon { position: fixed; bottom: 12px; right: 12px; z-index: 200; background: #0f172a; color: #fff; font-size: .72rem; font-weight: 600; padding: .35rem .7rem; border-radius: 999px; box-shadow: 0 4px 16px rgba(0,0,0,.2); opacity: .85; }

/* ── Toasts ──────────────────────────────────────────────────────────────────
   Bottom-right, newest on top, stacked in one fixed column. Not centred: the
   middle of the screen is where the work is, and a confirmation that lands on
   top of the row you just acted on is read as an obstacle, not as feedback.
   Still the same plain pill it was — only the fill changed, so the outcome
   reads before the sentence does: green done, red failed, amber we need
   something from you first, blue accepted and finishing elsewhere.

   The four fills are a step darker than the --green/--amber/--red/--blue text
   tokens: those are tuned to sit ON white, and white text on them lands under
   4.5:1. Above .cx-overlay (400) — a dialog can toast and the answer must not
   land behind it. */
.toast-stack {
  position: fixed; right: 1rem; bottom: 1rem; z-index: 420;
  display: flex; flex-direction: column-reverse; align-items: flex-end; gap: .45rem;
  max-width: min(380px, calc(100vw - 2rem));
  pointer-events: none;                    /* the column never eats clicks; cards re-enable it */
}
body.has-mock-ribbon .toast-stack { bottom: 3.1rem; }

.toastx {
  pointer-events: auto; cursor: pointer;   /* click dismisses */
  max-width: 100%; padding: .55rem .85rem;
  background: #15803d; color: #fff;
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  font-size: .8125rem; line-height: 1.45; font-weight: 500; overflow-wrap: anywhere;
  animation: toastIn .2s ease;
}
.toastx.tx-err  { background: #b91c1c; }
.toastx.tx-warn { background: #b45309; }
.toastx.tx-info { background: #1d4ed8; }
.toastx.tx-out  { animation: toastOut .18s ease forwards; }

@keyframes toastIn  { from { opacity: 0; transform: translateY(8px); } }
@keyframes toastOut { to   { opacity: 0; transform: translateX(12px); } }

@media (prefers-reduced-motion: reduce) {
  .toastx, .toastx.tx-out { animation: none; }
}
