*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font: 14px/1.5 var(--font-ui);
  -webkit-font-smoothing: antialiased;
  font-variant-numeric: tabular-nums;
}

/* Numbers an agent reads all day: tabular so a ticking timer never shifts. */
.num { font-variant-numeric: tabular-nums; letter-spacing: .01em; }

/* ── shell ────────────────────────────────────────────────────────── */
/* Three columns: navigation, work, the phone. The phone column exists only
   on Calls — a call must never move or reflow while somebody is on it. */
.shell {
  display: grid;
  grid-template-columns: 224px minmax(0, 1fr) 336px;
  grid-template-rows: auto minmax(0, 1fr);
  height: 100vh;
  background: var(--bg);
}
.shell.no-phone { grid-template-columns: 224px minmax(0, 1fr); }

/* ── sidebar ──────────────────────────────────────────────────────── */
/* Dark in both themes: the rail is navigation, not content, and a dark
   column is what separates the two at a glance. Its own ink tokens, so the
   page palette underneath is untouched. */
.sidebar {
  grid-row: 1 / -1;
  background: linear-gradient(180deg, var(--rail), var(--rail-2));
  color: var(--rail-ink);
  overflow: hidden;
  display: flex; flex-direction: column;
}
.rail-head {
  flex: none; display: flex; align-items: center; gap: 10px;
  padding: 16px 16px 14px;
}
.rail-mark {
  width: 34px; height: 34px; border-radius: 9px; flex: none;
  background: var(--accent); display: grid; place-items: center;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 38%, transparent);
}
.rail-mark svg { width: 18px; height: 18px; color: #fff; }
.rail-mark.has-logo {
  background: none; box-shadow: none; width: auto; max-width: 132px;
  border-radius: 0; place-items: center start;
}
.rail-mark.has-logo img { max-width: 100%; max-height: 30px; display: block; }
.rail-head-t { min-width: 0; }
.rail-name {
  font-size: 14.5px; font-weight: 700; letter-spacing: -.01em;
  color: var(--rail-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rail-sub { font-size: 11px; color: var(--rail-ink-2); margin-top: -1px; white-space: nowrap; }
.rail-mark.has-logo + .rail-head-t { display: none; }

/* Collapse lives in the rail head now, not on the top bar. */
.rail-toggle {
  margin-left: auto; width: 26px; height: 26px; flex: none;
  display: grid; place-items: center; border: 0; border-radius: 7px;
  background: rgba(255,255,255,.06); color: var(--rail-ink-2); cursor: pointer;
}
.rail-toggle:hover { background: rgba(255,255,255,.11); color: var(--rail-ink); }
.rail-toggle svg { width: 14px; height: 14px; transform: rotate(90deg); transition: transform var(--med) var(--ease); }
.shell.is-rail .rail-toggle svg { transform: rotate(-90deg); }

.rail-links {
  flex: 1; overflow-y: auto; min-height: 0; padding: 0 12px 10px;
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.12) transparent;
}
.side-lone { margin-top: 6px; }

/* Groups fold. <details>, so keyboard and screen readers get it for free. */
.side-group { border: 0; margin-top: 10px; }
.side-group > summary {
  list-style: none; display: flex; align-items: center; gap: 6px;
  padding: 6px 8px; cursor: pointer; user-select: none;
  font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--rail-ink-3); font-weight: 600;
}
.side-group > summary::-webkit-details-marker { display: none; }
.side-group > summary:hover { color: var(--rail-ink-2); }
.side-group > summary .chev { margin-left: auto; opacity: .7; display: grid; transition: transform var(--fast) var(--ease); }
.side-group > summary .chev svg { width: 12px; height: 12px; }
.side-group[open] > summary .chev { transform: rotate(180deg); }
.side-group > summary .gcount { margin-left: 4px; font-size: 10.5px; opacity: .7; font-variant-numeric: tabular-nums; }
.side-group[open] > summary .gcount { display: none; }
.side-group > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

.side-link {
  display: flex; align-items: center; gap: 11px;
  width: 100%; height: 38px; padding: 0 12px; border-radius: 9px;
  background: none; border: none; cursor: pointer; text-align: left;
  font: inherit; font-size: 13.5px; color: var(--rail-ink-2);
  position: relative;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.side-link svg { width: 17px; height: 17px; flex: none; color: var(--rail-ink-3); transition: color var(--fast) var(--ease); }
.side-link:hover { background: rgba(255,255,255,.055); color: var(--rail-ink); }
.side-link:hover svg { color: var(--rail-ink-2); }
/* The open screen: a soft pill plus the same 3px rail a live call row wears. */
.side-link.is-on {
  background: rgba(255,255,255,.08); color: #fff; font-weight: 500;
  box-shadow: inset 3px 0 0 var(--accent), inset 0 0 0 1px rgba(255,255,255,.04);
}
.side-link.is-on svg { color: var(--accent); }
.side-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.side-count {
  margin-left: auto; font-size: 10.5px; font-weight: 600;
  background: var(--accent); color: #08251C;
  border-radius: 8px; padding: 1px 6px; font-variant-numeric: tabular-nums;
}

.rail-foot {
  flex: none; margin: 8px 12px 12px; padding: 10px 12px; border-radius: 10px;
  background: rgba(255,255,255,.05);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.06);
  display: flex; align-items: center; gap: 10px;
}
.rail-foot-t { min-width: 0; flex: 1; }
.rail-foot-n {
  font-size: 13px; font-weight: 600; color: var(--rail-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rail-foot-r { font-size: 11px; color: var(--rail-ink-2); }

/* Collapsed: icons only. Labels are hidden, not removed, so the accessible
   name and the title survive. */
.shell.is-rail { grid-template-columns: 64px minmax(0, 1fr) 336px; }
.shell.is-rail.no-phone { grid-template-columns: 64px minmax(0, 1fr); }
.shell.is-rail .rail-head { flex-direction: column; gap: 8px; padding: 14px 0 12px; }
.shell.is-rail .rail-toggle { margin: 0; }
.shell.is-rail .rail-head-t, .shell.is-rail .rail-foot-t,
.shell.is-rail .side-link > span:not(.side-count),
.shell.is-rail .side-group > summary > span:not(.chev) {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.shell.is-rail .side-link { justify-content: center; padding: 0; gap: 0; }
.shell.is-rail .side-group > summary { justify-content: center; padding: 6px 0; }
.shell.is-rail .side-group > summary .chev { margin: 0; }
.shell.is-rail .side-count { position: absolute; top: 4px; right: 4px; margin: 0; font-size: 9.5px; padding: 0 4px; }
.shell.is-rail .rail-foot { justify-content: center; padding: 8px 0; }

/* The ☰ that opens the rail on a phone, and the scrim behind it.
   It carries `bar-icon burger`, and `.bar-icon { display: inline-flex }` is
   declared LATER at the same (0,1,0) weight — so a bare `.burger` lost and
   the ☰ showed on desktop. Matched to (0,2,0) so it wins wherever it sits,
   and the phone rule below is matched the same way. */
.bar-icon.burger { display: none; }
.rail-scrim {
  display: none; position: fixed; inset: 0; z-index: 79;
  background: rgba(15, 23, 42, .36); opacity: 0; pointer-events: none;
  transition: opacity var(--med) var(--ease);
}
.rail-scrim.is-on { opacity: 1; pointer-events: auto; }

/* ── header search ────────────────────────────────────────────────
   Filters what is already loaded — screens and tenants in memory. There is
   no search endpoint, and a box that pretends to reach the whole estate
   would be lying about its reach. Deliberately not bound to ⌘K: that focuses
   the dialler, which an agent uses all shift. */
.hsearch { position: relative; display: flex; align-items: center; flex: 1; max-width: 520px; min-width: 0; }
.hsearch > svg { position: absolute; left: 11px; width: 14px; height: 14px; color: var(--text-muted); }
.hsearch-i {
  width: 100%; min-width: 0; font: inherit; font-size: 13px; height: 36px;
  padding: 0 12px 0 34px;
  border: 1px solid var(--border); border-radius: 9px;
  background: var(--surface-sunken); color: var(--text-primary);
}
.hsearch-i:focus { outline: 2px solid var(--accent); outline-offset: 1px; background: var(--surface); }
.hsearch-i::placeholder { color: var(--text-muted); }
.hsearch-drop {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 40;
  background: var(--ground-raised); border: 1px solid var(--line);
  border-radius: var(--r3); box-shadow: var(--lift-3); padding: var(--s2);
  max-height: 320px; overflow-y: auto;
}
.hsearch-r {
  display: flex; align-items: center; gap: 9px; width: 100%;
  background: none; border: 0; cursor: pointer; font: inherit; font-size: 13px;
  color: var(--ink); padding: 7px var(--s3); border-radius: var(--r2); text-align: left;
}
.hsearch-r:hover { background: var(--ground-sunken); }
.hsearch-r svg { width: 15px; height: 15px; flex: none; color: var(--ink-faint); }
.hsearch-k { margin-left: auto; font-size: 11px; color: var(--ink-faint); }
.hsearch-none { padding: 10px var(--s3); font-size: 12.5px; color: var(--ink-faint); }

/* Column 2 onward, not the full width: the rail now spans both rows and
   carries its own head, so the header sits above the work area only and the
   two meet on one line across the top. */
.topbar {
  grid-column: 2 / -1;
  display: flex; align-items: center; gap: 12px;
  padding: 0 24px; height: 56px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.topbar .spacer { flex: 1; }

/* .brand, .brand-sub and .bar-icon* live with the buttons below. The copies
   that used to sit here were dead — later blocks of the same specificity won
   — except `.brand .brand-sub`, which at (0,2,0) beat `.brand-sub` (0,1,0)
   and kept the old --ink-faint through a palette change. Same failure the
   door hit: a local redeclaration outliving the token it shadowed. */

/* One pill, three states. It replaced a lamp plus a separate button that
   could disagree on screen — "Offline" beside "Taking calls" — because
   registration is the precondition for taking a call, not a parallel fact. */
.presence {
  display: inline-flex; align-items: center; gap: 7px;
  font: inherit; font-size: 12.5px; font-weight: 500; height: 34px;
  padding: 0 12px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-secondary); white-space: nowrap;
}
.presence-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--text-muted); }
/* Taking calls: the one state that earns the live hue. */
.presence[data-state="available"] { background: var(--live-wash); color: var(--accent-ink); border-color: transparent; }
.presence[data-state="available"] .presence-dot { background: var(--live); box-shadow: 0 0 0 3px var(--live-wash); }
/* On break is a deliberate choice, so it is stated, not alarming. */
.presence[data-state="break"] { background: var(--warn-wash); color: var(--warn); border-color: transparent; }
.presence[data-state="break"] .presence-dot { background: var(--ring); }
/* Offline is not a choice — it is the phone being down, so it recedes. */
.presence[data-state="offline"] { opacity: .8; }
/* The presence pill opens a menu, so it has to answer the pointer.
   It had `cursor: pointer` and nothing else: the only clickable thing on the
   topbar that stayed completely still under the mouse, which reads as a status
   label rather than a control. The state colour is load-bearing here — it is
   how an agent knows they are taking calls — so hover tightens the ring rather
   than repainting the fill, and every state keeps its own colour. */
.presence-btn { cursor: pointer; transition: box-shadow var(--fast) var(--ease); }
.presence-btn:hover:not(:disabled) {
  box-shadow: inset 0 0 0 1px currentColor;
}
.presence-btn:disabled { cursor: not-allowed; }
.presence-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.main { padding: 22px 24px 32px; overflow: auto; min-width: 0; background: var(--bg); }
/* No reading-width cap: the approved world runs the work area full-bleed, and
   a modal is appended into main by all twelve of its call sites — the old cap
   clamped its SCRIM and left white down the right-hand side. */
.main > *:not(.modal) { max-width: none; }
.side { border-left: 1px solid var(--border); background: var(--surface); padding: 20px; overflow: auto; display: flex; flex-direction: column; gap: 20px; }

/* ── section heading: more space above than below ─────────────────── */

/* More space above a heading than below it, so a section reads as belonging to
   what follows rather than floating between two blocks. The first one on a
   screen needs no lead-in. */
.sec { margin: var(--s6) 0 var(--s3); }
.main > .sec:first-child, .main > div > .sec:first-child { margin-top: 0; }
/* Not uppercased: every section heading on this product is Bangla, where
   uppercase does nothing, so the rule only ever fired on the English locale
   and made the two look like different designs. */
.sec h2 {
  font-size: 12.5px; font-weight: 600; letter-spacing: .02em;
  color: var(--ink-muted); margin: 0;
}
.sec p { font-size: 12px; color: var(--ink-faint); margin: var(--s1) 0 0; }
.main > .sec:not(:first-child) { margin-top: var(--s6); }

/* ── call rows: no cards. Depth from ground + one state rail ──────── */

.rows { display: flex; flex-direction: column; }

.row {
  display: grid;
  /* rail · who · redial · duration · state. The redial column is `auto`, so it
     costs nothing on the rows that do not render the button (a live call, or
     an agent who cannot dial) rather than reserving width on every row. */
  grid-template-columns: 3px minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) var(--s3) var(--s3) 0;
  border-radius: var(--r2);
  transition: background var(--fast) var(--ease);
}
.row + .row { box-shadow: inset 0 1px 0 var(--line); }
.row:hover { background: var(--ground-sunken); }

.rail { width: 3px; height: 26px; border-radius: 2px; background: transparent; }

.who { min-width: 0; }
.who .n {
  font-size: 13.5px; font-weight: 500; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.who .m { font-size: 11.5px; color: var(--ink-faint); margin-top: 2px; }

/* Direction rides INSIDE the identity cell, not as its own column: a column
   for a 12px glyph would cost its width on every row forever. Inline-grid
   rather than flex on the parent, so the name keeps its ellipsis. */
.dir {
  display: inline-grid; place-items: center; width: 15px; height: 15px;
  border-radius: 4px; margin-right: 7px; vertical-align: -3px;
}
.dir svg { width: 11px; height: 11px; }
.dir.is-in  { background: var(--blue-wash); color: var(--blue); }
.dir.is-out { background: var(--accent-wash-2); color: var(--accent-ink); }

/* A day heading inside the finished-call list. Terminal rows all recede to
   the same grey, so without this a week of calls reads as one undated block. */
.part {
  font-size: 10.5px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
  color: var(--ink-faint); padding: var(--s4) 0 var(--s2);
}

/* A section heading that carries its own count. The number IS the question the
   heading raises; without it the reader counts the rows. */
.sec-count { display: flex; align-items: baseline; gap: var(--s2); }
.sec-n {
  font-size: 11px; color: var(--ink-faint);
  padding: 1px 7px; border-radius: 999px; background: var(--surface-hover);
}
.sec-n.is-live { background: var(--live-wash); color: var(--live-ink); font-weight: 600; }

/* Terminal rows sit at .48 so they never compete with a live call. Hover
   brings them back — a row you are about to click should be readable while
   you click it. */
.row.is-ended:hover, .row.is-missed:hover, .row.is-cancelled:hover,
.row.is-failed:hover, .row.is-rejected:hover { opacity: .82; }

.dur { font-size: 13px; color: var(--ink-muted); min-width: 46px; text-align: right; }

.state { display: flex; align-items: center; gap: var(--s2); min-width: 104px; justify-content: flex-end; }
.lamp { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-faint); flex: none; }
.state .w { font-size: 11px; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-faint); }

/* connected — the only row allowed to be bright */
.row.is-connected { background: linear-gradient(90deg, var(--live-wash), transparent 45%); }
.row.is-connected .rail { background: var(--live); box-shadow: 0 0 10px var(--live-glow); }
.row.is-connected .lamp { background: var(--live); box-shadow: 0 0 8px var(--live-glow); }
.row.is-connected .w   { color: var(--live); }
.row.is-connected .dur { color: var(--live); }

/* ringing — blinks, because it needs an answer now */
.row.is-ringing .rail { background: var(--ring); box-shadow: 0 0 9px var(--ring-glow); }
.row.is-ringing .lamp { background: var(--ring); box-shadow: 0 0 7px var(--ring-glow); animation: blip 1.05s steps(1) infinite; }
.row.is-ringing .w    { color: var(--ring); }
@keyframes blip { 50% { opacity: .25; } }

.row.is-initiating .rail { background: var(--ring); opacity: .6; }
.row.is-initiating .lamp { background: var(--ring); opacity: .7; }
.row.is-initiating .w    { color: var(--ring); }

.row.is-queued .rail { background: var(--queue); opacity: .8; }
.row.is-queued .lamp { background: var(--queue); }
.row.is-queued .w    { color: var(--queue); }

/* terminal states recede — they must never compete with a live call */
.row.is-ended, .row.is-failed, .row.is-rejected,
.row.is-missed, .row.is-cancelled { opacity: .48; }
.row.is-failed .lamp, .row.is-rejected .lamp { background: var(--danger); }
.row.is-failed .w, .row.is-rejected .w { color: var(--danger); }

/* ── active call panel ────────────────────────────────────────────── */

.active {
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--ground);
  padding: var(--s4);
  box-shadow: var(--lift-1);
}
.active[data-live="true"] {
  border-color: color-mix(in srgb, var(--live) 34%, var(--line));
  box-shadow: var(--lift-2);
}

/* No call: a line, not a card. The card this replaced had a DISABLED
   destructive button as its largest element — the loudest thing on the phone
   said "you cannot press me", and it pushed the dial pad below the fold on a
   short screen. */
.idle-line {
  display: flex; align-items: center; gap: 9px; margin: 0;
  padding: var(--s2) 0; font-size: 12.5px; color: var(--ink-faint);
}
.idle-line .lamp { opacity: .5; }

/* The alert lives on the phone, so it has no list below it to clear. */
.phone-alert { margin-bottom: 0; }

.active .peer { font-size: 17px; font-weight: 600; letter-spacing: -.01em; margin: 0; }
.active .sub  { font-size: 12px; color: var(--ink-faint); margin: var(--s1) 0 0; }

.timer {
  font: 28px/1.1 var(--font-num);
  letter-spacing: -.02em;
  margin: var(--s4) 0 0;
  color: var(--ink);
}
.active[data-live="true"] .timer { color: var(--live); }

.controls { display: flex; gap: var(--s2); margin-top: var(--s4); flex-wrap: wrap; }

/* ── buttons ──────────────────────────────────────────────────────── */

.btn {
  font: inherit; font-size: 13px; font-weight: 500;
  height: 36px; padding: 0 14px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-primary);
  cursor: pointer; display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease), transform var(--fast) var(--ease);
}
.btn svg { width: 15px; height: 15px; }
/* The neutral button's hover fill, and ONLY the neutral button's.
   Every filled variant is excluded by name: this rule and `.btn-primary:hover`
   have identical specificity and this one is written first, so a variant that
   expresses its hover any other way — .btn-primary uses a `filter` — had its
   teal replaced by this near-white and then dimmed to grey. The primary button
   on every page head turned into a ghost on hover for exactly that reason, and
   it looked deliberate enough to survive a long time. A variant that does set
   its own `background` (.btn-dark, .btn-danger) was never affected, which is
   why the bug showed on one button and not the rank beside it. */
.btn:hover:not(:disabled):not(.btn-primary):not(.btn-dark):not(.btn-danger):not([aria-pressed="true"]) {
  background: var(--surface-hover);
}
.btn:active:not(:disabled) { transform: translateY(1px); }
/* A disabled control still has to say what it is.
   `opacity: .45` faded the label and the fill together, which took the primary
   button's white-on-teal from 3.74:1 to 1.75:1 — a label you cannot read is not
   a disabled button, it is a smudge. Recolouring instead keeps the word legible
   while still reading as off, and matters now that Send is disabled until the
   reply box has text. */
.btn:disabled:not(.is-loading) {
  cursor: not-allowed;
  background: var(--surface-sunken); border-color: var(--border);
  color: var(--text-muted); box-shadow: none;
}
/* A button that is WORKING is not a button that is off.
   The loading state disables the button so it cannot be clicked twice, which
   handed it to the rule above and repainted the primary button near-white —
   measured rgb(248,250,251) mid-request. So the one control the person is
   waiting on lost its colour at the exact moment it was doing the thing they
   asked for, and read as disabled rather than busy. Same shape as the hover
   bug in .btn-primary: a generic rule quietly winning over a variant that
   never restated its background.
   `cursor: wait` rather than `not-allowed`: the click landed, it is running. */
.btn.is-loading:disabled { cursor: wait; }
/* --accent-solid / --on-accent rather than --accent / #fff: the pair flips per
   theme, because white reads on the light teal and not on the dark mint. The
   hardcoded #fff measured 1.86:1 in dark — a label nobody could read on the
   screen's most important button. */
.btn-primary { background: var(--accent-solid); border-color: var(--accent-solid);
  color: var(--on-accent); box-shadow: var(--lift-1); }
/* Deepened toward black, never `filter: brightness()`.
   A filter dims the LABEL along with the fill. Mixing the background alone
   leaves the label at full strength, so the button gets darker and MORE
   readable as you aim at it: measured white-on-teal 5.47:1 at rest, 6.86:1 on
   hover, 8.10:1 pressed. The lift grows with it — a page's one committing
   action should come forward under the pointer, not just change tint. */
.btn-primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent-solid) 86%, #000);
  border-color: color-mix(in srgb, var(--accent-solid) 86%, #000);
  box-shadow: var(--lift-2);
}
/* Dark mode moves the other way — toward white, because the mint is already
   dark-on-dark and deepening it further would sink it into the surface. */
:root[data-theme="dark"] .btn-primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent-solid) 88%, #fff);
  border-color: color-mix(in srgb, var(--accent-solid) 88%, #fff);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn-primary:hover:not(:disabled) {
    background: color-mix(in srgb, var(--accent-solid) 88%, #fff);
    border-color: color-mix(in srgb, var(--accent-solid) 88%, #fff);
  }
}
/* Pressed sinks: the lift goes away and the fill deepens again, so the whole
   gesture reads rest -> forward -> down rather than rest -> tint.
   Written after the dark hover rules above and with the same specificity, so
   it wins in both themes — the earlier draft put it before them and dark mode
   pressed to exactly its hover colour, i.e. no press feedback at all. */
.btn-primary:active:not(:disabled) {
  background: color-mix(in srgb, var(--accent-solid) 76%, #000);
  border-color: color-mix(in srgb, var(--accent-solid) 76%, #000);
  box-shadow: none;
}
:root[data-theme="dark"] .btn-primary:active:not(:disabled) {
  /* Toward white here, matching the direction dark-mode hover already moves. */
  background: color-mix(in srgb, var(--accent-solid) 74%, #fff);
  border-color: color-mix(in srgb, var(--accent-solid) 74%, #fff);
}
@media (prefers-color-scheme: dark) {
  /* The `:not([data-theme="light"])` guard MUST stay inside the media query.
     Outside it, that selector also matches a light-mode page that has set no
     explicit theme — which would have pressed the light teal toward white. */
  :root:not([data-theme="light"]) .btn-primary:active:not(:disabled) {
    background: color-mix(in srgb, var(--accent-solid) 74%, #fff);
    border-color: color-mix(in srgb, var(--accent-solid) 74%, #fff);
  }
}
.btn-dark { background: var(--text-primary); border-color: var(--text-primary); color: var(--bg); }
.btn-dark:hover:not(:disabled) { background: color-mix(in srgb, var(--text-primary) 88%, var(--bg)); }
.btn-ghost { border-color: transparent; background: transparent; color: var(--text-secondary); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-hover); color: var(--text-primary); }
.btn-danger { background: var(--danger); border-color: var(--danger); color: var(--on-danger); }
/* Same two directions as .row-act.is-danger above, and for the same measured
   reason: dark's --danger is a light pink, so deepening it lowers contrast
   against the near-black label it carries. Border moves with the fill or the
   button grows a bright ring around a darkened face on hover. */
.btn-danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--danger) 88%, #000);
  border-color: color-mix(in srgb, var(--danger) 88%, #000);
}
:root[data-theme="dark"] .btn-danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--danger) 88%, #fff);
  border-color: color-mix(in srgb, var(--danger) 88%, #fff);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn-danger:hover:not(:disabled) {
    background: color-mix(in srgb, var(--danger) 88%, #fff);
    border-color: color-mix(in srgb, var(--danger) 88%, #fff);
  }
}
/* Mute and hold on the phone: the only .btn toggles, and both are the kind
   you want to see is ON. A filter or a period picker is a .chip or a .seg. */
.btn[aria-pressed="true"] { background: var(--danger-wash); border-color: var(--danger); color: var(--danger); }
.btn-sm { height: 30px; padding: 0 11px; font-size: 12.5px; }
.btn-lg { height: 42px; padding: 0 18px; font-size: 14px; }
/* Loading keeps the width: the label goes transparent, a spinner sits in it. */
.btn.is-loading { color: transparent; position: relative; pointer-events: none; }
.btn.is-loading::after {
  content: ''; position: absolute; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--text-muted); border-bottom-color: transparent; animation: spin .7s linear infinite;
}
.btn-dark.is-loading::after { border-color: #fff; border-bottom-color: transparent; }
.btn-danger.is-loading::after { border-color: var(--on-danger); border-bottom-color: transparent; }
/* Follows the label it spins beside, for the same reason. */
.btn-primary.is-loading::after { border-color: var(--on-accent); border-bottom-color: transparent; }
@keyframes spin { to { transform: rotate(360deg); } }

.btn:focus-visible, .jack:focus-visible, .key:focus-visible, .tab:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible, .chip:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* The tenant's logo sits before its name. Capped by height so a wide
   wordmark and a square icon both sit on the same baseline. */
/* The logo sits in a rounded tile rather than floating as a bare image: a
   tenant uploads whatever they have — a wide wordmark, a square icon, a
   screenshot — and an unframed <img> stretched across the bar reads as a
   coloured bar rather than as a mark. A fixed square with cover crops
   anything to the same silhouette. */
.brand { display: flex; align-items: center; gap: var(--s3); }
/* A logo is shown whole or not at all. This was a 30px square with
   object-fit: cover — right for an avatar, where a face should fill the
   circle, and wrong here: a tenant uploads a wide wordmark as often as a
   square icon, and cover kept only the middle of it ("LeadsNimble" arrived
   in the bar as "adsN"). Height is fixed so every logo sits on the same
   baseline; width follows the image's own ratio, capped so an extreme
   wordmark cannot push the bar around. */
.brand-logo {
  height: 26px; flex: none;
  display: inline-flex; align-items: center;
}
.brand-logo img {
  height: 100%; width: auto; max-width: 200px;
  object-fit: contain; display: block;
}
/* The tenant's name is the brand, so it carries the weight. */
.brand-name { font-weight: 650; color: var(--text-primary); }
.brand-sub { color: var(--text-muted); font-weight: 400; }
/* Without a logo the name is the only mark, so it stands a little larger. */
.brand.is-textonly .brand-name { font-size: 15px; }

/* The bar's icon buttons — collapse, theme, language. One shape, so a row of
   them reads as a toolbar rather than as three unrelated controls. */
.bar-icon {
  width: 30px; height: 30px; flex: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  border: 1px solid transparent; border-radius: var(--r2);
  background: none; color: var(--text-muted); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.bar-icon:hover { background: var(--surface-hover); color: var(--text-primary); }
.bar-icon svg { width: 17px; height: 17px; }
/* The language button carries its code beside the globe, so somebody can see
   which language they are in without opening anything. */
.bar-icon-wide { width: auto; padding: 0 var(--s2); }
.bar-icon-tag {
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
}

/* The wall clock. Two lines in 56px: the time is the point, the date is
   needed once a day and reads underneath it. Tabular figures throughout —
   the seconds change every tick, and proportional digits make the whole
   block twitch sideways once a second.
   Text only, no border and no hover: it is information sitting in a row of
   buttons, and anything button-shaped here invites a click that does
   nothing. */
.hclock {
  flex: none; margin-right: var(--s1); text-align: right; line-height: 1.1;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Baseline flex, not two spans in a block: Bengali digits at 15px and 11px
   sharing a line box sat the seconds up at cap height, which read as a
   superscript rather than as part of the clock. */
.hclock-t {
  display: flex; align-items: baseline; justify-content: flex-end; gap: 1px;
  color: var(--text-primary); font-size: 15px; font-weight: 600; letter-spacing: -.01em;
}
/* Quiet seconds. Only a live call is allowed to be bright, so the clock has
   to read as running without asking for the eye. */
.hclock-t .ss { font-size: 11px; font-weight: 500; color: var(--text-muted); }
.hclock-d { color: var(--text-muted); font-size: 10.5px; font-weight: 500; margin-top: 1px; }

/* A photo fills the circle rather than letterboxing inside it. The span keeps
   its own size and radius, so initials and image are interchangeable. */
/* The accent ground is for initials. A photo covers it — except a PNG with
   transparency, which let the green show straight through and read as a
   coloured halo around the face. Cleared, not merely covered. */
.avatar.has-img { padding: 0; overflow: hidden; background: var(--surface-sunken); }
.avatar.has-img img { width: 100%; height: 100%; object-fit: cover; display: block; }


.kebab-btn {
  width: 28px; height: 28px; border-radius: 7px; display: inline-grid; place-items: center;
  color: var(--text-muted); border: 0; background: none; cursor: pointer;
}
.kebab-btn:hover { background: var(--surface-hover); color: var(--text-primary); }

/* ── dial pad ─────────────────────────────────────────────────────── */

.dial input {
  width: 100%; font: 17px/1.4 var(--font-num);
  padding: 11px var(--s3);
  border: 1px solid var(--line-strong); border-radius: var(--r2);
  background: var(--ground); color: var(--ink);
  letter-spacing: .02em;
}
.dial input::placeholder { color: var(--ink-faint); }

.keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); margin-top: var(--s3); }
.key {
  font: 16px/1 var(--font-num);
  padding: 13px 0;
  border: 1px solid var(--line); border-radius: var(--r2);
  background: var(--ground-raised); color: var(--ink);
  cursor: pointer;
  transition: background var(--fast) var(--ease), transform var(--fast) var(--ease);
}
.key:hover { background: var(--ground-sunken); }
.key:active { transform: translateY(1px); background: var(--line); }
.key small { display: block; font-size: 9px; color: var(--ink-faint); margin-top: 3px; letter-spacing: .08em; }

/* ── keyboard hints ───────────────────────────────────────────────── */

.hints { display: flex; gap: var(--s4); flex-wrap: wrap; align-items: center;
         margin-top: auto; padding-top: var(--s4); border-top: 1px solid var(--line); }
.hint { font-size: 11.5px; color: var(--ink-faint); display: inline-flex; align-items: center; gap: 6px; }
kbd {
  font: 11px var(--font-num); color: var(--ink-muted);
  border: 1px solid var(--line-strong); border-bottom-width: 2px;
  border-radius: 4px; padding: 2px 6px; background: var(--ground-raised);
}

/* ── banner: connection truth, never hidden ───────────────────────── */

.banner {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 11px 13px; border-radius: 9px; font-size: 13px; margin-bottom: var(--s4);
}
.banner-warn { background: var(--warn-wash); color: var(--warn); }

/* ── empty state ──────────────────────────────────────────────────── */

.empty {
  padding: 28px 16px; text-align: center; color: var(--text-secondary);
  border: 1px dashed var(--border-strong); border-radius: var(--r3); background: var(--surface);
}
.empty-ic { width: 44px; height: 44px; border-radius: 12px; background: var(--surface-sunken); display: grid; place-items: center; margin: 0 auto 10px; }
.empty-ic svg { width: 20px; height: 20px; color: var(--text-muted); }
.empty p { margin: 0; font-size: 12.5px; max-width: 44ch; margin-inline: auto; }
.empty .big { color: var(--text-primary); font-size: 14px; font-weight: 600; margin-bottom: 3px; }
.empty .btn { margin-top: 12px; }

/* Skeleton: the content's own shape while it loads. Never a spinner. */
.skel { background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--surface-hover) 50%, var(--surface-sunken) 75%); background-size: 200% 100%; animation: shimmer 1.4s infinite; border-radius: 6px; height: 12px; display: block; }
@keyframes shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skel { animation: none; } }

/* ── admin tabs ───────────────────────────────────────────────────── */

.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 18px; }
.tab {
  font: inherit; font-size: 13px; font-weight: 500; padding: 9px 12px;
  background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px;
  color: var(--text-secondary); cursor: pointer; display: inline-flex; gap: 6px; align-items: center;
}
.tab:hover { color: var(--text-primary); }
.tab[aria-selected="true"] { color: var(--text-primary); border-bottom-color: var(--accent); }
.tab-count { font-size: 11px; background: var(--surface-sunken); border-radius: 8px; padding: 0 6px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.tab[aria-selected="true"] .tab-count { background: var(--accent-wash); color: var(--accent-ink); }
/* A provider's state beside its tab name. The lamp hues are .tag's, not new
   ones — the dot and the card's pill must never disagree — but a 24px pill
   per tab would make the strip taller than the tabs it labels, so this is the
   dot alone. The accessible name carries the word; a colour is not a state to
   a screen reader. */
.tab-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--text-muted); }
.tab-dot.ok   { background: var(--live); }
.tab-dot.bad  { background: var(--danger); }
.tab-dot.info { background: var(--blue); }
.tab-dot.off  { background: var(--border-strong); }

/* The action that belongs to a tab's own table, not to the screen. A band
   would read as a second heading under the strip. */
.tab-actions { display: flex; justify-content: flex-end; margin-bottom: var(--s4); }

/* Counts that used to sit in a band, kept as plain figures above the table:
   "how many are registered" is the question this screen is opened with. */
.tab-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s4); margin-bottom: var(--s4); flex-wrap: wrap;
}
.tab-stats { display: flex; gap: var(--s5); }
.tab-stat { display: flex; align-items: baseline; gap: 6px; font-size: 12.5px; color: var(--text-secondary); }
.tab-stat b {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: 18px; font-weight: 700; letter-spacing: -.02em; color: var(--text-primary);
}
.tab-stat b.is-on { color: var(--live); }

/* THE ADMIN TABLE — rewritten against a concrete external spec (Linear's
   restraint, Stripe's table density, Vercel's precision — named products
   with named reasons, not adjectives) after three prior passes on this file
   still read as generic. The rule that mattered most: no nested cards.
   Sections are separated by spacing + a heading + a hairline rule, never by
   wrapping content in a bordered box — the earlier .deck/.panel pair WAS
   that anti-pattern (a bevelled header box capping a bevelled table box) and
   is deleted below, not kept alongside the new rules. */
.tbl-card {
  border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface);
  box-shadow: var(--lift-1); overflow: hidden; margin-bottom: 16px;
}
/*
 * A card holding an OPEN menu stops clipping, and only then.
 *
 * `overflow: hidden` is doing two jobs worth keeping — it clips the rounded
 * corners, and it contains a wide table (see .tbl-tenants below). But the
 * kebab menu is absolutely positioned inside the card, so on the last row it
 * hangs past the bottom edge and was cut off entirely: measured at 38px below
 * a one-row Queues card, which is the whole menu. The trigger opened, nothing
 * appeared, and the row read as a broken button.
 *
 * Scoped to .has-menu-open — set by kebab() while a menu is open — rather than
 * made permanent, so the clipping that earns its place is only suspended for
 * the moment something needs to escape. `visible` in both axes, because
 * `overflow-y` alone computes to `auto` on the other axis and would reopen the
 * sideways scroll the shots script fails on.
 */
.tbl-card.has-menu-open { overflow: visible; }
/* A bare table (one not wrapped by table()'s card) keeps its lead-in; inside
   the card the card owns the spacing, or the margin shows as a gap between
   the card's edge and its own header strip. */
.tbl-card > table { margin-top: 0; }
/* A short cell — a date, a count, a pill — reads as one thing; wrapping
   "16 Sep, 2026" onto three lines turned the created column into a paragraph
   and stretched every row to match it. Only the name cell may wrap. */
.tbl-card td:not(.name) { white-space: nowrap; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead tr { height: var(--head-h); background: var(--surface-sunken); }
/* NOT uppercase, deliberately, against the spec: a header row mixing scripts
   — 'Extension' beside 'কোম্পানি' — has text-transform do nothing on the
   Bangla half, so half the row shouts and half does not, which reads as a
   rendering fault. Size, weight, tracking and colour carry the rank in both
   scripts instead. */
th {
  text-align: left; padding: 0 14px; font-size: 11px; letter-spacing: .04em;
  color: var(--text-muted); font-weight: 600; white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
th.r, td.r { text-align: right; }
/* `num` is the tabular-numeral face; `is-r` is the alignment, and table()
   puts it only on the columns a caller named in numCols — the same list the
   headers follow. They disagreed before: every non-name cell was right-
   aligned while only numeric headers were, so a text header like Email or
   Role sat left over values flushed to the far edge of a wide column.
   The action rank keeps its right edge whatever the caller said. */
thead th.num, tbody td.is-r, tbody td.is-acts { text-align: right; }
/* The last column is the action rank on nearly every table here, named or
   not, and it has always sat against the card's right edge. Kept explicitly
   so tables that never passed actsCol do not drift left. */
tbody td:last-child:not(.name) { text-align: right; }
/* The action column holds a rank of buttons, not one kebab. 116px is sized to
   a rank of three; a screen whose rank is longer (Queues adds a hold-music
   select) says so with .acts-wide rather than every table paying for the
   longest one. Note `min-width` is NOT the tool here: several of these tables
   are table-layout:fixed, where min-width on a cell is ignored while its
   contents still claim the space — which shows up as the table quietly
   outgrowing its own card. */
th:last-child, td:last-child { width: 116px; padding-right: var(--s3); }
.acts-wide th:last-child, .acts-wide td:last-child { width: 232px; white-space: nowrap; }
/*
 * The settings that sit under a row's name, not in its action rank.
 *
 * A control among three buttons reads as a fourth button. The hold-music
 * picker is not an action — it is part of what the queue IS — so it sits with
 * the recording state under the name, carrying a label, because a bare select
 * in a table row says nothing about what it sets. Its own line stays quiet at
 * .sub-line weight; the label is the quietest part of it, since it is read
 * once and then never again.
 */
.row-setts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.row-setts .sett-sep { color: var(--border-strong); }
.row-setts .sett-lbl { color: var(--text-muted); }
/*
 * A row carrying a CONTROL needs more height than one carrying text.
 *
 * Cells have no vertical padding — right for text, which sits on its own
 * baseline well inside the row. But a 28px select in a 52px row that already
 * holds a name and a second line has nothing left: measured at 1px between
 * the select's bottom edge and the row's, so on the last row it sat flush
 * against the card's own border and read as overflowing it.
 *
 * The row grows to fit rather than the select shrinking: 28px is already the
 * floor for a control somebody has to hit, and shaving the settings line
 * would just move the crowding onto the name above it.
 */
tbody tr.is-tall:has(.row-setts) { height: 64px; }


tbody tr { height: var(--row-h); border-bottom: 1px solid var(--border);
           transition: background var(--fast) var(--ease); }
tbody tr.is-tall { height: 52px; }
tbody tr:last-child { border-bottom: 0; }
tbody tr:hover { background: var(--surface-hover); }
tbody tr.is-row { cursor: pointer; }
tbody tr.is-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
tbody tr.is-sel { background: var(--accent-wash); }
td { padding: 0 14px; vertical-align: middle; color: var(--text-primary); }
td.num { font-family: var(--font-num); font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
td.name { color: var(--text-primary); font-weight: 600; }
/* The row's name, when the row opens something.
   It is plain text, not a button: the row itself and the acts-column chevron
   carry the click, so making this a second control would put two tab stops on
   one row for one action. It used to BE the button, wearing `.linklike` —
   12.5px in --ink-faint, fainter than the static cell beside it, which is how
   an interactive subject came to read as disabled. Underlining on row-hover is
   what says "this opens" without spending a colour the status pills need. */
td.name .tbl-link { text-decoration: underline; text-decoration-color: transparent;
  text-underline-offset: 3px; transition: text-decoration-color var(--fast) var(--ease); }
tr.is-row:hover td.name .tbl-link,
tr.is-row:focus-visible td.name .tbl-link { text-decoration-color: var(--border-strong); }
td .sub { display: block; font-size: 11.5px; color: var(--text-muted); font-weight: 400; }
td.mu { color: var(--text-muted); }

/* Identity cell: avatar + two lines. */
/* The tenants table has nine columns and one of them holds a sign-up email
   that can be sixty characters. Auto layout widened the name column to fit it
   and pushed the table 93px past the card, which .tbl-card's overflow:hidden
   then clipped the created date off the end of. Fixed layout with a capped
   name column makes the ellipsis that is already on .who-cell actually fire.
   Percentages, not auto: the widths must not change from page to page. */
.tbl-tenants { table-layout: fixed; }
/* 27% fitted a name over a sign-up email. Both lines ellipsis long before
   that, and the width was buying room nothing used while the columns at the
   other end fought for it — so the space moves to where the crowding is. */
.tbl-tenants th:nth-child(1) { width: 22%; }
.tbl-tenants th:nth-child(2) { width: 9%; }
.tbl-tenants th:nth-child(3) { width: 14%; }
/* Created carries "18 Sept 2026" and then the action rank starts. Without the
   padding the date's last glyph sits against the first button, which is what
   made this end of the row read as one crowded block. */
.tbl-tenants th:nth-child(8) { width: 15%; }
.tbl-tenants td:nth-child(8) { padding-right: var(--s4); }
/* THREE 28px buttons and the gaps between them.
   76px was correct when the rank was two. A third was added and this width was
   not, so the buttons were squeezed into a cell too narrow for them —
   table-layout is fixed here, so a cell cannot grow to fit its contents and
   the crowding has nowhere to go. The general rule above sizes a rank of
   three at 116px; this now agrees with it instead of contradicting it. */
.tbl-tenants th:last-child  { width: 116px; }

.who-cell { display: flex; gap: 10px; align-items: center; min-width: 0; }
.who-cell .t { min-width: 0; }
.who-cell b { display: block; font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.who-cell small { display: block; font-size: 11.5px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tav { width: 32px; height: 32px; border-radius: 50%; display: inline-grid; place-items: center; font-size: 11px; font-weight: 700; color: #fff; flex: none; }

/* Group row: partitions by what needs attention. */
tbody tr.tgroup { height: 34px; background: var(--surface-sunken); }
tbody tr.tgroup td { font-size: 12px; font-weight: 600; color: var(--text-secondary); padding: 0 14px; width: auto; }
/* A group row is one cell, so it is also the last one, and `td:last-child:not(.name)`
   right-aligned its label. :last-child here only to outrank that rule. */
tbody tr.tgroup td:last-child { text-align: left; }
tbody tr.tgroup.warn { background: var(--warn-wash); }
tbody tr.tgroup.warn td { color: var(--warn); }
tbody tr.tgroup td .c { float: right; font-variant-numeric: tabular-nums; opacity: .8; }
tbody tr.tgroup td svg { width: 13px; height: 13px; vertical-align: -2px; margin-right: 6px; }

.tfoot { display: flex; align-items: center; gap: 12px; padding: 10px 14px; font-size: 12px; color: var(--text-secondary); border-top: 1px solid var(--border); }
.tfoot .right { margin-left: auto; color: var(--text-muted); }

/* Under 900px a row is a two-line card: the first cell, then status, then the
   rest as labelled meta. No horizontal scroll, ever. */
@media (max-width: 900px) {
  .tbl-card thead { display: none; }
  /* Flex, not grid: every cell is a direct child, so under grid each meta cell
     took its own implicit row and the card grew a line per column. Wrapping
     flex lets them share line two; `order` puts the pill beside the name. */
  .tbl-card tbody tr { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; height: auto; padding: 11px 14px; }
  .tbl-card tbody tr.tgroup { display: block; padding: 8px 14px; }
  .tbl-card tbody td { padding: 0; width: auto; }
  .tbl-card tbody td:first-child { order: 0; flex: 1 1 auto; min-width: 0; }
  /* The checkbox keeps its own width — it is a target, not a column. */
  .tbl-card tbody td.pick { flex: 0 0 auto; width: 22px; }
  .tbl-card tbody td.pick + td { order: 0; flex: 1 1 auto; min-width: 0; }
  .tbl-card tbody td.is-status { order: 1; flex: 0 0 auto; margin-left: auto; }
  .tbl-card tbody td.is-acts { order: 3; flex: 0 0 auto; margin-left: auto; }
  .tbl-card tbody td:not(:first-child):not(.pick):not(.is-status):not(.is-acts) {
    order: 2; flex: 0 0 auto; font-size: 11.5px; color: var(--text-muted);
  }
  /* A zero-height item at order 1.5 forces the wrap: identity and its status
     pill on line one, the labelled meta and the row actions on line two.
     Flexbox has no `break-after`, and a full-width first cell would push the
     pill off the name's line, so the break has to be its own item. */
  .tbl-card tbody tr:has(> td.is-status)::after {
    content: ''; order: 1; flex: 1 0 100%; height: 0; margin: -2px 0;
  }
  .tbl-card tbody td:not(:first-child):not(.is-status):not(.is-acts)::before { content: attr(data-label) ' '; }
  .tbl-card tbody td:not(:first-child):not(.is-status):not(.is-acts):empty { display: none; }
}

/* Status: a light fill, a dot, the word. Never colour alone. The outline pill
   this used to be gave every row the same weight; the tinted fill lets a live
   call carry the only brightness on the screen. */
.tag {
  display: inline-flex; align-items: center; gap: 6px;
  height: 24px; padding: 0 9px; border-radius: 99px;
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
  background: var(--surface-sunken); color: var(--text-secondary);
  /* A hairline and a hint of lift.
     The flat fill worked on a page of white cards and vanished on a sunken
     row, where a pill the same colour as its background is just floating
     text. The border is what gives it an edge at any depth; the inset
     highlight is a single pixel of light along the top, which is the whole
     difference between a shape drawn on the page and one sitting on it.
     Both inherit the pill's own colour through currentColor, so every tinted
     state below gets it without restating anything. */
  border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #FFF 55%, transparent),
              0 1px 1.5px color-mix(in srgb, currentColor 10%, transparent);
  letter-spacing: .01em;
}
.tag::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.tag.on, .tag.ok   { background: var(--live-wash);   color: var(--accent-ink); }
/* The WORD takes the *-ink pair, the DOT keeps the bright lamp hue. At 11.5px
   the lamp hues themselves read 2.9:1 on their own wash in light theme — a
   state nobody can read is not a state. Measured, not argued. */
.tag.live          { background: var(--live-wash);   color: var(--live-ink); }
.tag.live::before  { background: var(--live); box-shadow: 0 0 0 0 var(--live-glow); animation: pulse 2s var(--ease) infinite; }
.tag.ring          { background: var(--warn-wash);   color: var(--ring-ink); }
.tag.ring::before  { background: var(--ring); animation: blink 1.05s steps(1, end) infinite; }
.tag.warn          { background: var(--warn-wash);   color: var(--ring-ink); }
.tag.warn::before  { background: var(--warn); }
.tag.bad           { background: var(--danger-wash); color: var(--danger-ink); }
.tag.bad::before   { background: var(--danger); }
.tag.info, .tag.queued { background: var(--blue-wash); color: var(--blue); }
.tag.off           { background: var(--surface-sunken); color: var(--text-muted); }
.tag.no-dot::before { display: none; }
@keyframes blink { 50% { opacity: .3; } }

/* Where a recording came from (Uploaded · Recorded · TTS), under its name.
   A smaller pill than a status tag: it annotates the name, it is not the
   thing the row is about. */
.prompt-source { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.prompt-source .tag { height: 18px; padding: 0 7px; font-size: 10.5px; }
.prompt-source .tag::before { display: none; }
.prompt-source > span:last-child:not(.tag) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Recording from the microphone, inside the "New audio" form. */
.mic-panel {
  margin-top: var(--s3); padding: var(--s3);
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-sunken);
}
.mic-panel[hidden] { display: none; }
.mic-row { display: flex; align-items: center; gap: var(--s3); }
.mic-clock { flex: 1; font-variant-numeric: tabular-nums; color: var(--text-secondary); font-size: 13px; }
/* The recording light. Small on purpose: it says "the microphone is open",
   which somebody must not miss, without lighting the screen like a call. */
.mic-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger); flex: none;
           animation: blink 1.05s steps(1, end) infinite; }
.mic-meter { margin-top: var(--s2); height: 6px; border-radius: 99px; background: var(--border); overflow: hidden; }
.mic-meter-fill { height: 100%; width: 0; background: var(--accent-ink); transition: width 80ms linear; }
.mic-audio { width: 100%; margin: var(--s2) 0; }
@media (prefers-reduced-motion: reduce) { .mic-dot { animation: none; } }

/* Voice broadcast: the wizard and one broadcast's page. Existing tokens only —
   a screen that redeclares the palette stops following it. */
.bc-steps { display: flex; gap: var(--s2); list-style: none; margin: 0 0 var(--s3); padding: 0;
            font-size: 12px; color: var(--text-muted); flex-wrap: wrap; }
.bc-steps li { padding: 4px 10px; border-radius: 99px; background: var(--surface-sunken);
               border: 1px solid var(--border); }
.bc-steps li.is-on { color: var(--text-primary); border-color: var(--border-strong); font-weight: 600; }
.bc-steps li.is-done { color: var(--accent-ink); border-color: var(--accent-wash-2); background: var(--accent-wash); }
.bc-steps li { display: inline-flex; align-items: center; gap: 6px; }
.bc-steps li svg { width: 13px; height: 13px; }
.bc-row { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }
.bc-row > input, .bc-row > select { flex: 1 1 180px; min-width: 0; }
.bc-h { font-size: 13px; margin: var(--s3) 0 var(--s1); color: var(--text-secondary); }
.bc-pick { max-height: 220px; overflow: auto; border: 1px solid var(--border); border-radius: 10px;
           padding: var(--s2); margin-top: var(--s2); background: var(--surface-sunken);
           display: grid; gap: 2px; }
.bc-pick .check { display: flex; align-items: center; gap: var(--s2); }
.bc-total { font-weight: 600; margin: var(--s1) 0 0; }
.bc-summary { display: grid; grid-template-columns: max-content 1fr; gap: 4px var(--s3); margin: 0 0 var(--s2); }
.bc-summary dt { color: var(--text-muted); }
.bc-summary dd { margin: 0; font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.bc-estimate { color: var(--text-secondary); font-size: 13px; }
.bc-adv { margin: var(--s2) 0; border: 1px solid var(--border); border-radius: 10px; padding: var(--s2) var(--s3); }
.bc-adv summary { cursor: pointer; font-weight: 600; color: var(--text-secondary); }
.bc-progress { height: 8px; border-radius: 99px; background: var(--border); overflow: hidden; margin: var(--s3) 0 var(--s1); }
/* Accent, not live green: progress through a list is not a live call. */
.bc-progress-fill { height: 100%; background: var(--accent-ink); transition: width 300ms var(--ease, ease); }
.bc-chips { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s2) 0; }
.bc-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px 2px 2px; border-radius: 99px;
           border: 1px solid transparent; background: none; cursor: pointer; color: var(--text-secondary); }
.bc-chip.is-on { border-color: var(--border-strong); background: var(--surface-sunken); }
.bc-chip .num { font-variant-numeric: tabular-nums; font-size: 12px; }
@media (max-width: 520px) { .bc-summary { grid-template-columns: 1fr; } }

/* Step 2, choosing recipients: a filtered, paged table of contacts. */
.bc-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--s3); flex-wrap: wrap; }
.bc-head h2 { margin: 0 0 var(--s1); }
.bc-meta { display: flex; gap: var(--s4); align-items: center; flex-wrap: wrap; color: var(--text-secondary); font-size: 13px; }
.bc-meta > span { display: inline-flex; align-items: center; gap: 6px; }
.bc-meta-n { font-weight: 600; color: var(--text-primary); }
.bc-dnc-note { display: inline-flex; align-items: center; gap: var(--s2); padding: 8px 12px; border-radius: 10px;
               background: var(--blue-wash); color: var(--text-secondary); font-size: 12.5px; }
.bc-filters { display: grid; grid-template-columns: minmax(200px, 2fr) repeat(2, minmax(150px, 1fr));
              gap: var(--s2); margin: var(--s3) 0 var(--s2); }
.bc-search { display: flex; align-items: center; gap: var(--s2); border: 1px solid var(--border-strong);
             border-radius: 10px; padding: 0 var(--s3); background: var(--surface); }
/* As heavy as the global input rule (four :not()s) plus the wrapper, or that
   rule's border and focus ring draw a box inside this box. */
.bc-search input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i),
.bc-search input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i):focus {
  border: 0; background: none; box-shadow: none; flex: 1; min-width: 0; padding: 10px 0; outline: none; height: auto; }
.card-form .bc-filter select { border: 0; background: none; box-shadow: none; padding: 0; height: auto; }
.bc-search:focus-within { border-color: var(--accent); }
.bc-filter { display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--border-strong);
             border-radius: 10px; padding: 6px 10px; background: var(--surface); }
.bc-filter > span { font-size: 11px; color: var(--text-muted); }
.bc-filter select { border: 0; background: none; padding: 0; width: 100%; color: var(--text-primary); }
.bc-filter:focus-within { border-color: var(--accent); }
.bc-allbar { display: flex; align-items: center; gap: var(--s2); padding: 10px 14px; border-radius: 10px;
             background: var(--accent-wash); border: 1px solid var(--accent-wash-2); color: var(--accent-ink);
             font-weight: 600; font-size: 13px; margin-bottom: var(--s2); }
.bc-ctable table { width: 100%; }
.bc-ctable th, .bc-ctable td { vertical-align: middle; }
.bc-ctable .bc-cb { width: 36px; }
.bc-ctable tr.is-off td { color: var(--text-muted); }
.bc-sort { display: inline-flex; align-items: center; gap: 4px; background: none; border: 0; padding: 0;
           font: inherit; color: inherit; cursor: pointer; text-transform: inherit; letter-spacing: inherit; }
.bc-sort svg { width: 12px; height: 12px; opacity: .55; }
.bc-sort.is-on svg { opacity: 1; }
.bc-who { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.bc-av { width: 32px; height: 32px; flex: none; font-size: 12px; }
.bc-op { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px 3px 8px; border-radius: 99px;
         border: 1px solid var(--border); font-size: 12px; white-space: nowrap; }
.bc-op i { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); }
.bc-op-gp i { background: var(--blue); }
.bc-op-robi i { background: var(--rose); }
.bc-op-airtel i { background: var(--danger); }
.bc-op-banglalink i { background: var(--amber); }
.bc-op-teletalk i { background: var(--accent); }
.bc-tags { display: flex; gap: 4px; flex-wrap: wrap; }
.bc-tag { padding: 2px 8px; border-radius: 99px; background: var(--blue-wash); color: var(--blue); font-size: 11.5px; }
.bc-pager { display: flex; justify-content: space-between; align-items: center; gap: var(--s2); flex-wrap: wrap; margin: var(--s2) 0; }
.bc-pgs { display: flex; gap: 4px; align-items: center; }
.bc-pg { min-width: 32px; height: 32px; padding: 0 8px; border-radius: 8px; border: 1px solid var(--border);
         background: var(--surface); color: var(--text-secondary); cursor: pointer; }
.bc-pg.is-on { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--on-accent); }
.bc-pg:disabled { opacity: .45; cursor: default; }
.bc-pg svg { width: 14px; height: 14px; }
.bc-gap { color: var(--text-muted); padding: 0 2px; }
/* The footer stays in view at the bottom of the scrolling modal. */
.bc-foot { position: sticky; bottom: 0; display: flex; justify-content: space-between; align-items: center;
           gap: var(--s2); flex-wrap: wrap; padding: var(--s3) 0 0; margin-top: var(--s2);
           border-top: 1px solid var(--border); background: var(--surface); }
.bc-foot-who { display: inline-flex; align-items: center; gap: var(--s2); color: var(--text-secondary); }
.bc-foot-n { font-weight: 600; color: var(--text-primary); }
.bc-foot-acts { display: flex; gap: var(--s2); align-items: center; }
@media (max-width: 760px) {
  .bc-filters { grid-template-columns: 1fr; }
  .bc-ctable thead { display: none; }
  .bc-ctable tr { display: grid; grid-template-columns: 36px 1fr; gap: 2px var(--s2); padding: var(--s2) 0; }
  .bc-ctable td { padding: 0; border: 0; }
  .bc-ctable td:not(.bc-cb):not(:nth-child(2)) { grid-column: 2; }
}
/* A plan is not a state: the quiet pill, never green or red.
   Quiet is not the same as flat, though — it wears the same hairline and
   single-pixel highlight as .tag so the two badges in one row read as one
   family. The tracking is what makes an all-caps plan name look set rather
   than shouted. */
.plan-tag {
  display: inline-flex; align-items: center;
  height: 22px; padding: 0 10px; border-radius: 99px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em;
  background: var(--surface-sunken); color: var(--text-secondary);
  border: 1px solid var(--border);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #FFF 55%, transparent),
              0 1px 1.5px color-mix(in srgb, #0F172A 5%, transparent);
}
/* Tinted by tier, so a column of plans reads at a glance instead of word by
   word. Cool and rising — blue, violet, amber — and never green or red: the
   bright end of this palette belongs to a live call, and a plan is not a
   state anybody has to act on. Free keeps the quiet pill above. */
.plan-tag.is-blue   { background: var(--blue-wash);   color: var(--blue);
                      border-color: color-mix(in srgb, var(--blue) 22%, transparent); }
.plan-tag.is-violet { background: var(--violet-wash); color: var(--violet);
                      border-color: color-mix(in srgb, var(--violet) 22%, transparent); }
/* The word takes --ring-ink, not --amber. Amber itself measures 2.95 on its
   own wash at this size — a plan name nobody can read is not a badge. The dot
   hues elsewhere in this file keep the lamp colour for the same reason the
   text here cannot. Measured, not argued. */
.plan-tag.is-amber  { background: var(--amber-wash);  color: var(--ring-ink);
                      border-color: color-mix(in srgb, var(--amber) 28%, transparent); }

/* Dark: the top highlight comes almost all the way off and the drop shadow
   deepens. A 55% white inset over a dark wash is a grey stripe, not light —
   the lift has to be made out of the dark rather than painted on it. */
:root[data-theme="dark"] .tag,
:root[data-theme="dark"] .plan-tag {
  box-shadow: inset 0 1px 0 color-mix(in srgb, #FFF 7%, transparent),
              0 1px 2px color-mix(in srgb, #000 35%, transparent);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tag,
  :root:not([data-theme="light"]) .plan-tag {
    box-shadow: inset 0 1px 0 color-mix(in srgb, #FFF 7%, transparent),
                0 1px 2px color-mix(in srgb, #000 35%, transparent);
  }
}

.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
  font: inherit; font-size: 12px; font-weight: 500; padding: 5px 11px; border-radius: 99px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-secondary);
  display: inline-flex; gap: 6px; align-items: center; cursor: pointer;
}
.chip:hover { background: var(--surface-hover); }
.chip[aria-pressed="true"] { background: var(--text-primary); border-color: var(--text-primary); color: var(--bg); }
.chip .c { font-variant-numeric: tabular-nums; opacity: .65; }

.seg { display: inline-flex; gap: 2px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: 8px; padding: 2px; }
.seg button { font: inherit; border: 0; background: none; border-radius: 6px; padding: 4px 10px; font-size: 11.5px; font-weight: 500; color: var(--text-secondary); cursor: pointer; }
.seg button[aria-pressed="true"] { background: var(--text-primary); color: var(--bg); }

/* Icon-only controls on the topbar. Square, so two of them read as a pair
   rather than as two odd-width buttons. */
.icon-btn {
  font: inherit; width: 32px; height: 32px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-strong); border-radius: var(--r2);
  background: var(--surface); color: var(--text-secondary); cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.icon-btn:hover { color: var(--text-primary); border-color: var(--text-muted); }
.icon-btn:active { transform: translateY(1px); }
.lang-btn { font-size: 11px; font-weight: 600; letter-spacing: .02em; width: auto; padding: 0 9px; }

/* Redial, on a finished call's row.
   Faint until the row is hovered or the button itself is focused, because
   twelve solid buttons down a list read as twelve things to decide about —
   but it keeps its full size and hit area at all times, so it is never a
   target that moves. Always opaque for keyboard and touch, where there is no
   hover to reveal it. */
.row-redial { width: 28px; height: 28px; opacity: .25; }
.row:hover .row-redial, .row-redial:focus-visible { opacity: 1; }
.row-redial svg { width: 14px; height: 14px; }
@media (hover: none) { .row-redial { opacity: 1; } }

.nm { font-weight: 500; color: var(--text-primary); }
.sub-line { font-size: 12px; color: var(--text-muted); margin-top: 2px; }

/* A row's actions are a rank of inline icon buttons, the destructive one
   solid red at the end — the reference's pattern, chosen over the kebab this
   file used to argue for. Shown at rest everywhere: a row whose actions only
   appear on hover reads as a row with no actions, on a live screen exactly
   as much as on a setup one. */
.kebab { position: relative; display: flex; gap: 6px; justify-content: flex-end; }

.row-act {
  width: 28px; height: 28px; flex: none; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-strong); border-radius: var(--r1);
  background: var(--surface); color: var(--text-secondary); cursor: pointer;
  transition: background var(--fast) linear, color var(--fast) linear,
              border-color var(--fast) linear;
}
.row-act:hover { background: var(--surface-hover); color: var(--text-primary);
                 border-color: var(--text-muted); }
/* A toggle drawn as a row-act (e.g. per-queue recording): the pressed state
   needs its own look, since opacity alone does not read as "on". */
.row-act.is-active { color: var(--accent); border-color: var(--accent); }
/* A fetch-now spinner: the icon itself is the only feedback an icon button
   has, so it has to move while the request is in flight. */
.row-act.is-loading svg { animation: row-act-spin .6s linear infinite; }
@keyframes row-act-spin { to { transform: rotate(360deg); } }
/* Delete is the one filled control in a row: it is the action you cannot
   undo, and the reference marks it the same way.
   --on-danger, never a hardcoded #fff. The dark theme's danger is a LIGHT
   pink (#FB7185), so white-on-it measured 2.69:1 — under the 3:1 floor a
   non-text control needs, on the one button in the row you must not misread.
   The token already flips to near-black there; this rule just wasn't using it,
   the same bug --on-accent was introduced to fix on .btn-primary. */
.row-act.is-danger { background: var(--danger); border-color: var(--danger);
                     color: var(--on-danger); }
/* Deepened, not brightened. `filter: brightness(1.08)` washed the red toward
   pink — the wrong direction for the row's one irreversible action — and
   dimmed the glyph with it. Mixing the fill alone keeps the glyph crisp. */
.row-act.is-danger:hover {
  background: color-mix(in srgb, var(--danger) 86%, #000);
  border-color: color-mix(in srgb, var(--danger) 86%, #000);
  color: var(--on-danger); filter: none;
}
.row-act.is-danger:active {
  background: color-mix(in srgb, var(--danger) 76%, #000);
  border-color: color-mix(in srgb, var(--danger) 76%, #000);
}
/* Dark moves toward white for the same reason .btn-primary does: the theme's
   danger is a light pink carrying near-black text, so deepening it walks the
   contrast DOWN (measured 7.04:1 rest -> 5.30:1 hover before this rule).
   Brightening walks it up, and keeps hover meaning "more", not "less". */
:root[data-theme="dark"] .row-act.is-danger:hover {
  background: color-mix(in srgb, var(--danger) 86%, #fff);
  border-color: color-mix(in srgb, var(--danger) 86%, #fff);
}
:root[data-theme="dark"] .row-act.is-danger:active {
  background: color-mix(in srgb, var(--danger) 76%, #fff);
  border-color: color-mix(in srgb, var(--danger) 76%, #fff);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .row-act.is-danger:hover {
    background: color-mix(in srgb, var(--danger) 86%, #fff);
    border-color: color-mix(in srgb, var(--danger) 86%, #fff);
  }
  :root:not([data-theme="light"]) .row-act.is-danger:active {
    background: color-mix(in srgb, var(--danger) 76%, #fff);
    border-color: color-mix(in srgb, var(--danger) 76%, #fff);
  }
}
/* Visible at rest, like the rank it sits in: a trigger that only appears on
   hover reads as a row with no menu, and is unreachable by touch. */
.kebab-trigger {
  width: 28px; height: 28px; border-radius: var(--r2); border: 0; background: none;
  color: var(--text-muted); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--fast) linear, color var(--fast) linear;
}
.kebab-trigger:hover { background: var(--border); color: var(--text-primary); }
.kebab-menu {
  position: absolute; top: 32px; right: 0; min-width: 148px; max-width: calc(100vw - 40px);
  display: none; flex-direction: column; gap: 2px;
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--r2);
  box-shadow: var(--lift-2); padding: 4px; z-index: 20;
}
.kebab.is-open .kebab-menu { display: flex; }
/* Opened upward when the row is near the foot of the window — set by
   setKebabOpen() from a real measurement, never from the row's index. */
.kebab.drops-up .kebab-menu { top: auto; bottom: 32px; }
/* min-height, not height, and the label does not wrap: a fixed 32px cut the
   second line off "Google Contacts থেকে" in the import menu, and a menu item
   that is half a sentence reads as a rendering fault. */
.kebab-menu button {
  width: 100%; min-height: 32px; padding: 6px 8px; justify-content: flex-start;
  font: inherit; font-size: 13px; color: var(--text-primary); background: none; border: 0;
  border-radius: 4px; cursor: pointer; text-align: left; display: flex; align-items: center;
  white-space: nowrap;
}
.kebab-menu button:hover { background: var(--surface-hover); }
.kebab-menu button.danger { color: var(--danger); }
.kebab-menu button.danger:hover { background: var(--danger-wash); }

/* Page controls under a list. Only rendered when there is more than one page,
   so this never shows up as a stray row under six rows of contacts. */
.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-top: 12px;
}
/* Inside the table card it is the table's own footer, so it loses the outside
   margin and gains the hairline that separates it from the last row. */
.tbl-card > .pager {
  margin: 0; padding: 10px var(--s3); border-top: 1px solid var(--border);
}
.pager-buttons { display: flex; gap: 4px; align-items: center; }
.pager-count { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.pager-size { display: flex; align-items: center; gap: 6px; }
.pager-size select { height: 28px; padding: 0 6px; font-size: 13px; }

/* Numbered pages. A page number is a destination, so it is a square target
   with the figure in it, not a labelled button — at 28px the rank of them
   reads as one control rather than as six buttons in a row. */
.pg-num, .pg-arrow {
  display: grid; place-items: center;
  min-width: 28px; height: 28px; padding: 0 6px;
  border: 1px solid transparent; border-radius: var(--r1);
  background: none; color: var(--text-secondary);
  font: inherit; font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  cursor: pointer; transition: background var(--fast) linear,
                               color var(--fast) linear,
                               border-color var(--fast) linear;
}
.pg-num:hover, .pg-arrow:not(:disabled):hover {
  background: var(--surface-hover); color: var(--text-primary);
}
/* The current page is bordered, not filled: a filled square here would be the
   brightest thing on a screen whose only bright element is a live call. */
.pg-num.is-on {
  border-color: var(--accent); color: var(--accent-ink); font-weight: 600;
}
.pg-arrow:disabled { opacity: .35; cursor: default; }
.pg-arrow svg { width: 15px; height: 15px; }
.pg-gap { padding: 0 2px; color: var(--text-secondary); font-size: 12.5px; user-select: none; }
.pg-num:focus-visible, .pg-arrow:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* ── the contact table ───────────────────────────────────────────── */

/* A sortable header is a button filling the cell, so the whole header is the
   target rather than the few pixels of the word. */
.th-sort { padding: 0; }
.th-sort button {
  display: flex; align-items: center; gap: 5px;
  width: 100%; height: 100%; padding: 0 var(--s3);
  border: 0; background: none; cursor: pointer;
  font: inherit; color: inherit; text-align: left;
}
/* Secondary, not the muted grey the other headers wear. A sortable header is
   an interactive control, and --text-muted measures 2.48:1 on the header strip
   — fine for a label nobody presses, under the floor for a button. */
.th-sort button { color: var(--text-secondary); }
.th-sort button:hover { color: var(--text-primary); }
.th-sort.is-on button { color: var(--text-primary); font-weight: 600; }
/* The idle caret is visible at rest, faint. It is the both-ways chevron, so it
   reads as "this can be sorted" rather than as a column already sorted; the
   active column swaps to a single arrow in the accent, which is what tells the
   two apart. Hiding it until hover meant nothing on the page said the columns
   were sortable at all. */
.th-sort svg { width: 13px; height: 13px; opacity: .4; transition: opacity var(--fast) linear; }
.th-sort:hover svg, .th-sort button:focus-visible svg { opacity: .7; }
.th-sort.is-on svg { opacity: 1; color: var(--accent); }
.th-sort button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Actions is the last column and keeps its width from the rule above; the
   word is there for a screen reader and for the eye to land on. */
.th-actions { text-align: right; }

/* Columns sized to what they hold, so the name does not take half the table
   and leave the company floating in the middle of a gap. Percentages rather
   than auto: a page where most companies are blank otherwise gives the column
   almost nothing, and the widths then change from page to page as the content
   does — which is what makes a list feel unstable while scrolling it. */
/* Counted from the end, because the checkbox column is only there for somebody
   who can act on a selection — so the name is not always the same child index.
   Last is actions, then the number, then the company, then the name. */
.tbl-contacts { table-layout: fixed; }
.tbl-contacts th:nth-last-child(3) { width: 26%; }  /* company */
.tbl-contacts th:nth-last-child(2) { width: 24%; }  /* number  */
.tbl-contacts td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The one cell holding an open menu stops clipping.
   `overflow: hidden` above is what ellipsizes a long contact name, so it has
   to stay — but it also clips the kebab MENU, which is positioned out of the
   actions cell. The menu reported a sensible getBoundingClientRect() the whole
   time; what proved it was elementFromPoint() at the menu's own centre coming
   back as the .row-act BEHIND it. Same lesson as .tbl-card/.ov-card: every
   clipping ancestor has to be walked, and a cell is one of them.
   Scoped to :has(.kebab.is-open) so an ordinary row still ellipsizes. */
.tbl-contacts td:has(.kebab.is-open) { overflow: visible; }

/* On a phone the company goes, never the number. A number is what somebody
   opens this screen to find, and .tbl-card clips rather than scrolls — so
   keeping all three columns hid half of every number with nothing to say so,
   which no overflow check catches because the page itself never overflowed. */
@media (max-width: 720px) {
  .tbl-contacts th:nth-last-child(3),
  .tbl-contacts td:nth-last-child(3) { display: none; }
  .tbl-contacts th:nth-last-child(2) { width: 42%; }
  /* The rank of three eats a third of a 420px row. Two buttons and no border
     gap is what fits beside a name and a number; edit and delete are the two
     that matter, and the ticket action stays in the row's own detail view. */
  .tbl-contacts th:last-child, .tbl-contacts td:last-child { width: 78px; }
  .tbl-contacts .kebab { gap: 4px; }
  /* The middle of a rank of three — the ticket button. Hidden rather than
     rebuilt in JS so the row keeps one markup shape at every width. */
  .tbl-contacts .kebab .row-act:nth-child(2):not(:last-child) { display: none; }
}

/* The checkbox column. Sized to the box plus its target, not to the header
   word, so it does not steal width from the name. */
th.pick, td.pick { width: 40px; padding-left: var(--s3); padding-right: 0; }
th.pick input, td.pick input {
  width: 15px; height: 15px; margin: 0; cursor: pointer;
  accent-color: var(--accent);
}
/* A picked row stays marked while the page is scrolled past it — hover alone
   would lose track of what is selected the moment the pointer moves. */
tbody tr.is-picked { background: var(--accent-wash); }
tbody tr.is-picked:hover { background: var(--accent-wash-2); }

/* Name cell: the disc and the name are one unit.

   Scoped with :has(> .nm) rather than declared as a bare `.who`. Two unrelated
   components chose the same class name: this table cell, and the call row's
   identity block up at line 278, whose .n (name) and .m (number) are meant to
   STACK. This rule is later in the file and the same weight, so it won — it
   turned every call row's identity into one flex line, collapsing the name to
   the width of its direction chip and leaving nothing but an ellipsis, which
   read as a stray mid-dot between the arrow and the number. One selector here
   rather than a phone-width override at the call row, because the collision
   was never about the phone: it was wrong at 1440px too, just wide enough to
   hide it. */
.who:has(> .nm) { display: flex; align-items: center; gap: 10px; min-width: 0; }
.who .nm { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The initials disc is declared beside .avatar itself, further down this file
   — see the note there for why it cannot live here. */

/* The number, with the glyph that says it is one. */
.phone-cell { display: inline-flex; align-items: center; gap: 7px; }
.phone-cell svg { width: 14px; height: 14px; color: var(--text-muted); flex: none; }

/* What you can do with the selection. Appears above the table only once
   something is selected, so it is never an empty strip. */
.bulkbar {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  margin-bottom: var(--s3); padding: var(--s2) var(--s3);
  background: var(--accent-wash); border: 1px solid var(--accent);
  border-radius: var(--r2);
}
/* display:flex beats the hidden attribute's UA display:none, so the bar stayed
   on screen reading "0 selected" with every action live. Caught on the phone
   screenshot, not by the overflow check. */
.bulkbar[hidden] { display: none; }
.bulkbar-n {
  font-size: 12.5px; font-weight: 600; color: var(--accent-ink);
  font-variant-numeric: tabular-nums; margin-right: auto;
}
.bulkbar .linklike { font-size: 12.5px; }

/* The OAuth redirect URI, shown to be copied into Google Cloud rather than
   described in a sentence — it has to match there character for character. */
.redirect-uri {
  display: block; margin-top: 4px; padding: 8px 10px;
  background: var(--surface-hover); border: 1px solid var(--border);
  border-radius: var(--r2); font-size: 13px; color: var(--text-primary);
  word-break: break-all; user-select: all;
}

/* Every input and select in the console, not only ones inside a .field
   wrapper — an in-table picker like the queue hold-music selector rendered
   as an unstyled browser-default white box on a dark page otherwise,
   caught only by actually opening the Queues screen with data in it. */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i), select, textarea {
  font: inherit; font-size: 13px; color: var(--text-primary);
  height: 36px; padding: 0 11px; width: 100%; min-width: 0;
  border: 1px solid var(--border-strong); border-radius: 9px; background: var(--surface);
  transition: border-color var(--fast) var(--ease);
}
input:hover, select:hover, textarea:hover { border-color: var(--text-muted); }
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] { border-color: var(--danger); background: var(--danger-wash); }
input:disabled, select:disabled, textarea:disabled { background: var(--surface-sunken); color: var(--text-muted); cursor: not-allowed; }
textarea { height: auto; min-height: 80px; padding: 9px 11px; resize: vertical; }
select {
  appearance: none; padding-right: 30px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394A0AE' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
}
input[type="checkbox"], input[type="radio"] { width: 16px; height: 16px; accent-color: var(--accent); margin: 0; }
/* align-content: start is what keeps a row of fields on one line. A .field
   sits in a form-grid cell that stretches it to the row's height, and a grid's
   rows then share the slack equally — so a field with no hint text beside one
   with a hint had its control pushed down by half the difference. Every
   two-column form showed it; the IVR editor, with a select-and-three-buttons
   stack beside a plain input, showed it worst. */
.field { display: grid; gap: 6px; min-width: 0; margin-bottom: 14px; align-content: start; }
/* `display: grid` above outranks the UA sheet's `[hidden] { display: none }`,
   so every `el.hidden = true` on a field was silently ignored — the TTS form's
   Model field showed for Google, which is the one provider that has no model.
   Same class, plus the attribute: it wins by weight, not by !important. The
   two rules at .bulkbar[hidden] and .panel-form [hidden] are the same fix
   applied locally before anybody noticed it was general. */
.field[hidden] { display: none; }
.field > label { font-size: 12.5px; font-weight: 500; color: var(--text-primary); }
.field > label .req { color: var(--danger); margin-left: 2px; }

/* `.help` had NO bare rule for its whole life — it was styled only inside
   `.field`, so the other 50-odd uses across the console inherited body colour
   at body size and rendered as full-strength primary ink (#0F172A), the same
   weight as the heading above them. Measured off the running page, not
   guessed: a supporting line was the loudest text in its own panel.
   One rule here fixes every one of them. */
.help {
  font-size: 12.5px; line-height: 1.5; color: var(--text-muted);
  /* Space BELOW as well as above. The first version set `0` on the bottom,
     replacing the browser's default paragraph margin, and a hint under a
     field ran straight into the next row — measured on the settings panel,
     where the name hint touched the email label. A hint is attached to what
     is above it, so the top margin stays smaller than the bottom. */
  margin: var(--s2) 0 var(--s4);
}
/* Only where the hint ends a PADDED container does its bottom margin become
   double spacing. Inside a bare wrapper div — which is where nameForm() puts
   it — :last-child would strip the margin and let the hint run into whatever
   follows the wrapper, which is exactly what it did: the name hint touched
   the email label. Named containers only, never a bare :last-child. */
.panel-body > .help:last-child,
.drawer-body > .help:last-child,
.card-form > .help:last-child { margin-bottom: 0; }
.help.err { color: var(--danger); }

/* Inside a field, the grid's own `gap: 6px` already separates the hint from
   the input, and `.field`'s margin-bottom separates the row from the next.
   `.help`'s standalone margins (8px top, 16px bottom) stacked on both, so a
   field carrying a hint cost 38px below its input against 14px without one —
   which is what read as a loose, uneven form on the limits drawer. Measured,
   not guessed. The bare `.help` rule keeps its margins for the ~50 uses that
   are NOT in a field and have no grid gap to lean on. */
.field .help { font-size: 11.5px; color: var(--text-muted); margin: 0; }
.field .err { font-size: 11.5px; color: var(--danger); }
/* On/off that saves at once. A checkbox inside a form saves on submit. */
.switch { position: relative; width: 38px; height: 22px; border-radius: 11px; background: var(--border-strong); border: 0; cursor: pointer; flex: none; transition: background var(--fast); }
.switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.2); transition: transform var(--fast); }
.switch[aria-checked="true"] { background: var(--accent); }
.switch[aria-checked="true"]::after { transform: translateX(16px); }

/* A unit belongs against its figure. "Max wait (seconds)" makes the reader
   carry the unit from the label down to the box; this puts it where the
   number lands. */
.unit { position: relative; }
.unit input { padding-right: 62px; }
.unit .u {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  font-size: 11px; color: var(--ink-faint); letter-spacing: .04em;
  pointer-events: none;
}

/* Two columns where a form has more than a few fields. One column of eight
   inputs is a scroll; two is a shape the eye can take in. */
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s5); }
@media (max-width: 720px) { .field-grid { grid-template-columns: 1fr; } }

/* ── feedback ─────────────────────────────────────────────────────── */
/* Alert: inline, persistent, names the fix. */
.alert { display: flex; gap: 10px; align-items: flex-start; padding: 11px 13px; border-radius: 9px; font-size: 13px; margin-bottom: 14px; }
.alert svg { width: 15px; height: 15px; flex: none; margin-top: 1px; }
.alert b { font-weight: 600; }
.alert .alert-detail { display: block; margin-top: 4px; font-family: var(--font-num); font-size: 11.5px; opacity: .85; overflow-wrap: anywhere; }
.alert.info { background: var(--blue-wash);   color: var(--blue); }
.alert.warn { background: var(--warn-wash);   color: var(--warn); }
.alert.bad  { background: var(--danger-wash); color: var(--danger); }
.alert.ok   { background: var(--accent-wash); color: var(--accent-ink); }
.alert .alert-text { color: var(--text-secondary); }
.alert .alert-text b { color: inherit; }
.alert.warn .alert-text b { color: var(--warn); } .alert.bad .alert-text b { color: var(--danger); }
.alert.info .alert-text b { color: var(--blue); } .alert.ok .alert-text b { color: var(--accent-ink); }

/* Toast: bottom-left, four seconds, for a completed act. Never for errors —
   an error is an alert beside the thing that failed. */
.toast {
  position: fixed; bottom: 20px; left: 20px; z-index: 90;
  display: inline-flex; gap: 10px; align-items: center;
  background: var(--text-primary); color: var(--bg);
  padding: 10px 14px; border-radius: 10px; font-size: 13px; box-shadow: var(--lift-3);
  animation: rise var(--med) var(--ease);
}
.toast svg { width: 15px; height: 15px; color: var(--live); }
.toast.is-bad { background: var(--danger); color: #fff; }
.toast.is-bad svg { color: #fff; }
@keyframes rise { from { opacity: 0; transform: translateY(8px); } }

/* The console is a desktop tool; small screens are a courtesy, not the target.
   The topbar sheds identity and chrome rather than wrapping into itself. */
@media (max-width: 900px) {
  .shell, .shell.no-phone {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr);
  }
  .side { border-left: none; border-top: 1px solid var(--line); }

  /* Off-canvas drawer behind the ☰ in the top bar, never a scrolling strip. */
  .sidebar {
    position: fixed; top: 0; bottom: 0; left: 0; width: 264px; z-index: 80;
    transform: translateX(-100%); transition: transform var(--med) var(--ease);
    grid-row: auto; border-right: 0;
  }
  .sidebar.is-open { transform: none; box-shadow: var(--lift-3); }
  .rail-toggle { display: none; }
  .shell.is-rail, .shell.is-rail.no-phone { grid-template-columns: minmax(0, 1fr); }
  .shell.is-rail .side-link > span:not(.side-count),
  .shell.is-rail .rail-head-t, .shell.is-rail .rail-foot-t,
  .shell.is-rail .side-group > summary > span:not(.chev) {
    position: static; width: auto; height: auto; clip-path: none;
  }
  .shell.is-rail .side-link { justify-content: flex-start; padding: 0 12px; gap: 11px; }
  .shell.is-rail .rail-head { flex-direction: row; padding: 16px 16px 14px; }
  .bar-icon.burger { display: inline-flex; }
  .rail-scrim { display: block; }
  .topbar { grid-column: 1; height: auto; padding: var(--s3) var(--s4); gap: var(--s3); flex-wrap: wrap; }
  .topbar > span { display: none; }          /* agent identity: shown in the panel */
  /* The bar has picked up the burger and started wrapping, so the clock
     gives up its second line: the time is the half anybody reads, and the
     date is a fact that changes once a day. */
  .hclock-d { display: none; }
  .presence span:last-child { display: none; }
  .main { padding: var(--s4); }
  .main > * { max-width: none; }
}

/* ── drawer ───────────────────────────────────────────────────────── */

/* Inspect a record without leaving the list. A drawer, not a modal: the list
   stays on screen, so you keep your place in it. */
.scrim { position: fixed; inset: 0; z-index: 70; background: rgba(15,23,42,.36); opacity: 0; pointer-events: none; transition: opacity var(--med) var(--ease); }
.scrim.is-on { opacity: 1; pointer-events: auto; }
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(420px, 92vw); z-index: 71;
  background: var(--surface); border-left: 1px solid var(--border); box-shadow: var(--lift-3);
  transform: translateX(100%); transition: transform var(--med) var(--ease);
  display: flex; flex-direction: column;
}
.drawer.is-on { transform: none; }
.drawer > header { display: flex; gap: 12px; align-items: center; padding: 16px 18px; border-bottom: 1px solid var(--border); }
.drawer > header .t { flex: 1; min-width: 0; }
.drawer > header h2 { margin: 0; font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.drawer > header .sub { font-size: 12px; color: var(--text-muted); }
.drawer-body { flex: 1; overflow-y: auto; padding: 18px; }
.drawer-body h3 { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); font-weight: 600; margin: 18px 0 9px; }
.drawer-body h3:first-child { margin-top: 0; }
.drawer-foot { border-top: 1px solid var(--border); padding: 12px 18px; display: flex; gap: 8px; }
.drawer-foot .btn-danger { margin-left: auto; }
/* ── the contact drawer ───────────────────────────────────────────── */

/* A section heading that says WHOSE data follows. If a figure here is wrong,
   whether to fix it in this console or in the customer's own CRM depends on
   the answer, so the agent must never have to guess. */
.drawer-body h3 { display: flex; align-items: center; gap: 8px; }
.cd-from {
  margin-left: auto; font-size: 9.5px; letter-spacing: .04em; font-weight: 600;
  color: var(--violet); display: inline-flex; align-items: center; gap: 5px;
}
.cd-from::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  background: var(--violet); flex: none;
}

/* How often, how recently, what is still open. This was a whole "Overview"
   tab repeating the two figures the ringing panel showed ten seconds earlier;
   as a strip it costs three lines instead of a pane. */
.cd-figs { display: flex; gap: var(--s2); margin-bottom: var(--s5); }
/* The CRM's marks on a customer (VIP, owner, language, do-not-call). */
.cd-marks { margin: var(--s2) 0 var(--s3); }
.cd-fig {
  flex: 1; min-width: 0; padding: var(--s3);
  border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-sunken);
}
.cd-fig .n { font-size: 19px; line-height: 1.1; letter-spacing: -.02em; }
.cd-fig .l {
  font-size: 10px; color: var(--text-muted); margin-top: 3px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* A note is prose: the sentence IS the record, so it wraps instead of being
   clipped to one line the way a title does. */
.cd-note {
  font-size: 12.5px; line-height: 1.55; color: var(--text-primary);
  white-space: normal;
}

.d-meters { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 18px; }
.d-meters > div { background: var(--surface-sunken); border-radius: 9px; padding: 10px 12px; }
.d-meters .v { font-size: 18px; font-weight: 700; letter-spacing: -.02em; }
.d-meters .v.on { color: var(--live); }
.d-meters .k { font-size: 11.5px; color: var(--text-secondary); }
.kv { display: grid; grid-template-columns: 110px 1fr; gap: 9px 12px; font-size: 13px; align-items: center; margin: 0; }
.kv dt { color: var(--text-secondary); font-size: 12.5px; }
.kv dd { margin: 0; font-variant-numeric: tabular-nums; }

/* ── the estate overview: Home, Companies, Health ──────────────────
   The three screens the owner approved from preview-batch1.html. Everything
   here is that preview's CSS, translated onto the console's own tokens.

   Named .ov-* where the preview used a bare name the console already owns:
   .side is the phone column, .card is a padded panel. A collision there
   would have re-skinned half the console by accident. */
.ov-grid { display: grid; grid-template-columns: minmax(0,1fr) 268px; gap: 20px; align-items: start; }
@media (min-width: 1800px) { .ov-grid { grid-template-columns: minmax(0,1fr) 300px; gap: 24px; } }
.ov-col { display: grid; gap: 18px; min-width: 0; align-content: start; }
/* Sticky, because the rail is as short as the role's permissions make it: an
   owner fills it with four cards, a supervisor or agent has one, and a single
   card against a page of call rows left a metre of empty gutter that read as
   a column that had failed to load. Following the scroll makes the one card
   useful for the whole page instead. `.main` is the scroller, not the window,
   so `top` is measured inside it. */
.ov-side {
  display: grid; gap: 18px; min-width: 0; align-content: start;
  position: sticky; top: 0; max-height: calc(100vh - 88px); overflow: auto;
  scrollbar-width: none;
}
.ov-side::-webkit-scrollbar { width: 0; }
/* The column already spaces its children by its own gap, so the tab strip
   does not carry the bottom margin it needs above a bare table. */
.ov-col > .tabs { margin-bottom: 0; }
.two { display: grid; grid-template-columns: minmax(0,1.55fr) minmax(280px,1fr); gap: 18px; }

/* The overview's card: header + body own their padding, unlike the console's
   older .card which pads itself. A table or a list has to reach the card's
   own edges, and a padded shell cannot let it. */
.ov-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r3);
  box-shadow: var(--lift-1); min-width: 0; overflow: hidden; }
/* The section card wrapping a table clips too, so suspending it on .tbl-card
   alone left the menu invisible for a second reason — found by asking which
   element actually paints at the menu's centre, not by looking. Same scope and
   same reasoning as .tbl-card.has-menu-open above. */
.ov-card.has-menu-open { overflow: visible; }
.ov-card > header { display: flex; align-items: center; gap: 10px; padding: 16px 18px 0; flex-wrap: wrap; }
.ov-card > header h2 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; display: flex; gap: 9px; align-items: center; }
.ov-card > header .sub { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
.ov-card > header .right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.ov-card .body { padding: 16px 18px 18px; }
/*
 * A form handed straight to sectionCard() gets the same inset as .body.
 *
 * sectionCard() appends its body with no wrapper, which is right for a table
 * — a table owns its own edges and a padded one would float inside the card.
 * A form does not: measured on the storage screen, the retention form started
 * 0px below the header and its first label sat 1px from the card's left
 * border, so the heading and the field labels read as one run of text.
 */
.ov-card > .card-form { padding: 16px 18px 18px; }
/* An empty state inside a card is already framed by the card, so it drops its
   own dashed border and grows to fill the height a taller sibling in the same
   `.two` row sets — otherwise "no calls" floats in a white void. */
.ov-card { display: flex; flex-direction: column; }
.ov-card > .empty, .ov-card > .plan-state > .empty {
  border: 0; background: none; border-radius: 0;
  flex: 1; display: flex; flex-direction: column; justify-content: center;
  /* A column flex child stretches to the cross axis, which turned the empty
     state's one action into a full-card-wide bar. Centre it instead — the
     button keeps the width its own label asks for. */
  align-items: center;
}
.ov-card .tbl-card { border: 0; border-radius: 0; box-shadow: none; margin-bottom: 0; }

/* A call list inside a card. `.rows` carries no padding of its own — it was
   built to sit on the page ground, where the page's own gutter supplied it —
   so inside a card the rows would butt against both edges. The `> ` keeps
   this off the queue strip's nested lists, which set their own inset.
   Matched at .ov-card > .rows (0,2,0) rather than with !important: the only
   other rule touching .rows is the bare element selector above it. */
.ov-card > .rows, .ov-card > div > .rows { padding: 0 18px; }
.ov-card > .rows:last-child, .ov-card > div > .rows:last-child { padding-bottom: 14px; }
.ov-card > div > .part { padding: var(--s3) 18px var(--s2); }
.ov-card > header + .rows, .ov-card > header + div > .part:first-child { margin-top: 10px; }
.hd-ic { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; flex: none; background: var(--wash, var(--surface-sunken)); }
.hd-ic svg { width: 17px; height: 17px; color: var(--hue, var(--text-secondary)); }
.live-tag { display: inline-flex; gap: 6px; align-items: center; height: 22px; padding: 0 9px; border-radius: 99px;
  background: var(--live-wash); color: var(--accent-ink); font-size: 11.5px; font-weight: 600; }
:root[data-theme="dark"] .live-tag { color: var(--accent); }
.live-tag i { width: 6px; height: 6px; border-radius: 50%; background: var(--live); display: block; }

/* ── greeting ─────────────────────────────────────────────────────
   Home opens on a person, not on a group label — so it uses this rather than
   pageHead(), whose eyebrow the reference does not have. */
.greet { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 4px; }
.greet .hi { font-size: 13px; color: var(--text-secondary); }
/* Flex so the account's state pill sits beside the name at its own size
   rather than inheriting the 26px heading. Wraps at a narrow width instead
   of pushing the pill off the side of a long company name. */
.greet h1 { font-size: 26px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2; margin: 2px 0 0;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.greet h1 .tag { font-size: 11.5px; font-weight: 600; letter-spacing: 0; }
.greet p { font-size: 13px; color: var(--text-secondary); margin-top: 3px; }
/* A tenant's greeting carries the one button an agent needs, and nothing
   else — the date and clock that used to sit beside it moved to the top bar
   (.hclock), where they belong to the app rather than to this page. The
   estate's greeting has no action, so this block is simply absent there. */
.greet-acts { margin-left: auto; display: flex; align-items: center; gap: 14px; }
@media (max-width: 640px) {
  .greet { flex-wrap: wrap; }
  .greet-acts { margin-left: 0; width: 100%; }
}

/* ── chart ────────────────────────────────────────────────────────
   Offered as a grey backdrop bar, answered in accent, and the answered trend
   as a line over both. One scale, a real axis, per-bar tooltip. */
.chart-wrap { position: relative; }
.chart-wrap svg { width: 100%; height: auto; overflow: visible; display: block; }
.chart-wrap .bar-g { cursor: pointer; }
.chart-wrap .bar-g rect.hit { fill: transparent; }
.chart-wrap .bar-g .cap { transition: opacity var(--fast) var(--ease); }
.chart-wrap svg:hover .bar-g:not(:hover) .cap { opacity: .55; }
.legend { display: flex; gap: 14px; font-size: 12px; color: var(--text-secondary); padding: 0 18px 14px; flex-wrap: wrap; }
.legend span { display: inline-flex; gap: 6px; align-items: center; }
.legend i { width: 10px; height: 10px; border-radius: 3px; display: block; }
.legend i.line { height: 2px; width: 14px; border-radius: 1px; }
.tip { position: absolute; pointer-events: none; background: var(--surface); border: 1px solid var(--border);
  border-radius: 9px; box-shadow: var(--lift-2); padding: 9px 11px; font-size: 12px; min-width: 150px;
  opacity: 0; transform: translate(-50%,-8px); transition: opacity var(--fast) var(--ease); z-index: 5; }
.tip.is-on { opacity: 1; }
.tip b { display: block; font-size: 12px; margin-bottom: 5px; }
.tip .row { display: flex; gap: 8px; align-items: center; justify-content: space-between; color: var(--text-secondary); }
.tip .row .k { display: inline-flex; gap: 6px; align-items: center; }
.tip .row .k i { width: 12px; height: 2px; border-radius: 1px; display: block; }
.tip .row strong { color: var(--text-primary); font-variant-numeric: tabular-nums; }
.tip::after { content: ''; position: absolute; left: 50%; bottom: -6px; width: 10px; height: 10px; background: var(--surface);
  border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
  /* --tip-arrow is how far the bar sits from the box's centre, which is zero
     until wireChart clamps the box against an edge. Clamped inside the box so
     an extreme value cannot walk the arrow off its own corner. */
  transform: translateX(calc(-50% + clamp(calc(14px - 50%), var(--tip-arrow, 0px), calc(50% - 14px)))) rotate(45deg); }

/* ── service rows ─────────────────────────────────────────────────── */
.svc { margin: 0; padding: 6px 0 4px; list-style: none; }
.svc li { display: flex; gap: 12px; align-items: center; padding: 11px 18px; border-top: 1px solid var(--border); }
.svc li:first-child { border-top: 0; }
.svc .ic { width: 30px; height: 30px; border-radius: 8px; background: var(--surface-sunken); display: grid; place-items: center; flex: none; }
.svc .ic svg { width: 15px; height: 15px; color: var(--text-secondary); }
.svc .nm { font-size: 13px; font-weight: 500; flex: 1; min-width: 0; }
.svc .nm small { display: block; font-size: 11.5px; color: var(--text-muted); font-weight: 400; overflow-wrap: anywhere; }
.svc .tag { flex: none; }
/* A service row that links somewhere is a button, but must read as a row. */
.svc li.is-link { cursor: pointer; }
.svc li.is-link:hover { background: var(--surface-hover); }

/* ── attention ────────────────────────────────────────────────────── */
.att-ic { width: 26px; height: 26px; border-radius: 7px; background: var(--rose-wash); display: grid; place-items: center; flex: none; }
.att-ic svg { width: 14px; height: 14px; color: var(--rose); }
.badge { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; background: var(--rose); color: #fff;
  font-size: 11px; font-weight: 700; display: grid; place-items: center; font-variant-numeric: tabular-nums; }
.att-sub { padding: 4px 18px 10px; margin: 0; font-size: 12px; color: var(--text-secondary); }
.att-list { margin: 0; padding: 0; list-style: none; }
.att-list li { display: flex; gap: 10px; align-items: center; padding: 10px 18px; border-top: 1px solid var(--border);
  cursor: pointer; width: 100%; text-align: left; background: none; border-left: 0; border-right: 0; border-bottom: 0; font: inherit; color: inherit; }
.att-list li:hover { background: var(--surface-hover); }
.att-list li > i { width: 7px; height: 7px; border-radius: 50%; flex: none; display: block; }
.att-list .t { flex: 1; min-width: 0; }
.att-list .t b { display: block; font-size: 12.5px; font-weight: 600; line-height: 1.3; }
.att-list .t small { display: block; font-size: 11.5px; color: var(--text-secondary); }
.att-list li > svg { width: 14px; height: 14px; color: var(--text-muted); flex: none; }
.att-foot { padding: 12px 18px; }
.link { font-size: 12px; color: var(--accent-ink); font-weight: 500; display: inline-flex; gap: 5px; align-items: center;
  background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit; }
:root[data-theme="dark"] .link { color: var(--accent); }
.link svg { width: 13px; height: 13px; color: currentColor; }

/* ── insights promo ───────────────────────────────────────────────── */
.promo { position: relative; overflow: hidden; border-radius: var(--r3); border: 1px solid var(--border);
  background: linear-gradient(160deg, var(--surface) 0%, var(--surface) 40%, var(--accent-wash) 100%);
  box-shadow: var(--lift-1); padding: 18px; }
.promo .hd { display: flex; gap: 9px; align-items: center; font-size: 12.5px; font-weight: 600; margin-bottom: 12px; }
.promo .hd svg { width: 16px; height: 16px; color: var(--accent); }
.promo h2 { margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -.015em; line-height: 1.25; max-width: 200px; }
.promo p { font-size: 12.5px; color: var(--text-secondary); margin: 8px 0 14px; max-width: 210px; }
.promo .wave { position: absolute; right: -30px; bottom: -40px; width: 200px; height: 160px; opacity: .9; pointer-events: none; }

/* ── queue strip ───────────────────────────────────────────────────
   Three right-now readings in one low card, below the four day figures.
   No sparkline and no delta anywhere in here on purpose: queue depth and
   agent presence are never stored, so there is no history behind any of
   these three and a trend line would be drawn out of nothing. The caption
   in the last cell says so rather than leaving the absence to be guessed. */
.qstrip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r3);
  box-shadow: var(--lift-1); overflow: hidden; }
.qcell { padding: 14px 16px; border-left: 1px solid var(--border); display: flex; gap: 12px;
  align-items: center; min-width: 0; }
.qcell:first-child { border-left: 0; }
.qcell .ic { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center;
  flex: none; background: var(--wash, var(--surface-sunken)); }
.qcell .ic svg { width: 17px; height: 17px; color: var(--hue, var(--text-secondary)); }
.qcell .t { min-width: 0; }
.qcell .t .k { font-size: 11.5px; color: var(--text-secondary); display: block; }
.qcell .t .v { font-size: 21px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2;
  display: flex; align-items: center; gap: 7px; }
.qcell .t .v.is-on { color: var(--live); }
.qcell .t small { font-size: 11px; color: var(--text-muted); display: block; }
.qstrip .now { margin-left: auto; display: flex; gap: 6px; align-items: center; font-size: 11px;
  color: var(--text-muted); }
.qstrip .now svg { width: 13px; height: 13px; color: currentColor; flex: none; }
/* The caption cell has no figure, so at one column it is a stray line under
   three readings rather than a fourth one. */
@media (max-width: 620px) { .qstrip .qcell:last-child { border-top: 1px solid var(--border); } }

/* ── quick actions ─────────────────────────────────────────────────
   A rank of rows, not buttons in a grid: each one is a sentence — what you
   would do, and why you would go there — and a grid of four tiles has room
   for the first half of that only. */
.qa-list { margin: 0; padding: 6px 0; list-style: none; }
.qa { display: flex; gap: 12px; align-items: center; width: 100%; padding: 11px 18px;
  text-align: left; border: 0; border-top: 1px solid var(--border); background: none;
  font: inherit; color: inherit; cursor: pointer; transition: background var(--fast) var(--ease); }
.qa-list li:first-child .qa { border-top: 0; }
.qa:hover { background: var(--surface-hover); }
.qa .ic { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center;
  flex: none; background: var(--wash, var(--surface-sunken)); }
.qa .ic svg { width: 17px; height: 17px; color: var(--hue, var(--text-secondary)); }
.qa .t { flex: 1; min-width: 0; }
.qa .t b { display: block; font-size: 13px; font-weight: 600; line-height: 1.3; }
.qa .t small { display: block; font-size: 11.5px; color: var(--text-secondary); }
.qa > svg { width: 14px; height: 14px; color: var(--text-muted); flex: none; }
.qa:hover > svg { color: var(--text-secondary); }

/* A card header's own search box. Sized to the header rather than the form
   grid, which is the only place the default input height belongs. */
.input-sm { height: 30px; width: 190px; min-width: 0; font-size: 12.5px; }
@media (max-width: 900px) { .input-sm { width: 100%; } }

/* ── activity ─────────────────────────────────────────────────────── */
.acts { margin: 0; padding: 0; list-style: none; }
.acts li { display: flex; gap: 11px; align-items: center; padding: 10px 18px; border-top: 1px solid var(--border); }
.acts .ic { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; flex: none; background: var(--wash, var(--surface-sunken)); }
.acts .ic svg { width: 14px; height: 14px; color: var(--hue, var(--text-secondary)); }
.acts .t { flex: 1; min-width: 0; }
/* Wrap, do not ellipsise: "+880 1712 345678 থেকে কল" truncated hides the
   very thing the row is about. Two lines is fine; half a number is not. */
.acts .t b { display: block; font-size: 12.5px; font-weight: 600; line-height: 1.35;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.acts .t small { font-size: 11.5px; color: var(--text-secondary); }
.acts .when { font-size: 11px; color: var(--text-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* ── health hero ──────────────────────────────────────────────────
   The switch is the one row that stops every call, so it gets the ring and
   the top slot rather than a tile in a row of five. */
.hero-sw { display: flex; gap: 16px; align-items: center; padding: 20px 22px; border-radius: var(--r3);
  border: 1px solid var(--border); background: linear-gradient(120deg, var(--accent-wash), var(--surface) 55%);
  box-shadow: var(--lift-1); flex-wrap: wrap; }
.hero-sw.is-down { background: linear-gradient(120deg, var(--rose-wash), var(--surface) 55%); }
.hero-sw .ring { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; flex: none;
  background: var(--surface); box-shadow: 0 0 0 2px var(--accent), 0 8px 20px var(--live-glow); }
.hero-sw.is-down .ring { box-shadow: 0 0 0 2px var(--rose); }
.hero-sw .ring svg { width: 24px; height: 24px; color: var(--accent); }
.hero-sw.is-down .ring svg { color: var(--rose); }
.hero-sw .t { min-width: 0; }
.hero-sw h2 { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -.015em; }
.hero-sw p { font-size: 12.5px; color: var(--text-secondary); margin: 2px 0 0; }
.hero-sw .mono { font-family: var(--font-num); font-size: 11.5px; color: var(--text-muted);
  margin-top: 6px; overflow-wrap: anywhere; }
.hero-sw > .tag { margin-left: auto; }

/* The Companies toolbar: search, filter chips, sort. Scoped rather than added
   to .toolbar, which caps its search at 340px and does not wrap — both right
   for a toolbar under a band, both wrong for this one. */
.ov-toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 0; }
.ov-toolbar .hsearch { flex: 1 1 180px; min-width: 150px; max-width: none; }
/* The chips must not push the sort select onto a second row: at 1440 the
   reference has search, chips and sort on one line, and a wrapped select then
   stretched to the card's full width because it was alone in its row. */
.ov-toolbar .chips { flex: 0 1 auto; min-width: 0; }
.ov-toolbar > select { flex: 0 0 auto; width: auto; margin-left: auto; }
.ov-toolbar .hsearch-i { width: 100%; }
/* Overrides .toolbar's 340px cap and its own border radius, which belong to
   a field that sits alone under a band. The padding it also needs cannot be
   set here — see the rule after `.toolbar input[type="search"]`, which is the
   same weight and declared later, so it wins on order. */
.ov-toolbar input[type="search"] { max-width: none; }
.ov-toolbar select {
  height: 34px; border: 1px solid var(--border); border-radius: 9px;
  background: var(--surface); color: var(--text-primary); font: inherit;
  font-size: 12.5px; padding: 0 9px; flex: none;
}
/* Five tiles that keep their width rather than collapsing to one per row. */
.strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px,1fr)); gap: 14px; margin-bottom: 0; }

.stat .more { position: absolute; top: 12px; right: 10px; color: var(--text-muted); width: 24px; height: 24px;
  border-radius: 6px; display: grid; place-items: center; border: 0; background: none; cursor: pointer; }
.stat .more:hover { background: var(--surface-hover); color: var(--text-secondary); }
.stat .more svg { width: 14px; height: 14px; }

/* ── responsive ───────────────────────────────────────────────────── */
@media (max-width: 1300px) {
  .ov-grid { grid-template-columns: 1fr; }
  /* One column: the rail is now a row under the page, so there is nothing
     for it to stick beside and a sticky strip would cover what is below it. */
  .ov-side {
    grid-template-columns: repeat(auto-fit, minmax(260px,1fr));
    position: static; max-height: none; overflow: visible;
  }
}
@media (max-width: 1280px) { .two { grid-template-columns: 1fr; } }
@media (max-width: 900px) {
  .greet { flex-wrap: wrap; }
  .ov-card > header .right { margin-left: 0; width: 100%; flex-wrap: wrap; }
}
@media (max-width: 640px) { .greet h1 { font-size: 22px; } }

/* ── confirm dialog (native <dialog>) ─────────────────────────────── */

/* Replaces window.confirm(), which cannot carry the name of the thing being
   destroyed, cannot be styled, and cannot ask somebody to type it back. */
dialog.dlg { border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); color: var(--text-primary); padding: 0; width: min(440px, 92vw); box-shadow: var(--lift-3); }
dialog.dlg::backdrop { background: rgba(15,23,42,.42); }
.dlg-body { padding: 20px; }
.dlg-body h2 { margin: 0 0 6px; font-size: 16px; font-weight: 600; }
.dlg-body p { margin: 0 0 12px; font-size: 13px; color: var(--text-secondary); }
.dlg-name { background: var(--danger-wash); border-radius: 9px; padding: 10px 12px; font-size: 13px; margin-bottom: 12px; }
.dlg-name b { color: var(--danger); }
.dlg-foot { border-top: 1px solid var(--border); padding: 12px 18px; display: flex; gap: 8px; justify-content: flex-end; }

/* ── admin forms ──────────────────────────────────────────────────── */

/* A section's action sits beside the thing it acts on, not in a toolbar
   somewhere else on the page. */
.sec-row { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s4); }

/* ── page head ────────────────────────────────────────────────────
   Eyebrow (the group), a 24px title, one line of live facts, actions right.
   No tile, no gradient, no counts inside it: counts are stat cards below.
   Replaces the band, which carried its own wash, its own hue and its own
   stat tiles — three decisions per screen where the approved world has one. */
.page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 18px; }
.ph-text { min-width: 0; }
.ph-eyebrow { font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); font-weight: 600; }

/* The way back, for a screen whose group the rail does not draw. It sits in
   the eyebrow's slot and at the eyebrow's size, because it is doing the
   eyebrow's job — saying where you are — and it must not become a second
   heading competing with the title under it. Not uppercased: it carries a
   screen's name ("Settings"), not a group label, and the primary locale is
   Bangla, where uppercase does nothing. */
.ph-back {
  display: inline-flex; align-items: center; gap: 4px;
  font: inherit; font-size: 11.5px; font-weight: 600;
  padding: 2px 6px 2px 2px; margin-left: -4px;
  border: 0; border-radius: var(--r1); background: none;
  color: var(--text-muted); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.ph-back:hover { background: var(--surface-hover); color: var(--text-primary); }
.ph-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ph-back svg { width: 13px; height: 13px; flex: none; }
.ph-text h1 { margin: 2px 0 0; font-size: 24px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2; color: var(--text-primary); }
.ph-text p { margin: 3px 0 0; font-size: 13px; color: var(--text-secondary); max-width: 70ch; }
.ph-acts { margin-left: auto; display: flex; gap: 8px; flex: none; flex-wrap: wrap; align-items: center; padding-top: 4px; }
/* Two or more actions read right-to-left in importance, so the primary one
   sits rightmost where the eye ends. */
.ph-acts.is-reversed { flex-direction: row-reverse; }
/* Live screens keep it to one line so the first call row is not pushed down.
   Measured on the old band: the full head cost ~66px on Calls. */
.page-head.is-compact { margin-bottom: 14px; }
.page-head.is-compact .ph-eyebrow { display: none; }
.page-head.is-compact .ph-text h1 { font-size: 18px; margin-top: 0; }
.page-head.is-compact:not(.keeps-sub) .ph-text p { display: none; }
/* Below this the title and two buttons do not share a line. */
@media (max-width: 620px) {
  .page-head { flex-wrap: wrap; }
  .ph-acts { width: 100%; margin-left: 0; padding-top: 0; }
}
/* The menu opens from the right edge of its trigger, so near the right of a
   narrow screen it would otherwise run off. */
.ph-acts .kebab-menu { right: 0; left: auto; }

/* ── the import menu ─────────────────────────────────────────────── */

/* Wider than an action menu, because each row carries a line explaining what
   it does. A bare label makes somebody guess whether "Google Contacts"
   uploads a file or connects an account. */
.imp-menu { min-width: 300px; padding: 6px; }
.imp-group {
  margin: 8px 8px 4px; font-size: 11px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase; color: var(--text-muted);
}
.imp-group:first-child { margin-top: 4px; }

.imp-choice {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 8px; border: 0; border-radius: var(--r2);
  background: none; font: inherit; text-align: left; cursor: pointer;
}
.imp-choice:hover { background: var(--surface-hover); }
.imp-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The trailing chevron is the only thing here allowed to grow a gap, so it
   pins right whatever the label's length. */
.imp-choice > svg:last-child {
  width: 15px; height: 15px; margin-left: auto; flex: none; color: var(--text-muted);
}

.imp-tile {
  width: 32px; height: 32px; border-radius: var(--r2); flex: none;
  display: grid; place-items: center;
}
.imp-tile svg { width: 16px; height: 16px; }
/* Tinted, not solid. These sit beside text in a menu rather than standing in
   for a person, so the wash is enough to tell the two sources apart without
   two saturated squares pulling the eye off the words. */
.imp-tile.is-violet { background: var(--violet-wash); color: var(--violet); }
.imp-tile.is-blue   { background: var(--blue-wash);   color: var(--blue); }

.imp-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.imp-title {
  font-size: 13px; font-weight: 500; color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.imp-hint {
  font-size: 11.5px; color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* What already happened. Not buttons: there is nothing to press on a finished
   import, and making these look pressable would promise a detail view that
   does not exist. */
.imp-recent:not(:empty) {
  margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--border);
}
.imp-past { display: flex; align-items: center; gap: 10px; padding: 7px 8px; }
.imp-past-i {
  width: 32px; height: 32px; border-radius: var(--r2); flex: none;
  display: grid; place-items: center;
  background: var(--accent-wash); color: var(--accent-ink);
}
.imp-past-i svg { width: 15px; height: 15px; }
.imp-past.is-bad .imp-past-i { background: var(--danger-wash); color: var(--danger); }

/* ── stat cards ───────────────────────────────────────────────────
   Icon tile in the card's hue, label, value, one line of context. One card
   per row may be the hero (live green value).

   auto-FILL, not auto-fit: with one tally, auto-fit collapses the empty
   tracks and stretches the single card across the whole row, which reads as
   a bug rather than as a count. */
.stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; margin-bottom: 18px; }
/* Five (or four) tiles across, not the auto-fill the rest of the console uses:
   the overview's rows are a fixed set, and auto-fill wrapped the last one onto
   a line of its own. Declared AFTER .stats deliberately — it is the same
   specificity, so source order is what decides it. */
.stats.ov-stats   { grid-template-columns: repeat(5, minmax(0,1fr)); margin-bottom: 0; }
.stats.ov-stats-4 { grid-template-columns: repeat(4, minmax(0,1fr)); margin-bottom: 0; }
@media (max-width: 1280px) {
  .stats.ov-stats   { grid-template-columns: repeat(3, minmax(0,1fr)); }
  .stats.ov-stats-4 { grid-template-columns: repeat(2, minmax(0,1fr)); }
}
@media (max-width: 640px) { .stats.ov-stats, .stats.ov-stats-4 { grid-template-columns: 1fr 1fr; } }
@media (max-width: 440px) { .stats.ov-stats, .stats.ov-stats-4 { grid-template-columns: 1fr; } }
.stat {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r3);
  box-shadow: var(--lift-1); padding: 14px 14px 12px;
  display: grid; gap: 3px; align-content: start; position: relative; min-width: 0;
  --hue: var(--text-secondary); --wash: var(--surface-sunken);
}
.stat.is-blue   { --hue: var(--blue);   --wash: var(--blue-wash); }
.stat.is-violet { --hue: var(--violet); --wash: var(--violet-wash); }
.stat.is-amber  { --hue: var(--amber);  --wash: var(--amber-wash); }
.stat.is-cyan   { --hue: var(--cyan);   --wash: var(--cyan-wash); }
.stat.is-rose   { --hue: var(--rose);   --wash: var(--rose-wash); }
.stat.is-brand  { --hue: var(--accent); --wash: var(--accent-wash); }
.stat.is-live   { --hue: var(--live);   --wash: var(--live-wash); }
.stat-ic { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: var(--wash); margin-bottom: 6px; }
.stat-ic svg { width: 16px; height: 16px; color: var(--hue); }
.stat-label { font-size: 12px; color: var(--text-secondary); }
.stat-value { font-size: 26px; font-weight: 700; letter-spacing: -.025em; line-height: 1.1; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
.stat.is-live .stat-value { color: var(--live); }
/* A count of zero is not a live state. Coloured, it announces "something is
   running" on a screen where nothing is — the one thing this product must
   never do. */
.stat.is-live .stat-value.is-zero { color: var(--text-muted); }
.stat.warn .stat-value { color: var(--ring); }
.stat-sub { font-size: 11.5px; color: var(--text-muted); display: flex; gap: 5px; align-items: center; min-height: 16px; }
.stat-sub.up { color: var(--accent-ink); } .stat-sub.down { color: var(--danger); }
.stat-spark { height: 34px; margin-top: 6px; }
.stat-spark svg { width: 100%; height: 34px; overflow: visible; display: block; }
.pulse { width: 9px; height: 9px; border-radius: 50%; background: var(--live); flex: none;
  box-shadow: 0 0 0 0 var(--live-glow); animation: pulse 2s var(--ease) infinite; }
@keyframes pulse { 70% { box-shadow: 0 0 0 9px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }

/* ── the superadmin dashboard ──────────────────────────────────────
   The estate's own numbers, its week, and what is up. Built on .stat so
   the dashboard and the counts on Companies and Health cannot drift into two
   designs for the same figure — that drift is what made this screen read as
   three unfinished dashboards instead of one system. */

/* Six tiles, not four: minmax is smaller here so the row still fits without
   wrapping a lone tile onto a second line. */
.dash-stats {
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  margin-top: 0; margin-bottom: var(--s5);
}
/* The card already carries its own ground; this only adds the positioning
   context the corner glyph needs. */
.dash-stat { position: relative; }
/* --text-muted is a caption colour and lands at 2.64:1 on the tile in light,
   under the 3:1 a non-text graphic needs to stay perceivable. Measured, not
   guessed: the glyph is the only thing telling six tiles apart at a glance,
   so it takes the secondary ink instead. */
.dash-stat-i {
  position: absolute; top: var(--s4); right: var(--s4);
  color: var(--text-secondary); display: inline-flex;
}
.dash-stat-i svg { width: 15px; height: 15px; }
/* The qualifier under the label — "longest 4:12". Small, and secondary
   rather than muted ink: --text-muted measures 2.64:1 on this tile's ground
   in light, under the 4.5:1 body text needs. */
.dash-stat-sub {
  margin-top: 3px; font-size: 11px; color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
/* The glyph takes the figure's colour when the figure is live, so the tile
   reads as one thing rather than a number beside an unrelated mark. */
.dash-stat.on .dash-stat-i { color: var(--live); }
.dash-stat.on .stat-value { color: var(--live); }
/* A zero is not a live state, so it keeps the ordinary ink and drags the
   glyph back with it — :has() rather than a sibling combinator because the
   glyph sits before the figure in the markup, and ~ only looks forward. */
.dash-stat.on .stat-value.is-zero { color: var(--text-primary); }
.dash-stat.on:has(.stat-value.is-zero) .dash-stat-i { color: var(--text-muted); }

/* The trend line under a tile's label — 14 real days, drawn thin. Secondary
   ink by default, not muted: a line is a non-text graphic same as the tile's
   own glyph, and --text-muted measured 2.64:1 in light here, under the 3:1
   floor a non-text graphic needs to stay perceivable — the same failure
   DESIGN.md already caught once on .dash-stat-i. Only a tile already wearing
   the live colour draws its line in it too, so the accent still means "a
   call is happening right now" and not "this number went up". */
.dash-stat-spark {
  display: block; width: 100%; height: 22px; margin-top: var(--s2);
}
.dash-stat-spark path {
  fill: none; stroke: var(--text-secondary); stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
.dash-stat-spark.is-live path { stroke: var(--live); }

/* The comparison line — "↑ 2 since last week". Up takes the live green, down
   takes danger red, flat stays secondary ink with no arrow: a dash of colour
   on every tile would be the thing DESIGN.md rules out. --text-muted measured
   2.64:1 here in light, under the 4.5:1 body text needs, same fix as the
   tile's own sub-caption above. */
.dash-stat-delta {
  display: flex; align-items: center; gap: 3px;
  margin-top: 5px; font-size: 11px; color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.dash-stat-delta svg { width: 11px; height: 11px; flex: none; }
/* Darkened 20% toward black rather than plain --live: at 11px this is small
   text (WCAG's large-text 3:1 floor does not apply, unlike the bold 22px
   tile number that also wears --live), and the token on its own measured
   3.20:1 in light, under the 4.5:1 body text needs. The mix still reads as
   the accent and clears 4.75:1 light / 6.04:1 dark. */
.dash-stat-delta.up { color: color-mix(in srgb, var(--live) 80%, black); }
.dash-stat-delta.down { color: var(--danger); }

/* The week beside the service list: two questions asked in one glance. The
   chart takes the room because a shape needs width to be a shape; the list is
   a column of three short rows and does not. */
.dash-pair {
  display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(240px, 1fr);
  gap: var(--s4); margin-bottom: var(--s5); align-items: start;
}
@media (max-width: 900px) { .dash-pair { grid-template-columns: 1fr; } }
.dash-panel {
  border: 1px solid var(--border); border-radius: var(--r3);
  padding: var(--s4) var(--s5) var(--s5);
  background: var(--surface); min-width: 0;
}
/* The panel supplies the padding, so the heading inside it starts at the top
   rather than carrying the section margin it would use on a bare page. */
.dash-panel > .sec:first-child { margin-top: 0; }

/* The right column: service health above recent activity, stacked rather
   than side by side — both are "what is the estate doing right now" and
   neither needs the chart's width. */
.dash-side { display: flex; flex-direction: column; gap: var(--s4); }

/* The chart's own heading carries the range toggle on its far side, same row
   — a control about the chart belongs next to the chart's title, not in a
   toolbar elsewhere on the page. */
.dash-chart-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); }

.range-toggle {
  display: flex; flex: none; border: 1px solid var(--border);
  border-radius: var(--r2); overflow: hidden;
}
.range-toggle button {
  border: 0; background: none; padding: 5px 10px; font-size: 12px;
  color: var(--text-secondary); cursor: pointer; font-variant-numeric: tabular-nums;
}
.range-toggle button + button { border-left: 1px solid var(--border); }
.range-toggle button:hover:not(.is-on) { color: var(--text-primary); }
/* The same treatment .segmented's chosen range wears, for the same reason:
   these two controls do one job — "which period am I looking at?" — and a
   sunken grey pill answered it so quietly the eye had to hunt. Brand, via
   accent-ink for the contrast, and never the live hue: a range picker is not
   an event. */
.range-toggle button.is-on {
  background: var(--accent-wash); color: var(--accent-ink); font-weight: 600;
}

/* Standing alone above the tile row rather than inline in a .dash-chart-head
   flex row, where a flex item shrink-wraps its content by default. A block
   parent gives a flex-display child `width: auto`, which stretches to fill
   the row instead — .range-toggle's own `flex: none` only governs how it
   behaves as somebody else's flex item, not its own intrinsic width, so the
   picker read as a wide two-tab bar instead of the compact pill every other
   filter in this console is. width: fit-content is the fix, plus the bottom
   margin .range-toggle otherwise has none of. */
.home-tile-toggle { width: fit-content; margin-bottom: var(--s3); }

/* The estate as a table, embedded directly rather than only reachable from
   Companies — see the reference dashboard this screen now matches. */
.dash-tenants { margin-bottom: var(--s3); }
.dash-tenants .tbl-card { border: 0; margin-top: var(--s3); }

/* "N live" beside the calls-in-progress heading — same pill shape as a
   service state, dedicated because it carries a live-dot rather than a
   status word. */
.live-pill {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--live) 14%, transparent);
  color: color-mix(in srgb, var(--live) 70%, black);
}
:root[data-theme="dark"] .live-pill { color: var(--live); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .live-pill { color: var(--live); }
}
.live-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--live); }

/* One call happening right now — the same row a supervisor reads on the
   Floor screen's monitor panel, minus the join controls (those act on
   someone else's call and belong on the screen built for that). */
.call-list { display: flex; flex-direction: column; }
.call-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) 0; border-bottom: 1px solid var(--border);
}
.call-row:last-child { border-bottom: 0; }
.call-lamp {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--live); box-shadow: 0 0 6px var(--live-glow);
}
/* A call that was never answered is not live, so it does not get the live
   colour — the one rule this palette exists to protect. */
.call-lamp.is-off { background: var(--border-strong); box-shadow: none; }
.call-who { min-width: 0; flex: 1; }
.call-name { font-size: 13px; font-weight: 600; }
/* --text-secondary, not --text-muted: the latter measured 2.64:1 on this
   surface in light, under the 4.5:1 body text needs — same fix repeated
   elsewhere in this file. */
.call-sub { font-size: 11.5px; color: var(--text-secondary); margin-top: 1px; }
.call-time { font-size: 13px; color: var(--text-secondary); flex: none; }

/* A queue's waiting count and a bar for how close its longest wait is to its
   own ceiling — the same max_wait_sec the Floor screen's breach warning
   already reads, just drawn as a fraction instead of a threshold flag. */
.queue-list { display: flex; flex-direction: column; }
.queue-row { padding: var(--s3) 0; border-bottom: 1px solid var(--border); }
.queue-row:last-child { border-bottom: 0; }
.queue-top { display: flex; align-items: baseline; gap: var(--s2); margin-bottom: 6px; }
.queue-name { font-size: 13px; font-weight: 600; }
.queue-nums { margin-left: auto; font-size: 11.5px; color: var(--text-secondary); }
/* --border, not --surface-sunken: the sunken tone sits only 5 steps off this
   panel's own background in dark theme (barely visible at 0% fill, which is
   the common case — most tenants have nobody waiting most of the time), and
   a progress track that vanishes at rest reads as a missing element rather
   than an empty one. */
.queue-bar {
  height: 5px; border-radius: 999px; background: var(--border); overflow: hidden;
}
.queue-bar span { display: block; height: 100%; background: var(--accent); border-radius: 999px; }

.svc-list { display: flex; flex-direction: column; }
.svc-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) 0; border-bottom: 1px solid var(--border);
}
.svc-row:last-child { border-bottom: 0; }
.svc-name { font-size: 13px; color: var(--text-primary); }
/* Pushed right, in a pill: the state is the answer, but the service is the
   question, and the question is what the eye scans down. A chip rather than
   bare text separates "is this a value or a state" at a glance — the same
   distinction a status tag makes on every table row in this console. */
.svc-pill {
  margin-left: auto; font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: var(--surface-sunken); color: var(--text-secondary);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Plain --live text on its own wash clears 4.5:1 in dark (6.75:1) but not in
   light (2.75:1) — darkening toward black helps light and actively hurts
   dark (dark wants brighter, not darker), so this is genuinely a per-theme
   fix rather than one mix serving both. */
.svc-pill.live {
  background: color-mix(in srgb, var(--live) 14%, transparent);
  color: color-mix(in srgb, var(--live) 70%, black);
}
:root[data-theme="dark"] .svc-pill.live { color: var(--live); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .svc-pill.live { color: var(--live); }
}
/* Same story as .svc-pill.live above: plain --warn on its own wash measured
   3.07:1 in light, under 4.5:1 — darkened for light, restored to full
   brightness for dark (8.31:1 unmixed there, comfortably clear already). */
.svc-pill.ring { background: var(--warn-wash); color: color-mix(in srgb, var(--warn) 75%, black); }
:root[data-theme="dark"] .svc-pill.ring { color: var(--warn); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .svc-pill.ring { color: var(--warn); }
}

/* The estate's own audit feed. Same row rhythm as .svc-row — this dashboard
   already reads top-to-bottom as tile → chart+services → activity, so a new
   list shape here would be a fourth idiom for no reason. */
.activity-list { display: flex; flex-direction: column; }
.activity-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) 0; border-bottom: 1px solid var(--border);
}
.activity-row:last-child { border-bottom: 0; }
.activity-i {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex: none; border-radius: var(--r2);
  background: var(--surface-sunken); color: var(--text-secondary);
}
.activity-i svg { width: 15px; height: 15px; }
.activity-t { min-width: 0; flex: 1; }
.activity-top { display: flex; align-items: baseline; gap: var(--s2); }
.activity-line {
  font-size: 13px; color: var(--text-primary); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* --text-muted measures 2.64:1 in light on this surface, under the 4.5:1
   body text needs — both lines take the secondary ink instead, same fix as
   the sparkline above. */
.activity-who { font-size: 11px; color: var(--text-secondary); margin-top: 1px; }
.activity-when {
  margin-left: auto; flex: none; font-size: 11px; color: var(--text-secondary);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* ── the table, in a card ─────────────────────────────────────────
   The reference wraps its table in a rounded card with a titled header strip.
   Adopted deliberately: the previous world banned it, and three rounds built
   against that ban were rejected. */
/* .tbl-card itself is declared up in the table block; only the bare-table
   lead-in lives here, because it must lose to `.tbl-card > table`. */
table { margin-top: var(--s3); }

/* A form is a rule above it and fields below — never a bordered box. */
.panel-form { padding-top: var(--s4); border-top: 1px solid var(--border); margin-bottom: var(--s4); }
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--s3) var(--s5);
}
.form-actions { display: flex; gap: var(--s2); margin-top: var(--s4); }
.form-error { color: var(--danger); font-size: 12.5px; margin: var(--s2) 0 0; min-height: 1.2em; }
.panel-form [hidden] { display: none; }

/*
 * A save that is still running.
 *
 * Saving an IVR greeting waits on a speech vendor — measured at 5.8-11.9s —
 * and a form that looks idle for that long reads as a click that missed. So
 * the fields go quiet and a spinner sits over them.
 *
 * pointer-events is what actually prevents the second save; the blur and the
 * dimming only explain why. Blur alone would still let an impatient admin hit
 * Save again through it.
 */
.panel-form.is-busy { position: relative; }
.panel-form.is-busy > * { filter: blur(1.5px); opacity: .55; pointer-events: none; }
.panel-form.is-busy::after {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 26px; height: 26px; border-radius: 50%;
  border: 2.5px solid var(--accent); border-bottom-color: transparent;
  animation: spin .7s linear infinite;
}
/* The spinner must not blur itself — it is a sibling of the blurred children,
   not one of them, which is why the rule above targets `> *` and not the form. */
@media (prefers-reduced-motion: reduce) {
  .panel-form.is-busy::after { animation-duration: 2.4s; }
}

/* ── language ─────────────────────────────────────────────────────── */

.lang {
  font: inherit; font-size: 12.5px;
  padding: 7px var(--s2);
  border: 1px solid var(--line-strong); border-radius: var(--r2);
  background: var(--ground-raised); color: var(--ink-muted);
  cursor: pointer;
}
.lang:hover { color: var(--ink); }

.login-langs { display: flex; gap: var(--s3); justify-content: center; margin-top: var(--s5); }
.linklike {
  background: none; border: none; padding: 4px 2px; cursor: pointer;
  font: inherit; font-size: 12.5px; color: var(--ink-faint);
  border-bottom: 1px solid transparent;
}
.linklike:hover { color: var(--ink-muted); }
.linklike[aria-current="true"] { color: var(--ink); border-bottom-color: var(--accent); }

/* An action that cannot be undone gets a border, not a colour wash: the wash
   reads as an error that has already happened. */
.danger-zone {
  border: 1px solid var(--danger);
  border-radius: var(--r2);
  padding: var(--s4);
  max-width: 560px;
}
.danger-zone .help { margin: 0 0 var(--s3); }

/* A list of things that are unfinished. Rows, not cards: each is one line of
   "this is wrong, click to fix it", and a card would give each equal weight to
   the work behind it. */
.todo { display: flex; flex-direction: column; max-width: 620px; }
.todo-item {
  display: flex; align-items: center; gap: var(--s3);
  background: none; border: none; border-bottom: 1px solid var(--line);
  cursor: pointer; font: inherit; font-size: 13.5px; color: var(--ink);
  padding: var(--s3) var(--s2);
  text-align: left;
  transition: background var(--fast) var(--ease);
}
.todo-item:hover { background: var(--ground-sunken); }
.todo-item svg { margin-left: auto; opacity: .5; }

/* ── lamps outside a call row ─────────────────────────────────────── */

/* The same lamp a call wears, used for anything with a state: a tenant, a
   gateway, the switch. One vocabulary across the whole product means a green
   dot never has to be relearned. */
.lampline { display: flex; align-items: center; gap: var(--s2); }
.lamp-live { background: var(--live); box-shadow: 0 0 8px var(--live-glow); }
.lamp-ring { background: var(--ring); box-shadow: 0 0 7px var(--ring-glow); }
.lamp-queue { background: var(--queue); }
.lamp-danger { background: var(--danger); }
.lamp-idle { background: var(--ink-faint); opacity: .55; }

.stat-v-sm { font-size: 14px; font-weight: 500; }

/* One line, with the whole string in the title. A switch's status line runs
   long, and letting it wrap makes one stat twice the height of its neighbours. */
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── the door: sign-in and sign-up ────────────────────────────────── */

/*
 * THESIS: one surface, lit — not two halves with a border down the middle.
 * The earlier door set a flat #eef0f3 panel beside flat #f7f8f9 paper and
 * drew a 1px line between them, which is exactly what read as cheap: the
 * page announced itself as two columns and neither was lit.
 *
 * OWN-WORLD: light falls across the whole page from the upper right, and a
 * wireframe globe carries a call between two of its nodes on the console's
 * own state ladder — queued, ringing, connected, released. Same tokens, page
 * scale. The pool of light the live arc sits in is deliberately OUTSIDE the
 * clipped panel so its falloff crosses the column boundary; clip it and the
 * seam comes straight back.
 *
 * STORY: a stranger sees what the product does — a call reaching a person,
 * anywhere — before they have an account.
 *
 * FIRST VIEWPORT: form left in the quiet part of the sweep, claim and globe
 * right. Primary action under the fields.
 */

/* The paper ramp, the sheen and the grain all live in tokens.css now.
 *
 * They used to be redeclared here, and because console.css loads after
 * tokens.css the local copies won — so the door kept its own hardcoded ramp
 * straight through the palette change and never re-coloured. It merely looked
 * consistent, because the old greys sat close to the new near-white. Found by
 * reading computed style on the live page, not by looking at it.
 *
 * `--edge` is gone rather than moved: it had no consumers anywhere in the
 * file.
 */

.door {
  position: relative;
  min-height: 100dvh;
  display: grid;
  /* The form is the subject of the page, not a widget sitting on it: it takes
     the larger share. Was 1fr/1.06fr, which made the globe the subject. */
  grid-template-columns: minmax(0, 1.12fr) minmax(0, .88fr);
  isolation: isolate;
  background: var(--paper-2);
}

/* The grading. No border anywhere — the tonal shift IS the seam. */
.door::before {
  content: '';
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(130% 100% at 88% 6%, var(--sheen), transparent 52%),
    /* --accent, not --live: only a live call may be bright, and this wash
       tints the whole sheet. Teal is the brand. */
    radial-gradient(80% 64% at 100% 88%,
      color-mix(in srgb, var(--accent) 7%, transparent), transparent 66%),
    linear-gradient(102deg, var(--paper-1) 0%, var(--paper-2) 44%, var(--paper-3) 100%);
}

/* Fine grain so the large flats have tooth and never band. One texture across
   the whole page is part of what makes it read as a single sheet. */
.door::after {
  content: '';
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: var(--grain);
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

.door-form-wrap {
  position: relative; z-index: 2;
  display: grid; place-items: center;
  padding: clamp(40px, 7vh, 80px) clamp(28px, 6vw, 92px);
}
/* ONE rule, answering two things that used to pull against each other.
 *
 * 1. SIZE. The figure used to stretch with the grid row, and sign-up's form is
 *    ~240px taller than sign-in's — so the same globe was drawn into an 820px
 *    box on one form and a 1060px box on the other, and the canvas buffer was
 *    built for whichever it saw first.
 * 2. POSITION. Sign-up is tall enough to scroll at ordinary viewport heights,
 *    and the figure scrolled away with it: the globe was visible while reading
 *    the first two fields and gone for the rest of the form, leaving the lower
 *    half of the page as a form on bare paper.
 *
 * Both are the same question — how tall is this box and where does it sit — so
 * they are answered together rather than by stacking a sticky rule on top of a
 * max-height. `height` is DEFINITE, which is what makes the artwork identical
 * on both forms AND gives sticky something to travel inside: a stretched grid
 * item fills its row and can never move.
 *
 * `align-self: start` (never `center`): the figure's children are all
 * absolutely positioned, so it has no content height of its own — but a
 * definite `height` supplies one, which is why un-stretching it here does not
 * collapse the box to zero the way `center` alone did.
 *
 * --door-fig-inset is declared once and used twice, in `top` and in the height
 * subtraction. They are the same number by construction; written separately
 * they drift, and the drift shows up as a globe whose bottom edge is cut off
 * only at certain viewport heights. */
.door-figure {
  --door-fig-inset: clamp(24px, 4vh, 56px);
  position: sticky; top: var(--door-fig-inset);
  align-self: start;
  height: min(820px, calc(100dvh - var(--door-fig-inset) * 2));
  z-index: 1; overflow: hidden;
}

/* Grows with the column instead of sitting at a fixed 384px.
   The globe is sized as a fraction of its column (min(W*.345, H*.33)), so it
   filled ~69% of its side at every width while a fixed form filled 62% at
   1280 and only 47% at 1680 — the two sides drifted further apart the bigger
   the screen got. The floor keeps small screens exactly as they were, and
   30vw tracks the globe to within ~7pt from 1280 all the way to 1920.
   The ceiling is a reading limit, not a layout one: past 576px the labels and
   help text under each field get too wide to scan comfortably, so on a very
   large monitor the form stops growing and the whitespace takes the slack.

   34vw, not 30: the form's column grew from 1fr to 1.12fr, and unless the form
   grows with it the extra width lands as empty space beside the fields — which
   makes the form look SMALLER on the page rather than more important. The
   ceiling is unchanged; it is a reading limit and the column ratio does not
   change how wide a label may comfortably be. */
.door-form { width: 100%; max-width: clamp(384px, 34vw, 576px); }

/* Above the figure (z-index 1) and the grading (0), so it stays clickable over
   the globe. Its own z-index also keeps it out of .door-figure's overflow
   clip. Corner-set and quiet: this is a comfort control somebody uses once,
   and it must not read as the page's primary action next to Sign in. */
.door-theme {
  position: absolute; top: clamp(16px, 3vh, 28px); right: clamp(16px, 2.4vw, 32px);
  z-index: 3;
  background: color-mix(in srgb, var(--ground-raised) 70%, transparent);
  backdrop-filter: blur(6px);
  border-color: color-mix(in srgb, var(--ink) 12%, transparent);
}
/* Below 920px the figure is gone, so the button sits over the form's own
   ground and the blur has nothing to soften. */
@media (max-width: 920px) {
  .door-theme { background: var(--ground-raised); backdrop-filter: none; }
}

/* Identity is the lamp itself, at rest: the product's own state vocabulary
   rather than a wordmark set in caps for no reason. */
.door-brand {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 clamp(32px, 6vh, 52px);
  font-size: 13px; letter-spacing: .02em; font-weight: 600; color: var(--ink);
}
.door-brand::before {
  content: '';
  width: 8px; height: 8px; border-radius: 50%;
  /* Teal, not the live-call green: nothing on this page is live, and a static
     brand mark wearing the call colour breaks the one rule the whole palette
     rests on. */
  background: var(--accent); flex: none;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 13%, transparent),
              0 0 12px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* The claim, carrying the page's display size, at the top of the FORM column —
   where the eye lands first. It used to sit at 17-20px over the globe while
   "সাইন ইন" took 34px, which spent the largest type on the page on two words
   no buyer cares about. Inverted: the claim leads, the form heading follows. */
.claim-lead { margin: 0 0 clamp(20px, 3.5vh, 30px); }
.claim-lead h2 {
  font-size: clamp(30px, 3.9vw, 44px); line-height: 1.16; letter-spacing: -.03em;
  font-weight: 600; margin: 0 0 10px; color: var(--ink); text-wrap: balance;
}
.claim-lead p {
  font-size: 14.5px; line-height: 1.6; margin: 0;
  color: var(--ink-muted); max-width: 42ch; text-wrap: balance;
}

.door-form h1 {
  font-size: 24px;
  line-height: 1.12; letter-spacing: -.02em;
  margin: 0 0 8px; font-weight: 560; text-wrap: balance;
}
.door-form .lede {
  color: var(--ink-muted); font-size: 14px; line-height: 1.55;
  margin: 0 0 26px; max-width: 36ch;
}

.door-form .field { margin-bottom: 16px; }
.door-form .field-top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 7px;
}
/* No text-transform: uppercase has no meaning in Bangla, and applying it to a
   mixed-script UI gives one label small caps and the next one nothing. */
.door-form .field label {
  font-size: 12.5px; color: var(--ink-muted); font-weight: 500; margin: 0;
}
/* Quieter than the label it sits beside: it is the exception, not the field. */
.door-form .field-aid {
  font: inherit; font-size: 12px; color: var(--ink-faint);
  background: none; border: 0; padding: 0; cursor: pointer;
  transition: color var(--fast) var(--ease);
}
.door-form .field-aid:hover { color: var(--ink); }

/* MUST BEAT the console-wide field rule at ~line 1025:
 *   input:not([type=checkbox]):not([type=radio]):not([type=range]):not(.hsearch-i)
 * which is (0,5,1) against a plain `.door-form input` at (0,1,1). It won, so
 * the door silently rendered 36px-tall, 13px, 9px-radius app controls with no
 * inset highlight and no lift — measured off the running page, not guessed.
 *
 * The cure is to carry the SAME :not() chain and add the door's own class:
 * (0,6,1) beats (0,5,1) by exactly one class, and it reads as what it is —
 * the door restating the generic rule it has to outrank. The generic rule is
 * untouched: it serves ~30 other screens.
 *
 * Never !important here. Specificity has bitten this project eight times now;
 * an !important would make the ninth unfixable. */
.door-form input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i) {
  width: 100%; box-sizing: border-box;
  font: inherit; font-size: 15px; height: auto; padding: 16px 15px;
  border: 1px solid var(--edge-2); border-radius: 10px; color: var(--ink);
  background: color-mix(in srgb, var(--ground-raised) 76%, transparent);
  box-shadow: 0 1px 1.5px rgba(17, 21, 26, .035),
              inset 0 1px 0 rgba(255, 255, 255, .55);
  transition: border-color var(--fast) var(--ease),
              box-shadow var(--fast) var(--ease),
              background var(--fast) var(--ease);
}
:root[data-theme="dark"] .door-form input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i) {
  box-shadow: 0 1px 2px rgba(0, 0, 0, .34), inset 0 1px 0 rgba(255, 255, 255, .03);
}
/* The token, not a hardcoded hex. It used to be #6b7581/#858e9c, justified by
   a measurement taken before the palette changed; re-measured on today's
   ground --ink-faint gives 4.84:1 light and 5.07:1 dark — both pass, and the
   hardcodes were by then WORSE in light (4.68:1). A door that keeps its own
   copy of a colour sails through the next palette change; that cost a whole
   round once already (DESIGN.md). */
.door-form input::placeholder { color: var(--ink-faint); opacity: 1; }
/* Sign-up's mobile: one box holding flag ▾, calling code and number, the
   way the other door fields look. The box carries the border and the focus
   ring; the number inside it is bare. A button, not a <select> — a select
   cannot hold a search. */
.door-form .phone-box {
  position: relative; display: flex; align-items: center;
  border: 1px solid var(--edge-2); border-radius: 10px;
  background: color-mix(in srgb, var(--ground-raised) 76%, transparent);
  box-shadow: 0 1px 1.5px rgba(17, 21, 26, .035), inset 0 1px 0 rgba(255, 255, 255, .55);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
:root[data-theme="dark"] .door-form .phone-box {
  box-shadow: 0 1px 2px rgba(0, 0, 0, .34), inset 0 1px 0 rgba(255, 255, 255, .03);
}
.door-form .phone-box:hover { border-color: color-mix(in srgb, var(--ink) 18%, transparent); }
.door-form .phone-box:has(#s-phone:focus, .cc-btn[aria-expanded="true"]) {
  background: var(--ground-raised);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--accent) 24%, transparent);
}
/* The id, because the door's input rule is a five-:not() chain that a class
   here would have to restate to outrank. :not(autofill) keeps the inset
   shadow that covers Chrome's autofill wash. */
.door-form #s-phone {
  flex: 1; min-width: 0; border: 0; border-radius: 10px; background: transparent;
  padding: 16px 15px 16px 6px;
}
.door-form #s-phone:not(:-webkit-autofill) { box-shadow: none; }
.door-form .cc-btn {
  display: inline-flex; align-items: center; gap: 6px; align-self: stretch;
  padding: 0 4px 0 15px; border: 0; background: none; cursor: pointer; color: var(--ink-faint);
}
.door-form .cc-flag { font-size: 18px; line-height: 1; }
.door-form .cc-caret {
  border: 4px solid transparent; border-top-color: currentColor; border-bottom: 0;
}
.door-form .cc-btn[aria-expanded="true"] .cc-caret { transform: rotate(180deg); }
.door-form .phone-box label.cc-code { margin: 0; font-size: 15px; font-weight: 400; color: var(--ink); padding-left: 6px; font-variant-numeric: tabular-nums; }
.door-form .cc-pop {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 30;
  background: var(--ground-raised); border: 1px solid var(--edge-2);
  border-radius: 12px; box-shadow: var(--lift-2); overflow: hidden;
}
.door-form .cc-pop[hidden] { display: none; }
.door-form .cc-search { position: relative; padding: 8px; }
.door-form .cc-search svg {
  position: absolute; left: 22px; top: 50%; transform: translateY(-50%);
  color: var(--ink-faint); pointer-events: none;
}
.door-form #s-cc-q { padding: 11px 12px 11px 40px; }
.door-form .cc-list { list-style: none; margin: 0; padding: 0 0 6px; max-height: 260px; overflow-y: auto; }
.door-form .cc-list li {
  display: flex; align-items: center; gap: 10px; padding: 10px 16px;
  cursor: pointer; font-size: 15px; color: var(--ink);
}
.door-form .cc-list li.on { background: color-mix(in srgb, var(--ink) 6%, transparent); }
.door-form .cc-list li[aria-selected="true"] .n { font-weight: 600; }
.door-form .cc-list .n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.door-form .cc-list .c { color: var(--ink-faint); font-variant-numeric: tabular-nums; flex: none; }
.door-form .cc-list .cc-none { cursor: default; color: var(--ink-faint); }
.door-form input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i):hover {
  border-color: color-mix(in srgb, var(--ink) 18%, transparent);
}
/* --accent, not --live. The email field is autofocused on load, so a live-call
   green ring lit up on first paint with no call anywhere. */
.door-form input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i):focus {
  outline: none;
  background: var(--ground-raised);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--accent) 24%, transparent);
}
/* Dark had NO visible focus indicator at all — WCAG 2.4.7, on the product's
   first screen. The dark rule above re-asserts the resting box-shadow and, at
   equal weight and later in the file, wiped the ring; `outline: none` had
   already killed the browser's fallback. So dark states the focus ring
   explicitly, keeping its inset highlight. */
:root[data-theme="dark"] .door-form input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i):focus {
  box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--accent) 34%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .03);
}
/* Chrome paints a lavender wash over an autofilled field and there is no
   property that turns it off — a 100px inset shadow is the only way to cover
   it. Two things were missing and they made a dark sign-in unusable:
   `:-internal-autofill-selected` is the state Chrome uses BEFORE the field is
   touched, so an untouched form stayed white and its placeholder vanished
   into it, while :hover matched the rule below and snapped it dark; and the
   ground has to be the field's own, not --ground-raised, which is #ffffff in
   light. `:autofill` is the standard alias, harmless where unsupported. */
.door-form input:-webkit-autofill,
.door-form input:-webkit-autofill:hover,
.door-form input:-webkit-autofill:focus,
.door-form input:-webkit-autofill:active,
.door-form input:-internal-autofill-selected,
.door-form input:autofill {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 100px var(--field) inset;
  box-shadow: 0 0 0 100px var(--field) inset;
  caret-color: var(--ink);
  border-color: var(--edge-2);
  /* Chrome animates the wash back in on some transitions; a long delay on a
     no-op keeps the covering shadow from being repainted over. */
  transition: background-color 6000000s ease-in-out 0s;
}

/* The same wash lands on the console's own fields — the change-password form
   in Settings is the one a browser offers to fill. Same cure, on --ground,
   which is what those inputs actually sit on. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus,
.field input:-webkit-autofill:active,
.field input:-internal-autofill-selected,
.field input:autofill {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 100px var(--ground) inset;
  box-shadow: 0 0 0 100px var(--ground) inset;
  caret-color: var(--ink);
  transition: background-color 6000000s ease-in-out 0s;
}

/* The door's submit no longer carries `.btn btn-primary` (see app.js): that
   class set height:36px and clamped this button to app-toolbar size while the
   door's own gradient and glow still painted — a cheap-sized button wearing an
   expensive finish. Removing the classes is the fix; nothing here has to
   out-weigh anything, so the rule below is the whole button, including the
   flex/cursor basics .btn used to supply. */
.door-form button[type="submit"] {
  width: 100%; display: inline-flex; align-items: center; justify-content: center;
  font: inherit; cursor: pointer;
  font-size: 14.5px; font-weight: 500; padding: 13px 18px; margin-top: 10px;
  color: #fff; border: 0; border-radius: 10px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--door-cta) 94%, #fff), var(--door-cta) 52%,
    color-mix(in srgb, var(--door-cta) 91%, #000));
  /* The CTA is teal in both themes, but it cannot wear the same label colour
     in both. In light it takes the deeper --accent-ink (see tokens.css) and a
     white label: 5.47:1. In dark the token is a bright mint, where white
     measured 1.92:1 and the button glared at 9.85:1 against the page — so dark
     takes ink on the mint instead, 8.93:1, and the glare goes with it. */
  box-shadow: 0 1px 2px color-mix(in srgb, var(--door-cta) 26%, transparent),
              0 8px 20px -8px color-mix(in srgb, var(--door-cta) 55%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .26);
  transition: transform var(--fast) var(--ease),
              box-shadow var(--med) var(--ease), filter var(--fast) var(--ease);
}
.door-form button[type="submit"]:hover:not(:disabled) {
  filter: brightness(1.045);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--door-cta) 28%, transparent),
              0 12px 28px -9px color-mix(in srgb, var(--door-cta) 62%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .28);
}
.door-form button[type="submit"]:active:not(:disabled) {
  transform: scale(.985);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--door-cta) 30%, transparent),
              inset 0 1px 3px rgba(0, 0, 0, .16);
}
/* Sign-up disables its own submit while the request is in flight. .btn used to
   supply this; the door states it now. */
.door-form button[type="submit"]:disabled { opacity: .45; cursor: not-allowed; }

/* Dark: ink on the mint, not white. The lamp green stays the lamp green — the
   button is still unmistakably the live colour — but the label is now legible
   and the halo no longer burns a hole in a dark room. */
:root[data-theme="dark"] .door-form button[type="submit"] {
  color: #06231a;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .4),
              0 8px 22px -12px color-mix(in srgb, var(--door-cta) 42%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .18);
}
:root[data-theme="dark"] .door-form button[type="submit"]:hover:not(:disabled) {
  box-shadow: 0 1px 2px rgba(0, 0, 0, .42),
              0 12px 28px -12px color-mix(in srgb, var(--door-cta) 50%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .2);
}

/* Collapses to nothing when silent. Reserving a line for an error that is
   usually absent leaves a hole above the button on every single load. */
.door-err { color: var(--danger); font-size: 12.5px; line-height: 1.45; margin: 0; }
.door-err:not(:empty) { margin: calc(var(--s1) * -1) 0 var(--s3); }

.door-alt { font-size: 12.5px; color: var(--ink-muted); margin: 24px 0 0; }
/* Restrained: a second saturated link beside a saturated button makes the page
   argue with itself about where to click. */
.door-alt .linklike {
  color: var(--ink); font-size: inherit; font-weight: 500;
  text-decoration: underline; text-decoration-color: var(--edge-2);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--fast) var(--ease);
}
.door-alt .linklike:hover { text-decoration-color: var(--accent); }

.door-form .login-langs {
  display: flex; align-items: center; gap: 12px;
  justify-content: flex-start; margin-top: 28px;
}
.door-form .login-langs .lang-sep {
  width: 1px; height: 11px; background: var(--edge-2); flex: none;
}

/* ── the figure: a globe, and one call crossing it ────────────────── */
.door-figure canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

/* `.door-claim` is gone, not hidden: the claim is said once now, at display
   size, in the form column (`.claim-lead`). The globe below it is the same
   claim without words, and it no longer has a caption to keep clear of. */

/* Lives outside .door-figure on purpose: clipped, its soft edge becomes a hard
   vertical line at the column boundary — the seam this design exists to remove. */
.door-pool {
  position: fixed; border-radius: 50%; pointer-events: none; z-index: 0;
  transform: translate(-50%, -50%); opacity: 0;
  transition: opacity 1100ms var(--ease), left 1100ms var(--ease), top 1100ms var(--ease);
}

/* Below 920px the figure is dead weight and would push the form off screen.
   The lamp and the button carry the world alone. */
@media (max-width: 920px) {
  .door { grid-template-columns: 1fr; }
  .door-figure, .door-pool { display: none; }
  .door::before {
    background:
      radial-gradient(120% 44% at 84% 0%, var(--sheen), transparent 58%),
      linear-gradient(168deg, var(--paper-1), var(--paper-3));
  }
  /* On a phone a full display-size claim would eat the fold before the form
     appeared, so it steps down to large-but-not-display. The ORDER is what the
     design move is about — claim first, heading second — and that survives at
     every width. */
  .claim-lead h2 { font-size: 27px; line-height: 1.2; }
  .claim-lead p { font-size: 13.5px; }
}
/* ── incoming call ────────────────────────────────────────────────── */

/* A ringing call outranks everything else on the screen: the agent has seconds
   to decide, and should not have to look for the answer button. */
.incoming {
  border: 1px solid var(--live);
  border-radius: var(--r3);
  background: linear-gradient(180deg, var(--live-wash), var(--ground-raised));
  padding: var(--s4);
  box-shadow: var(--lift-2);
  animation: incoming-pulse 2s var(--ease) infinite;
}
@keyframes incoming-pulse {
  0%, 100% { box-shadow: var(--lift-2); }
  50%      { box-shadow: var(--lift-2), 0 0 0 4px var(--live-wash); }
}
/* "আসছে" / "Incoming" — Bangla in the primary locale, so no uppercase. */
.incoming-label {
  font-size: 11.5px; letter-spacing: .02em;
  color: var(--live); margin: 0 0 var(--s1); font-weight: 600;
}
.incoming-from { font-size: 18px; font-weight: 600; margin: 0; letter-spacing: -.01em; }
/* Who is ringing, in three figures. A row rather than a list: the agent has
   seconds, and three short facts read faster stacked side by side than as
   labelled lines. */
.incoming-facts {
  display: flex; gap: var(--s4);
  margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
.fact { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.fact-n { font-size: 15px; font-weight: 600; color: var(--ink); line-height: 1.2; }
.fact-l { font-size: 11px; color: var(--ink-muted); white-space: nowrap; }

.incoming-actions { display: flex; gap: var(--s2); margin-top: var(--s4); }
.incoming-actions .btn { flex: 1; justify-content: center; }

/* ── wallboard ───────────────────────────────────────────────────────
   Read from across a room, so the two numbers a supervisor acts on —
   how many waiting, how long the worst one has waited — carry the size.
   A queue only goes red against its OWN max_wait_sec; nothing else on
   this screen is allowed to shout. */

/*
 * A plain raised panel. Same surface as .wall-q, without the waiting/breach
 * states that one carries — a plan is not a thing that goes red.
 */
.card {
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--ground-raised);
  padding: var(--s4);
  box-shadow: var(--lift-1);
}
.card h3 { font-size: 14px; margin: 0 0 var(--s1); }
.card p { font-size: 12px; }

/* auto-FILL with a 300px ceiling, so one queue stays a tile rather than
   stretching across a 1440px card — the same reason .stats uses auto-fill.
   Inside a card body now, whose padding is the outer gap. */
.wall-queues {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 300px));
  gap: var(--s3);
}
.ov-card .wall-queues, .ov-card .wall-agents { margin: 0; }

/* ── plan & billing ───────────────────────────────────────────────
   The admin surface, not the agent's. PRODUCT.md licenses more room here:
   a tenant admin visits occasionally and the actions spend money, so the
   figures get to be legible rather than dense. */

.plan-state {
  border: 1px solid var(--border); border-radius: var(--r4);
  background: var(--surface); box-shadow: var(--lift-1);
  padding: var(--s6); margin-bottom: var(--s6);
}
/* Inside a card now, so it drops its own border, ground and lift — otherwise
   it is a card drawn inside a card. Matched at (0,2,0) against the (0,1,0)
   rule above it, which is what lets it win without !important. */
.ov-card > .plan-state {
  border: 0; border-radius: 0; background: none; box-shadow: none;
  padding: 14px 18px 18px; margin-bottom: 0;
}
.plan-state-top { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.plan-state-name {
  font-size: 22px; font-weight: 650; letter-spacing: -.025em; color: var(--text-primary);
}
.plan-state-price {
  margin-left: auto; font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: 15px; color: var(--text-secondary);
}
.plan-state-when { margin: var(--s2) 0 0; font-size: 13px; color: var(--text-secondary); }

/* What they are using against what they are allowed. A bar rather than
   "3/2", because being OVER a limit was previously indistinguishable from
   being under it — both were grey text. */
/* Two across, not two stacked full-width bars. The work area is deliberately
   full-bleed (see .main), so a stacked meter ran the whole window and the eye
   crossed ~1100px between "People" and "3 of 2" — label and figure are one
   fact and must be readable together. Measured at 1440px: 1100px → 282px. */
.plan-meters {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--s5); margin-top: var(--s5); max-width: 720px;
}
@media (min-width: 620px) { .plan-meters { grid-template-columns: 1fr 1fr; } }
/* What a tier unlocks, under its limits. Small and grey on purpose: the
   figure and the price lead the card; this is what the figure buys. */
.plan-tier-feats {
  list-style: none; margin: var(--s3) 0 0; padding: 0;
  display: grid; gap: 4px; font-size: 12.5px; color: var(--text-secondary);
}
.plan-tier-feats li { display: flex; align-items: center; gap: 6px; min-width: 0; }
.plan-tier-feats svg { width: 13px; height: 13px; flex: none; color: var(--accent); }
.aia-month { margin: 0 0 var(--s4); }
/* Same rhythm as the allowance line above it, so the card below is not touched. */
.aia-import { margin: 0 0 var(--s4); }

/* Where to send the call. Appears under the live-call controls, so it is
   deliberately plain: an agent is mid-conversation and reading one row of
   names, not browsing. */
.xfer { margin-top: var(--s3); text-align: left; }
.xfer-label {
  margin: 0 0 6px; font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
}
.xfer-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
.xfer-target { max-width: 100%; }
.xfer-target span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xfer-note:empty { display: none; }
.xfer-note { margin: 4px 0 0; }

/* What the AI heard, above the answer button. Quiet by design: the caller's
   name and the two buttons are what an agent acts on in the three seconds
   before they pick up, and this is context for the sentence they open with. */
.ai-transcript {
  margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line); text-align: left;
}
.ai-line { margin: 0 0 5px; font-size: 12.5px; line-height: 1.45; }
.ai-who {
  display: inline-block; min-width: 46px; margin-right: 6px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted); vertical-align: 1px;
}
/* The caller's own words carry the weight — they are the thing the agent has
   to answer; the AI's line is only there to say what was already covered. */
.ai-caller .ai-said { color: var(--text-primary); }
.ai-said { color: var(--text-secondary); }
/* The drawer version, read afterwards: a time column, and the notes (FAQ
   hit, could not make out, handoff) set apart from what anybody said. */
.ai-story { margin-top: 0; padding-top: 0; border-top: 0; }
.ai-story .ai-line { display: grid; grid-template-columns: 40px 52px 1fr; margin-bottom: 8px; }
.ai-story .ai-who { min-width: 0; }
.ai-at { font-size: 11px; color: var(--text-muted); padding-top: 1px; }
.ai-story .ai-note { grid-template-columns: 40px 1fr; font-size: 12px; }
.ai-note .ai-said { color: var(--text-muted); font-style: italic; }
.ai-note.ok .ai-said { color: var(--accent-ink); font-style: normal; }
.ai-note.warn .ai-said { color: var(--warn); }
.tags { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.tags.nowrap { flex-wrap: nowrap; }
/* A question over its answer in one cell (AI agents → facts, new questions). */
.qa-cell { min-width: 260px; max-width: 720px; padding: 8px 0; }
.qa-agent { font-family: var(--font-ui); color: var(--text-secondary); letter-spacing: 0; }
.qa-cell b { display: block; font-weight: 600; line-height: 1.4; }
.qa-cell span {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; margin-top: 3px; font-size: 12.5px; line-height: 1.45;
  color: var(--text-secondary);
}
.ai-earlier summary {
  font-size: 11.5px; color: var(--text-muted); cursor: pointer;
  margin-bottom: 5px;
}

.plan-meter-top {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3);
  font-size: 13px; color: var(--text-secondary);
}
.plan-meter-n {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: 15px; font-weight: 600; color: var(--text-primary);
}
.plan-meter-bar {
  margin-top: 7px; height: 5px; border-radius: 999px;
  background: var(--surface-sunken); overflow: hidden;
}
.plan-meter-fill {
  height: 100%; border-radius: 999px; background: var(--text-muted);
  transition: width var(--med) var(--ease);
}
/* Over the ceiling takes the billing hue — the one thing on this screen that
   needs somebody to act. Not the accent: only a live call is allowed that. */
/* The BAR keeps the billing hue; the TEXT does not. #D97706 on white measures
   3.19:1 at 12px, under the 4.5 small text needs, so the words are darkened
   while the signal stays in the fill. Dark's amber already passes, so this is
   a light-theme correction rather than a new colour. */
.plan-meter.is-over .plan-meter-fill { background: var(--amber); }
.plan-meter.is-over .plan-meter-n,
.plan-meter-note { color: color-mix(in srgb, var(--amber) 78%, #000); }
:root[data-theme="dark"] .plan-meter.is-over .plan-meter-n,
:root[data-theme="dark"] .plan-meter-note { color: var(--amber); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .plan-meter.is-over .plan-meter-n,
  :root:not([data-theme="light"]) .plan-meter-note { color: var(--amber); }
}
.plan-meter-note { margin: 6px 0 0; font-size: 12px; }

.plan-state-acts { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s5); }
.plan-note { margin: var(--s3) 0 0; font-size: 12.5px; color: var(--text-secondary); }

/* One control for "which of these few", shared by the billing period and the
   report range. Both were a row of separate buttons with the chosen one
   filled: four pills touching each other reads as a broken segment, not as a
   choice. A track with one pill in it reads as one control with one answer. */
.segmented {
  display: inline-flex; padding: 3px; gap: 2px;
  border: 1px solid var(--border); border-radius: 999px; background: var(--surface-sunken);
}
.segmented button {
  font: inherit; font-size: 12.5px; font-weight: 500;
  padding: 6px 14px; border: none; border-radius: 999px;
  background: none; color: var(--text-secondary); cursor: pointer;
  white-space: nowrap;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.segmented button:hover:not([aria-pressed="true"]) {
  color: var(--text-primary); background: var(--surface);
}
/* The chosen range wears the brand, not the live hue.
   It stays a raised pill — the accent belongs to a live call and a range
   picker is not an event — but white-on-near-white made "which range am I
   on?" a question you had to squint at: the selected pill and its sunken
   track differ by 3% lightness. A wash and accent-ink ink it without the
   filled-accent treatment a primary button gets, so it still reads as where
   you are rather than as something to press.

   accent-ink (#0F766E), not accent (#0D9488): 5.47:1 against the wash where
   the plain accent is 3.74:1. Same hue, and the only one of the two that a
   reader at hour eight can actually read. */
.segmented button[aria-pressed="true"] {
  background: var(--accent-wash); color: var(--accent-ink);
  box-shadow: var(--lift-1); font-weight: 600;
}
.segmented button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.plan-period { margin-bottom: var(--s4); }

.plan-tiers {
  display: grid; gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
  /* Stretch is kept deliberately, and it is the reason the buttons line up.
     `start` lets each card take its own height, which sounds tidier and
     measures worse: the four plans carry descriptions of different lengths
     and between two and five limit lines, so their buttons landed 93px apart.
     The cost of stretch is that expanding one card grows the row — the three
     it does not concern gain whitespace. Measured both ways in
     apps/console/test/plan-tier-layout.test.ts; a level row won. */
}

/* The one line on this screen that asks somebody to act, said first and said
   once. It was a note under a bar two thirds down the card — quieter than the
   plan name at 22px — with its remedy as prose 700px above where the remedy
   lives. It carries the remedy as a button instead. */
.bill-act {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 14px 16px; border-radius: var(--r3);
  background: var(--warn-wash); color: var(--ring-ink);
  margin-bottom: var(--s5);
}
.bill-act .ic {
  width: 26px; height: 26px; border-radius: 7px; flex: none; margin-top: 1px;
  display: grid; place-items: center;
  background: rgba(255,255,255,.55); color: var(--ring-ink);
}
.bill-act .ic svg { width: 14px; height: 14px; }
.bill-act .tx { flex: 1; min-width: 0; font-size: 13px; line-height: 1.5; }
.bill-act .tx b { font-weight: 650; }
.bill-act .tx p { margin: 3px 0 0; font-size: 12.5px; opacity: .92; }
.bill-act .btn { flex: none; }

/* Which plan answers the problem stated at the top of this same screen. True
   and computed — the cheapest whose ceilings fit current usage — never an
   invented "most popular". */
.plan-tier.is-fits { border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
.tier-flag {
  display: inline-flex; align-items: center; gap: 5px; align-self: flex-start;
  font-size: 10.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px;
  background: var(--accent-wash-2); color: var(--accent-ink);
  margin-bottom: var(--s3);
}
.tier-flag svg { width: 11px; height: 11px; }
/* Keeps every card's body aligned although only one carries a flag. */
.tier-flag-slot { display: block; height: 25px; margin-bottom: var(--s3); }

/* When the new price bites, under the button that causes it — the route
   applies it from the NEXT period, charging nothing today and refunding
   nothing. Unsaid, somebody expects one or the other. */
.tier-note {
  margin: var(--s2) 0 0; font-size: 11.5px; line-height: 1.4;
  color: var(--text-muted); text-align: center;
}
.plan-tier {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--r4);
  background: var(--surface); padding: var(--s5);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
.plan-tier:hover { border-color: var(--border-strong); box-shadow: var(--lift-2); }
/* The one they are on. A hairline in the band hue, not a filled chip — the
   same restraint the status pills follow. */
.plan-tier.is-current {
  border-color: color-mix(in srgb, var(--amber) 45%, var(--border));
  box-shadow: var(--lift-1);
}
.plan-tier-head { display: flex; align-items: center; gap: 10px; }
.plan-tier-name {
  font-size: 13px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase;
  color: var(--text-secondary); margin: 0;
}
/* The figure is the thing somebody came to read, so it is sized like it. */
.plan-tier-price {
  display: flex; align-items: baseline; gap: 5px; margin-top: var(--s3);
}
.plan-tier-fig {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: 30px; font-weight: 700; letter-spacing: -.035em; line-height: 1;
  color: var(--text-primary);
}
/* secondary, not muted: muted is #9AA0A6, which is 2.64:1 on white. */
.plan-tier-per { font-size: 13px; color: var(--text-secondary); }
.plan-tier-save { margin: 6px 0 0; font-size: 12.5px; color: var(--accent-ink); }
.plan-tier-desc {
  margin: var(--s3) 0 0; font-size: 13px; line-height: 1.5; color: var(--text-secondary);
}
.plan-tier-limits {
  margin: var(--s4) 0 0; padding: var(--s3) 0 0; border-top: 1px solid var(--border);
  display: grid; gap: 6px; font-size: 12.5px; color: var(--text-secondary);
}
.plan-tier-limits b {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-weight: 600; color: var(--text-primary);
}
/* The button sits on the card's floor, not under whatever its own list
   happened to end at. Four cards sell four different numbers of features, so
   without `auto` the row of buttons stepped down across the row and the four
   plans stopped reading as one choice. `auto` eats the leftover height, so
   every card's button lands on the same line however short its body is.

   The note UNDER the button is taken out of that sum deliberately. Three
   cards say when the new price starts and the card they are already on says
   nothing, so aligning the block's bottom edge dropped that one button 24px
   below its neighbours — the one card somebody looks for first was the one
   out of line. Absolute takes the note out of the height `auto` is balancing,
   so the button lands on the same line whether a note follows it or not. */
.plan-tier-act {
  margin-top: auto; padding-top: var(--s5);
  position: relative;
}
.plan-tier-act .tier-note {
  position: absolute; left: 0; right: 0; top: 100%;
}
/* Room for that note, since taking it out of flow also stopped it reserving
   any. A card with no note keeps the space too, which is what holds the
   bottom edge level across the row.

   `min-height` on the note rather than padding on the card, so the reserved
   space and the thing filling it are the same measurement — a padding guessed
   separately from the text it clears is how the note ended up sitting on the
   card's edge, reading as cut off even though nothing was clipped. */
.plan-tier { padding-bottom: calc(var(--s5) + 2.1em); }
.plan-tier-act .btn { width: 100%; justify-content: center; }

/* The tail of the feature list, behind a disclosure. Styled as a quiet link
   rather than a button: it reveals what is already on the card, it does not
   change the plan, and next to the real action it must not look like a
   second one. */
.plan-more { margin-top: var(--s2); }
.plan-more > summary {
  display: flex; align-items: center; gap: 4px; cursor: pointer;
  list-style: none; width: fit-content;
  font-size: 12.5px; font-weight: 500; color: var(--accent-ink);
  padding: 2px 0;
}
.plan-more > summary::-webkit-details-marker { display: none; }
.plan-more > summary:hover { text-decoration: underline; }
.plan-more > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.plan-more > summary svg {
  width: 13px; height: 13px; flex: none;
  transition: transform var(--fast) var(--ease);
}
.plan-more[open] > summary svg { transform: rotate(180deg); }
.plan-more .plan-more-shut { display: none; }
.plan-more[open] .plan-more-open { display: none; }
.plan-more[open] .plan-more-shut { display: inline; }
.plan-more .plan-tier-feats { margin-top: 4px; }
@media (prefers-reduced-motion: reduce) {
  .plan-more > summary svg { transition: none; }
}

/* The plan they are on, as a disabled button rather than a line of text.
   It carries the band hue the card's border already uses, so the card and its
   control say the same thing. Not `cursor: not-allowed`: there is nothing to
   fix by clicking elsewhere, and the crossed-out cursor reads as a refusal
   rather than as a statement of where they already are. */
.btn.is-current-plan,
.btn.is-current-plan:disabled {
  border-color: color-mix(in srgb, var(--amber) 45%, var(--border));
  background: var(--amber-wash);
  color: color-mix(in srgb, var(--amber) 78%, #000);
  opacity: 1; cursor: default;
}
:root[data-theme="dark"] .btn.is-current-plan { color: var(--amber); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn.is-current-plan { color: var(--amber); }
}

.wall-q {
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--ground-raised);
  padding: var(--s4);
  box-shadow: var(--lift-1);
  transition: border-color var(--med) var(--ease), box-shadow var(--med) var(--ease);
}
/* Somebody is waiting: lift it out of the quiet set. */
.wall-q[data-waiting="true"] {
  border-color: var(--queue);
  box-shadow: var(--lift-2), 0 0 0 1px var(--queue-glow);
}
.wall-q[data-over="true"] {
  border-color: var(--danger);
  background: var(--danger-wash);
  box-shadow: var(--lift-2);
}

.wall-q-name {
  margin: 0 0 var(--s3);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.wall-q-nums { display: flex; gap: var(--s4); }
.wall-q-nums > * { flex: 1; min-width: 0; }
.wall-q-nums > * + * {
  border-left: 1px solid var(--line);
  padding-left: var(--s4);
}

.wall-big {
  margin: 0;
  font-family: var(--font-num);
  font-size: 30px;
  line-height: 1.05;
  color: var(--ink);
}
.wall-q[data-waiting="true"] .wall-big { color: var(--queue); }
.wall-q[data-over="true"]    .wall-big { color: var(--danger); }

.wall-lbl {
  margin: var(--s1) 0 0;
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--ink-faint);
}

.wall-breach {
  margin: var(--s3) 0 0;
  font-size: 11px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--danger);
}

.wall-agents {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--s2);
}

.wall-agent {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3);
  border-radius: var(--r2);
  background: var(--ground-raised);
  border: 1px solid var(--line);
}

/* State is a lamp, never a filled chip — same rule as the call rows. */
.wall-lamp {
  width: 8px; height: 8px;
  border-radius: 50%;
  flex: none;
  background: var(--ink-faint);
}
.wall-agent[data-state="available"] .wall-lamp {
  background: var(--live); box-shadow: 0 0 8px var(--live-glow);
}
.wall-agent[data-state="busy"] .wall-lamp {
  background: var(--ring); box-shadow: 0 0 7px var(--ring-glow);
}
.wall-agent[data-state="wrapup"] .wall-lamp { background: var(--queue); }
.wall-agent[data-state="break"]   .wall-lamp { background: var(--ink-faint); }
.wall-agent[data-state="offline"] { opacity: .5; }

.wall-agent-name {
  margin: 0;
  font-size: 13px;
  color: var(--ink);
}

/* A table-row action: smaller than a section button, so a row of them does
   not compete with the section heading above. */
/* A select sitting inside a table row: matches btn-sm so a row of controls
   lines up, and capped so a long prompt name cannot stretch the column. */
/* A select inside a row-action rank. The base rule gives every select
   width:100%, which inside a flex row resolves to "as small as the flex
   container allows" — so in the Queues rank the hold-music picker collapsed
   to a sliver of its own border and read as a clipped button. Sized to its
   own content instead, with a floor wide enough for the shortest option. */
.sel-sm { padding: 5px 8px; font-size: 12px; max-width: 190px;
          width: auto; min-width: 104px; flex: none; }

/*
 * A row-action select is a setting, not a field.
 *
 * At full input weight — a solid border at the row's own height — the Queues
 * hold-music picker was the heaviest element in its row, so the eye landed on
 * a dropdown instead of on the queue's name and figures. It also read as a
 * text input somebody was meant to type into, which is what "A file we
 * installed" made worse: a disabled option's label, sitting in what looked
 * like an editable box.
 *
 * So it recedes at rest and states itself on approach: a transparent ground
 * with a hairline, filling in on hover and taking a real border on focus. The
 * control is no quieter to reach — the hit area is unchanged — it just stops
 * outranking the data it sits beside. The focus ring is untouched: a keyboard
 * user must still see exactly where they are.
 */
td .sel-sm {
  height: 28px; padding: 0 22px 0 8px; border-color: transparent;
  background-color: transparent; color: var(--text-secondary);
  transition: background-color var(--fast) linear, border-color var(--fast) linear,
              color var(--fast) linear;
}
td .sel-sm:hover {
  background-color: var(--surface-hover); border-color: var(--border);
  color: var(--text-primary);
}
td .sel-sm:focus-visible { border-color: var(--border-strong); color: var(--text-primary); }

/* Queue membership. A tick list, not a multi-select: an admin needs to see
   who is already in without opening anything. */
.checks {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--s2);
}

.check {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--ground-raised);
  cursor: pointer;
  font-size: 13px;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.check:hover { border-color: var(--ink-faint); }
.check:has(input:checked) {
  border-color: var(--accent);
  background: var(--live-wash);
}
.check input { accent-color: var(--accent); width: 15px; height: 15px; }

/* ── reports ───────────────────────────────────────────────────────
   Numbers a supervisor reads at a glance, then a shape they read for
   pattern. Everything is built from the same tokens as the rest of the
   console, so both themes follow without a second definition. */

.hours {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 160px;
  padding: var(--s3);
  background: var(--ground-sunken);
  border-radius: var(--r2);
  overflow-x: auto;
}

.hour {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s1);
  flex: 1 0 22px;
  height: 100%;
}
/* A day-chart past a week (30D/90D) has more bars than the panel can grow to
   fit legibly — scroll instead of squeezing each bar unreadably thin. The
   7-day view keeps growing to fill the row, which is why this only applies
   above that count. */
.hours.is-scroll { justify-content: flex-start; }
.hours.is-scroll .hour { flex: 0 0 16px; }

/* The two bars overlap rather than stack: offered behind, answered in front,
   so the gap between them IS the abandoned calls. Stacking would hide the one
   thing worth looking at. */
.hour-bars {
  position: relative;
  flex: 1;
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.hour-bar {
  position: absolute;
  bottom: 0;
  width: 60%;
  min-height: 1px;
  background: var(--line-strong);
  border-radius: 2px 2px 0 0;
  transition: height var(--med) var(--ease);
}

.hour-bar.answered {
  background: var(--live);
  width: 60%;
}

.hour-label {
  font-family: var(--font-num);
  font-size: 10px;
  color: var(--ink-faint);
}

/* The date under the weekday — only the 7-day range has room for a second
   line without crowding the bars. */
.hour-sub {
  font-size: 9px;
  color: var(--text-muted);
  margin-top: -2px;
}

/* A warning under an IVR setting — amber, the colour of "check this", not of
   an error. */
.ivr-warn {
  display: flex;
  align-items: center;
  gap: var(--s2);
  color: var(--ring);
  margin-top: var(--s3);
}

/* ── IVR tree editor (081) ──────────────────────────────────────────
   The menu as a caller walks it, on the left; whatever is clicked, on the
   right. The tree is a list of rows, not cards: a five-level menu of boxes
   inside boxes stops being readable by the third level. Selection is the
   accent wash, never a bright fill — only a live call is bright. */

.ivrt-split {
  display: grid;
  grid-template-columns: minmax(280px, 400px) 1fr;
  gap: var(--s4);
  align-items: start;
}
.ivrt-left { display: flex; flex-direction: column; gap: var(--s2); min-width: 0; }
.ivrt-left > .help { margin: 0; }
.ivrt-tree {
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--ground-sunken);
  padding: var(--s2);
  max-height: 62vh;
  overflow: auto;
}
.ivrt-panel { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }
.ivrt-panel .sec { margin-top: 0; }

.ivrt-node {
  display: flex; align-items: center; gap: var(--s2);
  width: 100%;
  padding: 6px var(--s2);
  border: 1px solid transparent;
  border-radius: var(--r1);
  background: transparent;
  color: var(--ink);
  font: inherit; font-size: 13px; text-align: left;
  cursor: pointer;
}
.ivrt-node:hover { background: var(--ground); }
.ivrt-node:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: -2px; }
.ivrt-node.is-sel { background: var(--accent-wash); border-color: var(--accent-ink); }
.ivrt-node.has-problem .ivrt-node-sub { color: var(--danger); }
.ivrt-node > svg, .ivrt-node .ivrt-flag svg { flex: none; width: 16px; height: 16px; color: var(--ink-faint); }
.ivrt-flag { display: inline-flex; color: var(--danger); }
.ivrt-flag svg { color: var(--danger) !important; }
.ivrt-node-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.ivrt-node-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ivrt-menu .ivrt-node-title { font-weight: 600; }
.ivrt-node-sub {
  font-size: 12px; color: var(--ink-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The digit reads as a key on a phone, as in the old key rows. */
.ivrt-digit {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r1);
  background: var(--ground);
  font-family: var(--font-num); font-weight: 600; font-size: 13px;
}

.ivrt-keys { display: flex; flex-direction: column; gap: 2px; margin-left: var(--s3);
             padding-left: var(--s2); border-left: 1px solid var(--line); }
.ivrt-branch { margin-left: var(--s4); }
.ivrt-caret, .ivrt-caret-gap { flex: none; width: 16px; height: 16px; display: inline-flex; }
.ivrt-caret { color: var(--ink-faint); transition: transform 120ms ease; }
.ivrt-caret svg { width: 16px; height: 16px; }
.ivrt-caret.is-open { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .ivrt-caret { transition: none; } }

.ivrt-add {
  display: inline-flex; align-items: center; gap: var(--s1);
  align-self: flex-start;
  margin: 2px 0 var(--s1);
  padding: 4px var(--s2);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r1);
  background: transparent; color: var(--ink-muted);
  font: inherit; font-size: 12px; cursor: pointer;
}
.ivrt-add svg { width: 14px; height: 14px; }
.ivrt-add:hover:not(:disabled) { color: var(--ink); border-color: var(--accent-ink); }

/* Dragging: the row being carried fades, and the place it would land is
   marked by a line above it — where it goes, not a highlight on what is there. */
.ivrt-key[draggable="true"] { cursor: grab; }
.ivrt-key.is-dragging { opacity: .45; }
.ivrt-key.is-drop { box-shadow: inset 0 2px 0 var(--accent-ink); }
.ivrt-add.is-drop { border-style: solid; border-color: var(--accent-ink); color: var(--ink); }

.ivrt-invalid {
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s2) 0 0; padding: 0 var(--s2);
  font-size: 12px; color: var(--ink-faint);
}
.ivrt-invalid svg { width: 14px; height: 14px; flex: none; }

/* The panel */
.ivrt-keyhead { display: grid; grid-template-columns: 84px 1fr; gap: var(--s3); align-items: start; }
.ivrt-spoken { display: flex; flex-direction: column; gap: var(--s2); }
.ivrt-spoken .mic-panel[hidden] { display: none; }
.ivrt-note:empty { display: none; }
.ivrt-note { margin: 0; }
.ivrt-rec-chip {
  display: inline-flex; align-items: center; gap: var(--s1);
  font-size: 12px; color: var(--ink-muted);
}
.ivrt-rec-chip svg { width: 14px; height: 14px; }
.field-actions .btn svg { width: 14px; height: 14px; }

.ivrt-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.ivrt-chip {
  padding: 6px var(--s3);
  border: 1px solid var(--line-strong);
  border-radius: 99px;
  background: var(--ground);
  color: var(--ink-muted);
  font: inherit; font-size: 13px; cursor: pointer;
}
.ivrt-chip.is-on { background: var(--accent-wash); border-color: var(--accent-ink); color: var(--ink); font-weight: 600; }
.ivrt-chip:disabled { opacity: .5; cursor: not-allowed; }

.ivrt-target { display: flex; min-width: 0; }
.ivrt-panel .field[hidden] { display: none; }
.ivrt-target > select, .ivrt-target > input { width: 100%; }
.ivrt-subcard {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: var(--s3); border: 1px solid var(--line); border-radius: var(--r2);
  background: var(--ground-sunken); font-size: 13px;
}
.ivrt-whole {
  border: 1px solid var(--line); border-radius: var(--r2);
  padding: var(--s2) var(--s3);
}
.ivrt-whole > summary { cursor: pointer; font-size: 13px; color: var(--ink-muted); }
.ivrt-whole[open] > summary { margin-bottom: var(--s2); }
.ivrt-whole .field { margin-top: var(--s2); }

.ivrt-moves {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  padding-top: var(--s3); border-top: 1px solid var(--line);
}
.ivrt-moves select { max-width: 220px; font-size: 13px; }
.ivrt-spacer { flex: 1; }
.ivrt-del { display: inline-flex; align-items: center; gap: var(--s1); }
.ivrt-del svg { width: 14px; height: 14px; }
.ivrt-del:hover, .ivrt-del:focus-visible { color: var(--danger); background: var(--danger-wash); }

/* The list screen: the tree, read-only, under the menu's name. */
.ivrt-expand {
  display: inline-flex; align-items: center; gap: var(--s1);
  padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer;
}
.ivrt-outline {
  list-style: none; margin: var(--s2) 0 0; padding-left: var(--s3);
  border-left: 1px solid var(--line);
  font-size: 12px; color: var(--ink-muted);
}
.ivrt-outline li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); padding: 2px 0; }
.ivrt-outline .ivrt-digit { width: 20px; height: 20px; font-size: 12px; }
.ivrt-outline .ivrt-outline { flex-basis: 100%; margin-left: var(--s3); }

@media (max-width: 760px) {
  /* Stacked: the tree, then the panel for what was tapped. */
  .ivrt-split { grid-template-columns: 1fr; }
  .ivrt-tree { max-height: 45vh; }
  .ivrt-keyhead { grid-template-columns: 1fr; }
}

/* A field whose control has an action under it — the prompt dropdown with its
   upload button. Stacked rather than side by side: the button is the escape
   hatch for an empty dropdown, so it belongs under the thing it fills. */
.field-stack { display: flex; flex-direction: column; gap: var(--s2); }
.field-actions { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.field-actions .hint { margin: 0; }

/* ── contacts ─────────────────────────────────────────────────────── */

.toolbar { display: flex; gap: var(--s3); align-items: center; margin-bottom: var(--s4); }
.toolbar input[type="search"] {
  flex: 1; max-width: 340px;
  font: inherit; font-size: 13px; padding: 8px var(--s3);
  border: 1px solid var(--line-strong); border-radius: var(--r1);
  background: var(--ground); color: var(--ink);
}
/* An overview toolbar's search carries a magnifier inside the field, so its
   text has to start past the glyph. `.hsearch-i` says 34px at (0,1,0) and the
   rule above is (0,2,1), so the 12px won and the placeholder ran under the
   magnifier — measured on Companies, text at x279 against a glyph ending at
   x292. Declared here rather than beside the other .ov-toolbar rules because
   those sit earlier in the file and this one has to come after the rule it
   corrects, not merely match its weight. */
.ov-toolbar input[type="search"] { padding: 0 12px 0 34px; }
/* A toolbar holding nothing but the search. The 340px cap above is there to
   stop a field crowding the buttons beside it; with the buttons up in the band
   there is nothing to crowd, and a stub of a field under a full-width table
   reads as a leftover rather than as the search for it. */
.toolbar-search { display: block; }
.search-field { position: relative; display: block; }
.search-field svg {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--text-muted); pointer-events: none;
}
/* Padded past the glyph, not merely beside it — the placeholder ran under the
   magnifier at the inherited padding.
   The repeated `:not()` is load-bearing, not decoration. The generic field rule
   at the top of the component layer is
   `input:not([type=checkbox]):not([type=radio]):not([type=range]):not(.hsearch-i)`
   — four :not() pseudo-classes, specificity (0,5,1) — so a plain
   `.toolbar-search input[type="search"]` at (0,2,1) LOST to it and the
   placeholder ran back under the magnifier on EVERY toolbar search, not just
   Contacts. Read off computed style: padding-left resolved to 11px, and the
   16px glyph at x28-44 sat inside text starting at x27. Matching the weight
   here beats inventing an !important that the next rule has to out-shout. */
.toolbar-search input[type="search"]:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i) {
  max-width: none; width: 100%; padding-left: 36px;
}


/* The number under a known name while a call rings: an agent reading a name
   back to a caller still needs the number to hand. */
.incoming-sub { font-size: 12px; color: var(--ink-muted); margin: 2px 0 0; }
.incoming-add { margin-top: var(--s2); font-size: 12.5px; }

/* One dialog, used for adding and correcting a contact. A modal is right here
   and nowhere else in this product: the agent is mid-call, and anything that
   scrolls the screen underneath them loses the call they are looking at. */
.modal {
  position: fixed; inset: 0; z-index: 50;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--text-primary) 38%, transparent);
  animation: modal-in var(--fast) var(--ease);
}
@keyframes modal-in { from { opacity: 0; } }

.modal-card {
  width: min(92vw, 420px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r3);
  box-shadow: var(--lift-3);
  padding: var(--s5);
}
.modal-card h3 {
  font-size: 15px; font-weight: 600; letter-spacing: -.01em;
  margin: 0 0 var(--s4);
}

/* The × every dialog needs.
 *
 * The drawer has had one since it was built; modals never did, so the only way
 * out was Esc, the scrim, or finding Cancel at the bottom of a form tall enough
 * to scroll. Absolute rather than a flex row beside the heading: the eight call
 * sites build their own card markup, and this way none of them has to change.
 *
 * Padding-right on the heading so a long title stops before the button instead
 * of running underneath it. */
.modal-card { position: relative; }
.modal-card > h3 { padding-right: calc(var(--s5) + 32px); }
.modal-x {
  position: absolute; top: var(--s4); right: var(--s4);
  width: 30px; height: 30px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--r2);
  background: none; color: var(--text-muted); cursor: pointer;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.modal-x:hover { color: var(--text-primary); background: var(--paper-2); }
.modal-x:active { transform: translateY(1px); }
/* Above a scrolling form body, which would otherwise slide over it. */
.modal-x { z-index: 1; }
.modal-actions {
  display: flex; gap: var(--s2); justify-content: flex-end; margin-top: var(--s4);
  align-items: center; flex-wrap: wrap;
}
/* A modifier, not a verb. The internal-note toggle changes what the message
   IS; Close and Send are what you DO. Pushing it to the far left keeps the two
   verbs together as one decision instead of stringing three controls along one
   edge, where the eye has to read all three to find the primary. */
.modal-actions > .check { margin-right: auto; }
/* On a phone the row runs out of width, and a wrapped button that keeps
   shrinking is worse than one that takes its own line. The toggle drops to its
   own row, the verbs stay paired. */
@media (max-width: 560px) {
  .modal-actions > .check { margin-right: 0; flex: 1 0 100%; }
  .modal-actions > .btn { flex: 1; }
}
.modal-card .err { color: var(--danger); font-size: 12.5px; margin: var(--s2) 0 0; }

/* ── importing a spreadsheet ─────────────────────────────────────── */

/* Reachable by a screen reader and by the label that opens it, invisible to
   everybody else. Not display:none, which would take the input out of the
   accessibility tree and stop .click() opening the picker in some browsers. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* Wider than an ordinary modal: this one carries a drop zone and three
   figures, and at 380px the counts wrapped onto two lines. */
.imp-card { width: min(94vw, 460px); }

.imp-head { display: flex; gap: var(--s3); align-items: flex-start; margin-bottom: var(--s4); }
.imp-head h3 { margin: 0 0 2px; }
.imp-head .help { margin: 0; }
.imp-head-tile {
  width: 36px; height: 36px; border-radius: var(--r2); flex: none;
  display: grid; place-items: center;
  background: var(--violet-wash); color: var(--violet);
}
.imp-head-tile svg { width: 18px; height: 18px; }

/* The drop zone replaces the browser's own file input, which renders as a grey
   system button no stylesheet can reach — the one control on this screen that
   looked like it came from a different product. A button, not a div: it is
   pressed, so it must be reachable by keyboard and announce itself as such. */
.drop {
  display: block; width: 100%; padding: var(--s5) var(--s4);
  border: 1.5px dashed var(--line-strong); border-radius: var(--r3);
  background: var(--surface-sunken); color: inherit;
  font: inherit; text-align: center; cursor: pointer;
  transition: border-color var(--fast) linear, background var(--fast) linear;
}
.drop:hover { border-color: var(--accent); background: var(--accent-wash); }
.drop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* While a file is over it. Solid rather than dashed: the border stops being an
   invitation and becomes the edge of the thing about to receive the file. */
.drop.is-over {
  border-style: solid; border-color: var(--accent); background: var(--accent-wash-2);
}
/* Chosen. The dashes go for the same reason — there is nothing left to drop. */
.drop.has-file {
  border-style: solid; border-color: var(--accent);
  background: var(--accent-wash); text-align: left; padding: var(--s3);
}

.drop-empty { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.drop-title { font-size: 13.5px; font-weight: 600; color: var(--text-primary); }
.drop-empty .help { margin: 0; }
.drop-icon {
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center; margin-bottom: 2px;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--text-secondary);
}
.drop-icon svg { width: 18px; height: 18px; }
.drop-file { display: flex; align-items: center; gap: 10px; }
.drop-icon.is-file {
  width: 34px; height: 34px; flex: none; margin: 0;
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.drop-icon.is-file svg { width: 16px; height: 16px; }

/* The sample file, as a quiet second action under the zone. */
.imp-sample {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: var(--s3); padding: 0; border: 0; background: none;
  font: inherit; font-size: 12.5px; color: var(--text-secondary);
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.imp-sample:hover { color: var(--accent-ink); }
.imp-sample svg { width: 14px; height: 14px; }
.imp-sample:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* What the file would do, as three figures. This is the moment somebody
   decides whether to write to their own address book, so the counts are the
   content — a bullet list made them something to read past. */
.import-summary { margin-top: var(--s4); font-size: 13px; }
.imp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); }
.imp-stat {
  padding: var(--s3) var(--s2); border-radius: var(--r2); text-align: center;
  background: var(--surface-sunken); border: 1px solid var(--line);
}
.imp-stat-n {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: 20px; font-weight: 700; letter-spacing: -.02em;
  color: var(--text-primary); line-height: 1.1;
}
.imp-stat-l { font-size: 11.5px; color: var(--text-secondary); margin-top: 2px; }
.imp-stat.is-good .imp-stat-n { color: var(--accent-ink); }
/* Pushed away from the tile's own ground, not simply darkened: --danger is
   tuned for a filled button and a 1px border, and as a figure on the sunken
   tile it measured 4.41:1 in light. Darkening fixes light and breaks dark,
   where the token is already a light rose — so the mix goes toward black in
   one theme and toward white in the other. */
.imp-stat.is-bad .imp-stat-n { color: color-mix(in srgb, var(--danger) 82%, #000); }
:root[data-theme="dark"] .imp-stat.is-bad .imp-stat-n {
  color: color-mix(in srgb, var(--danger) 78%, #fff);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .imp-stat.is-bad .imp-stat-n {
    color: color-mix(in srgb, var(--danger) 78%, #fff);
  }
}
/* A zero is grey whatever the tile means — a red 0 skipped reads as a problem
   when it is the best possible outcome. Secondary rather than muted: muted is
   2.48:1 here, and this is a 20px figure somebody reads, not a hint. */
.imp-stat.is-zero .imp-stat-n { color: var(--text-secondary); }

/* The bad lines, folded away when there are many. Open by default when there
   are few, because eight is a list somebody fixes rather than a wall. */
.imp-problems { margin-top: var(--s3); font-size: 12px; }
.imp-problems summary {
  cursor: pointer; color: var(--text-secondary); font-size: 12.5px;
  padding: 2px 0;
}
.imp-problems summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.imp-problems ul {
  margin: var(--s2) 0 0; padding: 0; list-style: none;
  max-height: 150px; overflow-y: auto;
}
.imp-problems li {
  display: flex; gap: 8px; padding: 3px 0; color: var(--text-secondary);
}
/* Secondary, not muted. These are line numbers somebody reads off the screen
   and types into a spreadsheet's go-to box, so 2.64:1 was not good enough. */
.imp-line {
  flex: none; min-width: 52px;
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

/* ── tickets ──────────────────────────────────────────────────────── */

/* Status as a lamp, not a filled chip — the same rule the call rows follow.
   A filled badge gives a resolved ticket the same weight as an open one. */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; color: var(--ink-muted);
}
.pill::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: var(--ink-faint); flex: none;
}
.pill-open::before        { background: var(--queue); }
.pill-in_progress::before { background: var(--ring); box-shadow: 0 0 7px var(--ring-glow); }
.pill-pending::before     { background: var(--ink-faint); }
.pill-resolved::before    { background: var(--live); box-shadow: 0 0 7px var(--live-glow); }
.pill-resolved { opacity: .6; }

.btn.is-on { border-color: var(--accent); color: var(--ink); }

/* Wider than a form dialog: this one holds a conversation. */
.modal-wide { width: min(94vw, 620px); }
/* The plan editor carries three-across grids and a wrap of feature checkboxes.
   At 620px those collapse to one column each and the form becomes a scroll of
   forty rows, which is worse than the inline version it replaced. */
.modal-widest { width: min(96vw, 940px); }
/* Between the two: the new API key form, whose four permission cards sit two
   by two here instead of stacking into a long scroll at 620px. */
.modal-mid { width: min(94vw, 780px); }

/* The IVR tree editor: a tree beside the panel for whatever is selected.
   Declared here, after .modal-card, or the card's own width wins. */
.modal-tree { width: min(96vw, 1100px); }

/* A form modal carries a whole settings form, which is taller than a confirm
   dialog — cap it to the viewport and let the body scroll rather than pushing
   the buttons off-screen. The heading stays put so it is clear what is open.
   Both form classes are covered: agent/queue use .card-form, carrier and the
   other wiring screens use .panel-form. */
.modal-card:has(> .card-form),
.modal-card:has(> .panel-form) {
  max-height: min(86vh, 720px);
  display: flex; flex-direction: column;
}
.modal-card:has(> .card-form) > h3,
.modal-card:has(> .panel-form) > h3 { flex: none; }
.modal-card > .card-form,
.modal-card > .panel-form {
  overflow-y: auto; min-height: 0;
  margin: 0 calc(var(--s5) * -1) calc(var(--s5) * -1);
  padding: 0 var(--s5) var(--s5);
}
/* The grid's last row already carries the field's bottom margin; without this
   it stacks with .form-actions and leaves a visible hole above the buttons. */
.modal-card > .card-form > .form-grid,
.modal-card > .panel-form > .form-grid { margin-bottom: 0; }



.ticket-meta { display: flex; gap: var(--s4); }
.ticket-meta .field { flex: 1; margin-bottom: 0; }
.ticket-desc {
  font-size: 13.5px; line-height: 1.55; color: var(--ink);
  margin: var(--s4) 0 0; white-space: pre-wrap;
}

.ticket-thread { display: flex; flex-direction: column; gap: var(--s3); }
.msg { font-size: 13px; }
.msg p { margin: 4px 0 0; line-height: 1.5; white-space: pre-wrap; }
.msg-head {
  display: flex; align-items: baseline; gap: var(--s2);
  font-size: 11.5px; color: var(--ink-faint);
}
.msg-when { margin-left: auto; }

/* An internal note is marked, not hidden: everybody inside the company can
   read it, and the tint is what stops somebody reading one aloud to a
   customer. */
.msg-internal {
  background: var(--ground-sunken);
  border-left: 2px solid var(--line-strong);
  padding: var(--s2) var(--s3);
  border-radius: 0 var(--r1) var(--r1) 0;
}
.msg-tag {
  font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-faint);
}

.ticket-reply { margin-top: var(--s4); }
.ticket-reply textarea {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 13.5px;
  padding: var(--s3); border: 1px solid var(--line-strong);
  border-radius: var(--r1); background: var(--ground); color: var(--ink);
  resize: vertical;
}
.active-ticket { display: block; margin-top: var(--s3); font-size: 12.5px; }

/* Support statuses. Same lamp language as everything else: the extra state a
   support request needs is "waiting on you", and it reads as neither ours nor
   finished — a hollow ring rather than a filled dot. */
.pill-waiting_tenant::before {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--ring);
}
.pill-closed::before { background: var(--ink-faint); }
.pill-closed { opacity: .6; }

/* The company form: two columns at most. The shared .form-grid auto-fits, which
   on a wide screen puts four inputs on a row and scatters one logical record
   across the full width. */
.form-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* The address follows the grid, whose last row still carries a helper line.
   Without its own top margin the label lands on top of that helper text. */
.co-address { margin-top: var(--s4); }
.co-address textarea { width: 100%; resize: vertical; }
@media (max-width: 720px) { .form-grid-2 { grid-template-columns: 1fr; } }

/* ── settings: panels ──────────────────────────────────────────────
   The screen was one flat scroll: a 12.5px grey label, then full-size content,
   repeated four times with nothing containing anything. The label is correct
   on screens that are a single table — it is a caption for the thing below it.
   Here there are four unrelated kinds of thing on one page, so each gets a
   panel with a heading that is actually a heading.

   Scoped to .settings so the 41 other uses of .sec keep the caption they were
   designed with. */
/* Sub-navigation down the left, content on the right. */
.settings-layout {
  display: grid; grid-template-columns: 180px minmax(0, 1fr);
  gap: var(--s6); align-items: start; max-width: 940px;
}
.subnav { display: flex; flex-direction: column; gap: 2px; position: sticky; top: 0; }
.subnav-group {
  margin: var(--s4) 0 var(--s1); padding: 0 var(--s3);
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
}
.subnav-group:first-child { margin-top: 0; }
.subnav-item {
  appearance: none; border: 0; background: none; cursor: pointer;
  text-align: left; font: inherit; font-size: 13px;
  color: var(--text-secondary);
  padding: var(--s2) var(--s3); border-radius: var(--r2);
  /* The accent bar marks the open tab; the transparent one on every other
     item keeps the label from shifting 2px when it becomes current. */
  border-left: 2px solid transparent;
}
.subnav-item:hover { background: var(--surface-hover); color: var(--text-primary); }
.subnav-item.is-on {
  background: var(--accent-wash); color: var(--accent);
  border-left-color: var(--accent); font-weight: 600;
}
.settings-body { display: flex; flex-direction: column; gap: var(--s5); min-width: 0; }

@media (max-width: 720px) {
  .settings-layout { grid-template-columns: 1fr; gap: var(--s4); }
  .subnav { flex-direction: row; flex-wrap: wrap; position: static; }
  .subnav-group { display: none; }
  .subnav-item { border-left: 0; border-bottom: 2px solid transparent; }
  .subnav-item.is-on { border-left: 0; border-bottom-color: var(--accent); }
}

.panel {
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--ground-raised);
  box-shadow: var(--lift-1);
  overflow: hidden;
}
.panel-head {
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line);
  background: var(--ground);
}
/* 15px semibold in full-strength ink: the section is the most important thing
   in its own panel, and at 12.5px muted it read as a footnote to the content. */
.panel-head h2 {
  margin: 0; font-size: 15px; font-weight: 600;
  letter-spacing: -.01em; color: var(--ink);
}
.panel-head p { margin: 3px 0 0; font-size: 12.5px; color: var(--ink-faint); }
.panel-body { padding: var(--s5); }

/* A subnav row that NAVIGATES rather than switching a pane. It must not look
   identical to the rows above it, or the first click teaches the wrong thing
   about all of them — the chevron is the whole signal. */
.subnav-item.is-out { display: flex; align-items: center; gap: var(--s2); }
.subnav-item.is-out svg {
  width: 13px; height: 13px; margin-left: auto; flex: none; opacity: .5;
}

/* An explanation with an edge. A loose grey sentence has no boundary, so
   nothing separates "this explains the thing above" from "this is the next
   row" — which is how the name hint came to read as a fourth fact in the
   list below it. For a note ABOUT a group of things; a field's own one-line
   hint stays a plain `.field .help`, or a form becomes five boxes. */
.note {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 10px 12px; border-radius: var(--r2);
  background: var(--surface-sunken); border: 1px solid var(--border);
  font-size: 12.5px; line-height: 1.55; color: var(--text-secondary);
  /* Space BELOW as well. With `0` the badge explaining the name field sat
     flush against the facts list under it, which is the same collision the
     loose sentence had — a border alone does not separate two blocks that
     touch. Smaller above than below: a note belongs to what precedes it. */
  margin: var(--s3) 0 var(--s5);
}
/* Named containers only, never a bare :last-child — the note explaining the
   name field ends a plain wrapper div, so `:last-child` stripped its margin
   and it went straight back to touching the facts list. The identical trap
   `.help` fell into an hour earlier. */
.panel-body > .note:last-child,
.drawer-body > .note:last-child { margin-bottom: 0; }
.note .note-ic {
  width: 18px; height: 18px; border-radius: 5px; flex: none; margin-top: 1px;
  display: grid; place-items: center;
  background: var(--surface-hover); color: var(--text-muted);
}
.note .note-ic svg { width: 11px; height: 11px; }
.note b { color: var(--text-primary); font-weight: 600; }
/* A consequence rather than a description takes the warn ground — the same
   distinction the password rules draw. */
.note.is-warn { background: var(--warn-wash); border-color: transparent; color: var(--ring-ink); }
.note.is-warn .note-ic { background: rgba(255,255,255,.55); color: var(--ring-ink); }
.note.is-warn b { color: var(--ring-ink); }

/* ── password ─────────────────────────────────────────────────────── */

/* The form keeps an honest width — a 700px box for twelve characters reads as
   broken — and the space beside it carries what the form MEANS, rather than
   being left empty by one narrow column stretched across the panel. */
.pw-split {
  display: grid; grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
  gap: var(--s6); align-items: start;
}
.pw-aside { display: flex; flex-direction: column; gap: var(--s4); }
.sess-card {
  padding: var(--s4); border-radius: var(--r2);
  background: var(--surface-sunken); border: 1px solid var(--border);
}
.sess-card h4 {
  margin: 0 0 var(--s2); font-size: 10.5px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted);
}
/* Below 820px the companion column stops being a column. */
@media (max-width: 820px) {
  .pw-split { grid-template-columns: 1fr; }
}

/* The rules, stated before anybody types. Not a warning box: these are the
   terms of the thing you are about to do, and two of them the product would
   otherwise teach by refusing you. */
.pw-rules {
  display: flex; flex-direction: column; gap: var(--s2);
  padding: var(--s4); border-radius: var(--r2);
  background: var(--surface-sunken); border: 1px solid var(--border);
  margin-bottom: var(--s5);
}
.pw-rule { display: flex; gap: 9px; align-items: flex-start; font-size: 12.5px; line-height: 1.5; }
.pw-rule .ic {
  width: 18px; height: 18px; border-radius: 5px; flex: none; margin-top: 1px;
  display: grid; place-items: center;
  background: var(--surface-hover); color: var(--text-muted);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.pw-rule .ic svg { width: 11px; height: 11px; }
/* Met takes the ACCENT, never the live green: a satisfied rule is not a call,
   and only a live call is allowed to be bright. */
.pw-rule.is-met .ic { background: var(--accent-wash-2); color: var(--accent-ink); }
.pw-rule.is-met { color: var(--text-muted); }
/* The session rule is a consequence, not a condition — it can never be met, so
   it never ticks and it carries the warn hue instead. */
.pw-rule.is-note .ic { background: var(--warn-wash); color: var(--ring-ink); }

/* One column, not the three-across grid the inline version used: these are a
   sequence — old, new, new again — and a row of three reads as three
   unrelated settings. */
/* Width comes from the .pw-split column, not a second number to keep in sync. */
.pw-form .field { margin-bottom: var(--s4); }

/* Reveal sits inside the new-password field only. */
.pw-reveal { position: relative; }
.pw-reveal input { padding-right: 40px; }
.pw-eye {
  position: absolute; right: 6px; bottom: 6px;
  width: 28px; height: 28px; border-radius: var(--r1); border: 0;
  background: none; color: var(--text-muted); cursor: pointer;
  display: grid; place-items: center;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.pw-eye:hover { background: var(--surface-hover); color: var(--text-primary); }
.pw-eye:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pw-eye svg { width: 15px; height: 15px; }

/* Where else you are signed in. One line, because the table holds no
   user-agent and no IP — a list of rows all reading "3 days ago" would look
   like information and answer nothing. */
.sess-line { display: flex; gap: 10px; align-items: flex-start; margin: 0; font-size: 13px; }
.sess-ic {
  width: 22px; height: 22px; border-radius: 6px; flex: none;
  display: grid; place-items: center;
}
.sess-ic svg { width: 12px; height: 12px; }
.sess-ic.is-ok   { background: var(--accent-wash-2); color: var(--accent-ink); }
.sess-ic.is-warn { background: var(--warn-wash); color: var(--ring-ink); }
.sess-when { color: var(--text-muted); }
.sess .panel-actions { margin-top: var(--s3); }

/* Your own name: one field, one Save, on a line. The rest of this panel is not
   a form, so wrapping it in one to save a single field would be the larger
   change, not the smaller. */
/* No top border: this form follows `.logo-card`, which already draws a
   border-bottom, and `.panel-form`'s own border-top stacked a second line
   directly under it — two rules separating the same pair of things. */
.name-form { display: flex; gap: var(--s2); align-items: flex-end; flex-wrap: wrap;
             border-top: 0; padding-top: 0; }
.name-form .field { flex: 1; min-width: 200px; margin-bottom: 0; }
.name-form .form-error { flex-basis: 100%; margin: 0; }
.name-form .form-error:empty { display: none; }
/* The form and grid inside a panel bring their own margins; without this they
   stack with the panel's padding and leave a gutter that reads as a rendering
   fault rather than as spacing. */
.panel-body > .card-form,
.panel-body > .form-grid { margin: 0; }
.panel-body > :first-child { margin-top: 0; }
.panel-body > :last-child { margin-bottom: 0; }
.panel-body .field:last-child { margin-bottom: 0; }
.panel-actions { display: flex; gap: var(--s2); margin-top: var(--s4); }
.panel-body > .panel-actions:first-child { margin-top: 0; }
/* The last row of a grid already carries the field's bottom margin. */
.panel-body > .form-grid > .field { margin-bottom: 0; }

/* Closing the account is not the same kind of panel as the rest. */
.panel-danger { border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); }
.panel-danger .panel-head { background: color-mix(in srgb, var(--danger) 6%, transparent); }
.panel-danger .panel-head h2 { color: var(--danger); }

/* Your details: four facts, as a definition list. A two-column table drew
   full-width rules across the panel for what is really a short label/value
   pair, which is why the top of this screen looked like a report. */
.facts {
  display: grid; grid-template-columns: minmax(0, 140px) minmax(0, 1fr);
  /* Room under the last row. A `.help` line explaining the list sits below it
     with only its own 8px top margin, which put the sentence closer to
     "Extension" than "Extension" was to "Role" — it read as a fourth row. */
  gap: var(--s3) var(--s4); margin: 0 0 var(--s2);
}
.facts dt { font-size: 12.5px; color: var(--ink-faint); }
.facts dd {
  margin: 0; font-size: 13px; color: var(--ink);
  overflow-wrap: anywhere;
}
.facts dd.is-empty { color: var(--ink-muted); }

/* ── company logo ──────────────────────────────────────────────────
   Sits inside the company panel, so it must not repeat the panel's own frame:
   a bordered box inside a bordered box is the "cards inside cards" look. A
   rule underneath separates it from the fields instead. */
.logo-card {
  display: flex; gap: var(--s4); align-items: center;
  padding-bottom: var(--s5); margin-bottom: var(--s5);
  border-bottom: 1px solid var(--line);
}
.logo-shot {
  flex: none; width: 56px; height: 56px; border-radius: var(--r2);
  display: grid; place-items: center; overflow: hidden;
  border: 1px solid var(--line); background: var(--ground);
}
.logo-shot img { width: 100%; height: 100%; object-fit: contain; }
/* Initials while there is no logo — a blank square reads as a broken image. */
.logo-none {
  font-size: 17px; font-weight: 600; color: var(--ink-muted); letter-spacing: .02em;
}
.logo-body { min-width: 0; }
.logo-body h4 { margin: 0 0 2px; font-size: 13px; font-weight: 600; color: var(--ink); }
.logo-body .help { margin: 0; font-size: 12.5px; }
.logo-actions { display: flex; gap: var(--s3); align-items: center; margin-top: var(--s3); }
.logo-actions .danger { color: var(--danger); }
.logo-body .help.err { color: var(--danger); }

@media (max-width: 560px) {
  .logo-card { flex-direction: column; align-items: flex-start; }
  .facts { grid-template-columns: 1fr; gap: var(--s1) 0; }
  .facts dd { margin-bottom: var(--s3); }
}

/* ── account menu ──────────────────────────────────────────────────
   The bar carried presence, name, break, theme, language and sign out — six
   controls, of which only presence and break are things an agent needs while
   a call is ending. The rest are about the person, so they live behind the
   avatar where a SaaS user already looks for them. */
.account { position: relative; }
.avatar-btn {
  appearance: none; border: 0; background: none; padding: 0; cursor: pointer;
  border-radius: 50%; line-height: 0;
}
.avatar-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.avatar {
  display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--accent); color: #fff;
  font-size: 11.5px; font-weight: 600; letter-spacing: .02em;
}
.avatar-lg { width: 38px; height: 38px; font-size: 13px; }
/* A ring rather than a brightness filter: the disc carries the person's
   initials, and a filter fades those with the fill. The ring also gives the
   circle a hit edge, which a 30px disc with no border does not otherwise
   have. */
.avatar-btn .avatar { transition: box-shadow var(--fast) var(--ease); }
.avatar-btn:hover .avatar { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent); }

/* The contact-row disc. Declared HERE, after .avatar, and not beside the rest
   of the contact table where it belongs by subject — because .avatar above
   sets background and color at the same single-class specificity, so the
   later rule wins. Sitting it next to the table read correctly in the source
   and rendered as four identical green discs; caught by reading computed
   style, which is the second time this file has been bitten by exactly this.
   Do not move it back up without also raising its specificity. */
.avatar-sm {
  width: 28px; height: 28px; flex: none;
  font-size: 10.5px; font-weight: 600; letter-spacing: .01em;
  /* Solid discs at the hue's own strength, the way the reference has them.

     The letters are NOT always white. These tokens are chosen to be legible
     as text on the page ground, so their lightness varies a lot: white holds
     on light-theme blue and violet (5.2:1, 5.7:1) but collapses on brand and
     amber (3.2:1), and on every dark-theme hue, which are light by design —
     white on dark amber is 1.7:1, a disc with no readable initials at all.
     Darkening the disc until white worked turned all four near-black and lost
     the hue. So the ink is set per hue below: a deep tint of the hue itself
     where that reads better, white where it does not. Every pairing measured
     above 4.5:1 in both themes. */
  background: var(--av-hue, var(--accent));
  color: var(--av-ink, #fff);
}
/* Brand and amber are too light to carry white in either theme, so they take a
   deep tint of themselves. */
.avatar-sm.is-brand {
  --av-hue: var(--accent);
  --av-ink: color-mix(in srgb, var(--accent) 22%, #000);
}
.avatar-sm.is-amber {
  --av-hue: var(--amber);
  --av-ink: color-mix(in srgb, var(--amber) 22%, #000);
}
/* Blue and violet are dark enough for white in the light theme and far too
   light for it in the dark one, so the ink flips with the theme rather than
   being fixed. */
.avatar-sm.is-blue   { --av-hue: var(--blue);   --av-ink: #fff; }
.avatar-sm.is-violet { --av-hue: var(--violet); --av-ink: #fff; }
:root[data-theme="dark"] .avatar-sm.is-blue,
:root[data-theme="dark"] .avatar-sm.is-violet {
  --av-ink: color-mix(in srgb, var(--av-hue) 22%, #000);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .avatar-sm.is-blue,
  :root:not([data-theme="light"]) .avatar-sm.is-violet {
    --av-ink: color-mix(in srgb, var(--av-hue) 22%, #000);
  }
}

.menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  width: 264px; padding: var(--s2);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); box-shadow: var(--lift-3);
  animation: menu-in var(--fast) var(--ease);
}
@keyframes menu-in { from { opacity: 0; transform: translateY(-4px); } }

.menu-head { display: flex; gap: var(--s3); align-items: center; padding: var(--s3); }
.menu-id { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.menu-id strong { font-size: 13px; font-weight: 600; color: var(--text-primary); }
.menu-id span { font-size: 11.5px; color: var(--text-muted); overflow-wrap: anywhere; }
.menu-role {
  align-self: flex-start; margin-top: 3px;
  padding: 1px var(--s2); border-radius: 999px;
  background: var(--accent-wash); color: var(--accent);
  font-size: 10.5px; font-weight: 600;
}
.menu-sep { height: 1px; background: var(--border); margin: var(--s2) 0; }

.menu-item {
  display: flex; align-items: center; gap: var(--s3); width: 100%;
  appearance: none; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 13px; color: var(--text-primary);
  padding: var(--s2) var(--s3); border-radius: var(--r2); text-align: left;
}
.menu-item:hover { background: var(--surface-hover); }
.menu-item svg { flex: none; color: var(--text-muted); }
.menu-item > span:first-of-type { flex: 1; }
/* The current theme, language or plan, so the menu answers "what is it now?"
   without opening the screen it links to. */
.menu-note { font-size: 11.5px; color: var(--text-muted); }

/* ── a configuration card's body ──────────────────────────────────
   Mail, TTS, STT, LLM and Google OAuth all put a form inside .ov-card. The
   card already pays its 16/18px padding, and .card-form pays its own margins,
   so the two together doubled the gap under every header. Zeroed here rather
   than at five call sites: one rule, and a form that wants a margin still
   sets one on itself.

   Matched at .cfg-body > .card-form (0,2,0) — the same weight as the
   .ov-card .body rule it has to beat on the top edge, decided by source
   order, which is why it is declared at the end of the file. */
.cfg-body > .card-form, .cfg-body > div > .card-form { margin: 0; }
.cfg-body > .alert { margin-bottom: 12px; }
/* A heading inside a form that is already inside a card. .sec would put 24px
   above it and restart the page's own rhythm one level too deep. */
.cfg-part { margin: 18px 0 10px; }
.cfg-part h3 { margin: 0; font-size: 13px; font-weight: 600; }
.cfg-part p { margin: 3px 0 0; font-size: 12px; color: var(--text-secondary); }
/* Stat cards nested inside a card body carry no bottom margin of their own:
   the body's padding is the gap. Three across rather than auto-fill: this is
   a fixed set of three, and auto-fill left a third of the card empty. */
.stats.stats-flush { margin-bottom: 0; grid-template-columns: repeat(3, minmax(0,1fr)); }
@media (max-width: 760px) { .stats.stats-flush { grid-template-columns: 1fr; } }

/* A card header's right slot holding more than one thing — Tickets puts its
   filter toolbar there beside the count. */
.right-pair { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* The toolbar's own 16px bottom margin was for a strip sitting on the page
   ground. Inside a header it is a header row, and the margin pushed the
   count off the title's baseline. Matched at .toolbar.tk-filters (0,2,0),
   one step above the bare .toolbar it has to beat. */
.toolbar.tk-filters { margin-bottom: 0; }
.toolbar.tk-filters select, .toolbar.tk-filters .btn { font-size: 12.5px; }

/* The support-session bar.
   Amber rather than the live green: a live call is the only thing allowed to
   be bright (DESIGN.md), and this is a warning about who you are, not an
   event. Sticky at the top so scrolling a long report cannot hide it.

   Both tokens are defined for light AND dark in tokens.css, so no literal
   fallback: a hardcoded hex here would win in dark mode and the bar would be
   the one element that ignored the theme. That exact mistake is recorded in
   CLAUDE.md about the door. */
.imp-bar {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; gap: var(--s3);
  padding: 8px var(--s4);
  background: var(--amber-wash);
  color: var(--ink);
  border-bottom: 1px solid var(--amber);
  font-size: 13px;
}
.imp-bar strong { font-weight: 600; }
.imp-bar .imp-ro { color: var(--ink-faint); font-size: 12px; }
.imp-bar button { margin-left: auto; }

/* ── plans (platform) ─────────────────────────────────────────────
   List/grid toggle at the toolbar's end; the chosen one inked like a chip. */
.plan-views { display: flex; gap: 4px; margin-left: auto; }
.plan-views .row-act[aria-pressed="true"] { background: var(--text-primary); border-color: var(--text-primary); color: var(--bg); }
.plan-lim-ic { width: 20px; display: grid; place-items: center; color: var(--text-muted); flex: none; }
.plan-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; padding: 0 14px 14px; }
.plan-card { display: flex; flex-direction: column; gap: 10px; padding: 16px; min-width: 0;
  border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); }
.plan-card-price { display: flex; align-items: baseline; gap: 4px; }
.plan-card-price b { font-size: 22px; font-weight: 700; }
.plan-card-price span { font-size: 12px; color: var(--text-muted); }
.plan-card-foot { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 10px; border-top: 1px solid var(--border); }
.plan-card-foot .kebab { margin-left: auto; }
/* Fixed, like .tbl-tenants: auto layout let a long description widen the
   name column until the action rank was clipped off the card (box,
   2026-10-05, at ~1240px). The description now ellipses instead. */
.tbl-plans { table-layout: fixed; }
.tbl-plans th:nth-child(1) { width: 21%; }
.tbl-plans th:nth-child(2), .tbl-plans th:nth-child(3) { width: 11%; }
.tbl-plans th:nth-child(4) { width: 15%; }
.tbl-plans th:nth-child(5) { width: 8%; }
.tbl-plans th:nth-child(6) { width: 10%; }
.tbl-plans th:nth-child(7) { width: 13%; }
.tbl-plans td:nth-child(7) { padding-right: var(--s4); }
/* Three 28px buttons and their gaps, as .tbl-tenants sizes its rank. */
.tbl-plans th:last-child { width: 116px; }
/* The ••• drawn like the two buttons beside it, so the rank reads as three. */
.tbl-plans .kebab-trigger, .plan-card .kebab-trigger {
  border: 1px solid var(--border-strong); border-radius: var(--r1); background: var(--surface); }
/* Too narrow for eight columns, too wide for the phone layout: the yearly
   price steps out (the row's drawer still has it) so dates and prices are
   not cut. */
@media (min-width: 901px) and (max-width: 1200px) {
  .tbl-plans th:nth-child(3), .tbl-plans td:nth-child(3) { display: none; }
  .tbl-plans th:nth-child(1) { width: 25%; }
  .tbl-plans th:nth-child(7) { width: 16%; }
}
@media (min-width: 901px) and (max-width: 1060px) {
  .tbl-plans th:nth-child(7), .tbl-plans td:nth-child(7) { display: none; }
  .tbl-plans th:nth-child(2) { width: 15%; }
}

/* Sign in while its request is out (busyWhile in app.js): still the teal
   button, now with a spinner — working, not switched off. Declared after
   the door's own :disabled rule so it wins. */
.door-form button[type="submit"][aria-busy="true"] {
  opacity: 1; cursor: wait; color: transparent; position: relative; }
.door-form button[type="submit"][aria-busy="true"]::after {
  content: ''; position: absolute; left: calc(50% - 8px); top: calc(50% - 8px);
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid #fff; border-bottom-color: transparent; animation: spin .7s linear infinite; }

/* List filters (listFilters() in app.js): the Tenants toolbar, above a table
   instead of inside a card, so it needs its own gap below. */
.list-filters { margin-bottom: 12px; }
.list-more { display: flex; justify-content: center; margin: 14px 0 4px; }
/* Tenants pushes its one sort select to the far right; a filter row has
   several, and on a phone the push left the first one stranded mid-line. */
.list-filters > select { margin-left: 0; flex: 1 1 140px; max-width: 220px; }
/* The search in the tickets card header: narrower than a toolbar search,
   since it shares the row with the card title. */
.toolbar.tk-filters .hsearch { flex: 0 1 220px; min-width: 140px; }
.toolbar.tk-filters .hsearch-i { height: 32px; font-size: 12.5px; }
/* .toolbar input[type="search"] (0,2,1) resets the padding the icon needs. */
.toolbar.tk-filters .hsearch-i { padding-left: 32px; }
/* On a phone the card header is one column: the filters take its full width
   and share it, rather than shrinking each select to its chevron. */
@media (max-width: 780px) {
  .right-pair:has(> .tk-filters) { width: 100%; }
  .toolbar.tk-filters { width: 100%; flex-wrap: wrap; }
  .toolbar.tk-filters .hsearch { flex: 1 1 100%; }
  .toolbar.tk-filters select { flex: 1 1 0; min-width: 0; }
}
/* A particular date beside the period chips. Same height and edge as the
   filter selects, so the row reads as one set of controls. */
/* The console-wide field rule is (0,5,1) and sets width:100%; restating its
   :not() chain plus our class outranks it by one, as the door's inputs do. */
.list-filters input.f-date:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i) {
  height: 34px; width: auto; flex: 0 0 auto; padding: 0 10px; font: inherit; font-size: 12.5px;
  border: 1px solid var(--border); border-radius: 9px; background: var(--surface); color: var(--text-primary);
}
@media (max-width: 780px) {
  .list-filters input.f-date:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(.hsearch-i) { flex: 1 1 100%; }
}
/* The search keeps room for its placeholder; when the row is full the
   selects drop to a second line rather than squeezing it. */
.list-filters .hsearch { flex: 1 1 220px; min-width: 220px; }
@media (max-width: 780px) { .list-filters .hsearch { min-width: 0; flex-basis: 100%; } }

/* ── the new API key form ────────────────────────────────────────────
 * One column of lettered sections. A grid put the expiry at the foot of a
 * tall empty column beside the abilities, and each ability's code drifted
 * sideways as its label wrapped. */
.key-form { display: grid; gap: var(--s4); }
.key-step { display: flex; align-items: center; gap: var(--s2); }
.key-step .help { margin: 0; }
.key-sec { border-top: 1px solid var(--border); padding-top: var(--s4); }
.key-sec > h4 { display: flex; align-items: center; gap: 8px; margin: 0 0 var(--s2); font-size: 14px; }
.key-sec > .help { margin: 0 0 var(--s3); }
.key-sec-l {
  display: grid; place-items: center; width: 22px; height: 22px; border-radius: 6px;
  font: 600 12px var(--font-ui); color: var(--accent-ink); background: var(--accent-wash);
}
.key-res-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }
@media (max-width: 560px) { .key-res-list { grid-template-columns: minmax(0, 1fr); } }
.key-res {
  margin: 0; padding: 10px 12px 6px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface-sunken); min-width: 0;
}
.key-res > legend { padding: 0 6px; font-size: 12px; font-weight: 600; color: var(--text-secondary); }
.key-ab { display: flex; align-items: flex-start; gap: 8px; padding: 6px 0; cursor: pointer; }
.key-ab input { margin-top: 3px; flex: none; }
.key-ab > span { display: grid; gap: 1px; min-width: 0; }
.key-ab-name { font-size: 13px; color: var(--text-primary); }
.key-ab-code { font: 11.5px var(--font-num); color: var(--text-muted); }
.key-note {
  display: flex; gap: 8px; align-items: flex-start; padding: 10px 12px; border-radius: 10px;
  background: var(--surface-sunken); border: 1px solid var(--border); font-size: 13px; color: var(--text-secondary);
}
.key-note svg { flex: none; margin-top: 2px; }
.key-two { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0 var(--s4); }
.key-sum {
  border-radius: 10px; padding: 12px 14px; background: var(--surface-sunken); border: 1px solid var(--border);
}
.key-sum h4 { margin: 0 0 8px; font-size: 13px; }
.key-sum dl { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 0; font-size: 12.5px; }
.key-sum dt { color: var(--text-muted); }
.key-sum dd { margin: 0; color: var(--text-primary); }
.key-sum.is-empty { border-color: var(--warn); }
