/* base.css — global reset, typography, header + footer chrome.
   Font stack: system-first for VI diacritics coverage without needing
   external CDN fonts (CSP forbids font-src beyond 'self'). Segoe UI
   (Windows), Helvetica Neue (macOS), Roboto (Android/Chrome) all render
   Vietnamese tone marks cleanly. "Be Vietnam Pro" left in the stack in
   case someone self-hosts it later. */

* { box-sizing: border-box; }

/* Background on html only — this becomes the canvas (bottommost layer).
   Keeping body background-less (transparent) lets .bg-fx { z-index:-1 }
   render above the canvas but below body content, making bubbles visible. */
html {
  margin: 0;
  padding: 0;
  min-height: 100vh;
  background-color: #eef1fe;
  background-image:
    radial-gradient(ellipse 68% 60% at 94%  6%, rgba(79, 102, 241, .22) 0%, transparent 56%),
    radial-gradient(ellipse 64% 60% at  4% 96%, rgba(13, 148, 136, .17) 0%, transparent 56%),
    radial-gradient(ellipse 90% 55% at 50% 128%, rgba(30, 64, 175, .10) 0%, transparent 60%);
  background-attachment: fixed;
}

body {
  margin: 0;
  padding: 0;
  min-height: 100vh;
  font-family: "Be Vietnam Pro", "Inter", "Segoe UI", system-ui,
               -apple-system, "Helvetica Neue", Roboto, "Noto Sans", sans-serif;
  font-size: 15px;
  line-height: 1.5;
  color: #1f2937;
}

a {
  color: #2563eb;
  text-decoration: none;
}

a:hover { text-decoration: underline; }

button {
  font-family: inherit;
  font-size: 14px;
  cursor: pointer;
}

input, select, textarea {
  font-family: inherit;
  font-size: 14px;
  color: inherit;
}

h1, h2, h3, h4 { margin: 0 0 8px 0; font-weight: 600; }
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.018em; }
h2 { font-size: 18px; letter-spacing: -0.01em; }
h3 { font-size: 15px; }

/* --- Ambient animated backdrop --------------------------------------- */
/* Floating soft orbs that drift slowly upward behind the whole app.
   Lives at z-index:-1 so it sits above the body gradient but below all
   content; translucent chrome (topbar/sidebar/cards) blurs it through. */
.bg-fx {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
.bg-fx span {
  position: absolute;
  bottom: -120px;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  /* Indigo orb — saturated so it shows on the light background */
  background: radial-gradient(circle at 35% 30%,
    rgba(165, 170, 255, .65) 0%, rgba(99, 102, 241, .30) 55%, rgba(99, 102, 241, 0) 100%);
  opacity: 0;
  will-change: transform, opacity;
  animation: bubble-rise 24s linear infinite;
}
.bg-fx span:nth-child(3n) {
  background: radial-gradient(circle at 35% 30%,
    rgba(94, 210, 200, .60) 0%, rgba(13, 148, 136, .26) 55%, rgba(13, 148, 136, 0) 100%);
}
.bg-fx span:nth-child(3n+2) {
  background: radial-gradient(circle at 35% 30%,
    rgba(253, 186, 116, .60) 0%, rgba(249, 115, 22, .24) 55%, rgba(249, 115, 22, 0) 100%);
}

.bg-fx span:nth-child(1)  { left:  5%; width: 32px; height: 32px; animation-duration: 27s; animation-delay:  -3s; }
.bg-fx span:nth-child(2)  { left: 16%; width: 56px; height: 56px; animation-duration: 33s; animation-delay:  -9s; }
.bg-fx span:nth-child(3)  { left: 27%; width: 24px; height: 24px; animation-duration: 21s; animation-delay: -14s; }
.bg-fx span:nth-child(4)  { left: 38%; width: 44px; height: 44px; animation-duration: 30s; animation-delay:  -1s; }
.bg-fx span:nth-child(5)  { left: 48%; width: 30px; height: 30px; animation-duration: 25s; animation-delay: -19s; }
.bg-fx span:nth-child(6)  { left: 58%; width: 64px; height: 64px; animation-duration: 38s; animation-delay:  -6s; }
.bg-fx span:nth-child(7)  { left: 67%; width: 28px; height: 28px; animation-duration: 23s; animation-delay: -11s; }
.bg-fx span:nth-child(8)  { left: 75%; width: 50px; height: 50px; animation-duration: 31s; animation-delay: -22s; }
.bg-fx span:nth-child(9)  { left: 83%; width: 22px; height: 22px; animation-duration: 20s; animation-delay:  -4s; }
.bg-fx span:nth-child(10) { left: 90%; width: 38px; height: 38px; animation-duration: 28s; animation-delay: -16s; }
.bg-fx span:nth-child(11) { left: 96%; width: 30px; height: 30px; animation-duration: 26s; animation-delay:  -8s; }
.bg-fx span:nth-child(12) { left: 33%; width: 36px; height: 36px; animation-duration: 35s; animation-delay: -25s; }

@keyframes bubble-rise {
  0%   { transform: translate3d(0, 0, 0) scale(.6);  opacity: 0; }
  12%  { opacity: .80; }
  85%  { opacity: .72; }
  100% { transform: translate3d(36px, -114vh, 0) scale(1.25); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .bg-fx { display: none; }
}

/* --- Brand type utilities -------------------------------------------- */

/* Gradient keyword treatment — paints the brand sweep onto text and
   slowly shimmers it across the word so the header reads as "alive". */
.grad-text {
  background: linear-gradient(100deg,
    #1e40af 0%, #4f46e5 25%, #0f766e 50%, #4f46e5 75%, #1e40af 100%);
  background-size: 220% auto;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: grad-shimmer 7s linear infinite;
}
@keyframes grad-shimmer {
  to { background-position: 220% center; }
}
@media (prefers-reduced-motion: reduce) {
  .grad-text { animation: none; }
}

/* Eyebrow / kicker label — small uppercase tracked tag above a title.
   The leading dot keys the page into the brand accent. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: #4f46e5;
}
.eyebrow::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #f97316;
  box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.18);
}

/* --- Waveform signature ---------------------------------------------- */
/* The brand motif: a small animated equaliser that says "voice" at a
   glance. CSS-only; bars scale on a staggered loop. Falls back to a
   static varied silhouette when the user prefers reduced motion. */
.wave-mark {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 26px;
  filter: drop-shadow(0 2px 6px rgba(79, 70, 229, 0.35));
}
.wave-mark span {
  display: block;
  width: 3px;
  height: 40%;
  border-radius: 3px;
  background: linear-gradient(180deg, #4f46e5, #0f766e);
  transform-origin: center;
  animation: wave-bounce 1.15s ease-in-out infinite;
}
.wave-mark span:nth-child(1) { animation-delay: -0.9s; }
.wave-mark span:nth-child(2) { animation-delay: -0.5s; }
.wave-mark span:nth-child(3) { animation-delay: -0.1s; background: linear-gradient(180deg, #f97316, #4f46e5); }
.wave-mark span:nth-child(4) { animation-delay: -0.7s; }
.wave-mark span:nth-child(5) { animation-delay: -0.3s; }
.wave-mark span:nth-child(6) { animation-delay: -0.6s; }
.wave-mark span:nth-child(7) { animation-delay: -0.2s; }

@keyframes wave-bounce {
  0%, 100% { transform: scaleY(0.35); opacity: 0.65; }
  50%      { transform: scaleY(1.4);  opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .wave-mark span { animation: none; }
  .wave-mark span:nth-child(1) { height: 45%; }
  .wave-mark span:nth-child(2) { height: 80%; }
  .wave-mark span:nth-child(3) { height: 100%; }
  .wave-mark span:nth-child(4) { height: 60%; }
  .wave-mark span:nth-child(5) { height: 90%; }
  .wave-mark span:nth-child(6) { height: 50%; }
  .wave-mark span:nth-child(7) { height: 70%; }
}

/* --- Header ----------------------------------------------------------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: rgb(255 255 255 / 79%);
  -webkit-backdrop-filter: saturate(200%) blur(20px);
  backdrop-filter: saturate(200%) blur(20px);
  border-bottom: 1px solid rgba(99, 102, 241, 0.18);
  box-shadow: 0 1px 0 rgba(79, 70, 229, 0.10);
  padding: 10px 20px;
  gap: 16px;
  position: sticky;
  top: 0;
  z-index: 40;
}

.topbar__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 16px;
  color: #111827;
}

.topbar__brand-img {
  height: 64px;
  width: auto;
  display: block;
}

.topbar__right {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  flex-shrink: 1;
}

/* Admin button in topbar - hide text on smaller screens, keep icon */
.topbar__right .btn--sm {
  white-space: nowrap;
  flex-shrink: 0;
}

.user-chip {
  position: relative;
}

.user-chip__button {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border: 1px solid #e5e7eb;
  border-radius: 999px;
  background: #ffffff;
  color: #111827;
  font-weight: 500;
  max-width: 200px;
}

.user-chip__avatar {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #1e40af;
  color: white;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 600;
}

.user-chip__menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 10px;
  min-width: 200px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
  padding: 6px;
  display: none;
  z-index: 50;
}

.user-chip[data-open="true"] .user-chip__menu { display: block; }

.user-chip__menu a,
.user-chip__menu button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 12px;
  border: none;
  background: transparent;
  color: #111827;
  border-radius: 6px;
  text-decoration: none;
}

.user-chip__menu a:hover,
.user-chip__menu button:hover {
  background: #f3f4f6;
  text-decoration: none;
}

.user-chip__menu button { cursor: pointer; }

/* --- Context bar ------------------------------------------------------ */
/* One strip, three severities. Shown only when the answer to "whose data am I
   looking at" has stopped being obvious -- see templates/_context_bar.html for
   why this is not a permanent header. */

.context-bar {
  /* Pinned, and it exports its height as --topbar-h (set by org-context.js).
     Several pages already anchor their own sticky elements to that variable,
     so a strip that only pushed the layout down would leave every one of them
     50px out of place -- and the strip itself scrolled out of view, which is
     how the first version of this shipped: rendered, display:flex, and sitting
     at y = -50. */
  position: sticky;
  top: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 8px 20px;
  font-size: 13px;
  border-bottom: 1px solid transparent;
}

/* `display: flex` above beats the user-agent's `[hidden] { display: none }`,
   so the dismiss button would hide nothing -- the same trap the PBX keypad
   panel hit once. Stated explicitly rather than relying on specificity. */
.context-bar[hidden] { display: none; }

.context-bar__text { font-weight: 500; margin-right: auto; }

/* Working inside somebody else's organization: amber, informational. */
.context-bar--warn {
  background: #fffbeb;
  color: #92400e;
  border-bottom-color: #fde68a;
}

/* Impersonating, or viewing every tenant at once -- the two states where an
   action can land somewhere you did not name. */
.context-bar--danger {
  background: #fef2f2;
  color: #991b1b;
  border-bottom-color: #fecaca;
}

.context-bar__label {
  font-size: 12px;
  opacity: 0.8;
}

.context-bar__select {
  padding: 5px 8px;
  border-radius: 6px;
  border: 1px solid currentColor;
  background: #ffffff;
  color: inherit;
  font-size: 13px;
  max-width: 220px;
}

.context-bar__close {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: 6px;
  border: 1px solid transparent;
  background: transparent;
  color: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
}
.context-bar__close:hover { opacity: 1; border-color: currentColor; }

.context-bar__action {
  padding: 6px 12px;
  border-radius: 6px;
  border: 1px solid currentColor;
  background: #ffffff;
  color: inherit;
  font-weight: 600;
  cursor: pointer;
}

@media (max-width: 640px) {
  .context-bar { padding: 8px 12px; gap: 8px; }
  /* The label is redundant next to the picker once space is tight. */
  .context-bar__label { display: none; }
  .context-bar__text { flex: 1 1 100%; }
}

/* --- Impersonation banner --------------------------------------------- */

.impersonation-banner {
  background: #fef2f2;
  color: #991b1b;
  border-bottom: 1px solid #fecaca;
  padding: 8px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 13px;
}

.impersonation-banner__text { font-weight: 500; }

.impersonation-banner__stop {
  padding: 6px 12px;
  border-radius: 6px;
  border: 1px solid #fca5a5;
  background: #ffffff;
  color: #991b1b;
  font-weight: 600;
}

/* --- Shared text/confirm dialog (PBX.ask / PBX.confirmDelete / askText,
   native-prompt) -----------------------------------------------------------
   Was pbx.css's .pbx-modal, moved here so a page that loads neither pbx.css
   nor pbx-common.js (main.js, video.js, admin/organizations.html, the voice
   picker's rename) still gets a styled dialog rather than a bare unstyled
   box -- the 631 lesson: reuse the RULES, not the whole stylesheet, on a
   page that does not need pbx.css's tables/drawers/kebabs. Loaded on every
   page already (base.css, in <head>, ahead of any page's own extra_css), the
   same reason place-menu.js is loaded globally in base.html rather than
   per-page. Same shape as functions.css's own .fn-modal -- deliberately not
   unified with it here, to keep this a pure relocation with no other change. */
.pbx-modal {
  position: fixed; inset: 0; background: rgba(17, 24, 39, .45);
  display: flex; align-items: center; justify-content: center;
  z-index: 60; padding: 1rem;
}
/* Same [hidden]-vs-display gotcha CLAUDE.md documents repeatedly: .pbx-modal
   sets display:flex unconditionally, so the UA [hidden] rule loses at equal
   specificity and the modal would stay visibly rendered before JS opens it. */
.pbx-modal[hidden] { display: none !important; }
.pbx-modal__box {
  background: #fff; color: #1f2937; border-radius: 14px; padding: 1rem;
  width: min(460px, 100%); max-height: 90vh; overflow: auto;
}
/* Same modifier name as functions.css's .fn-modal__box--sm -- the base width
   here already matches "sm" (this page has no larger variant yet), so this
   is a no-op today kept for the day a bigger .pbx-modal shows up. */
.pbx-modal__box--sm { width: min(460px, 100%); }
.pbx-modal__head { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
.pbx-modal__title { flex: 1; font-weight: 600; }
.pbx-modal__consequence { margin: .25rem 0 .35rem; color: #1f2937; font-size: .95rem; line-height: 1.5; }
.pbx-modal__foot { display: flex; justify-content: flex-end; gap: .5rem; margin-top: .75rem; }
/* native-prompt: the text field askText/PBX.ask's input mode builds had NO rule
   anywhere (it was classed .pbx-form__input, a BEM name that only resolves
   under an ANCESTOR .pbx-form -- the dialog is not one), so every existing
   caller (pbx-config.js's site rename, pbx-screen-pop.js's variable name) has
   rendered a bare unstyled browser input since the day opts.input shipped.
   Fixed at the same time as the move, under the dialog's own name instead. */
.pbx-modal__input {
  display: block; width: 100%; box-sizing: border-box; margin: .35rem 0 0;
  padding: .5rem .6rem; border: 1px solid #d1d5db; border-radius: 8px;
  font: inherit; color: #1f2937;
}
.pbx-modal__input:focus { outline: 2px solid #6366f1; outline-offset: 1px; }

/* --- Toast ------------------------------------------------------------ */

.toast-stack {
  position: fixed;
  top: 80px;
  right: 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* PPBX-821: above every drawer (100), dialog (.dlg 1100), full-screen player and the agent-assist / desk layers --
     a toast after a save inside one of them must be seen. The SAME value as the web-phone dock (vwp.css), which stays
     the top layer on purpose (a call's controls are never covered): equal z-index, and the dock comes later in the
     page, so the dock wins the tie and the toast wins against everything else. */
  z-index: 2147483000;
  pointer-events: none;
}
/* A phone: the top-right corner holds the page header's primary button and a drawer's close X, so the stack moves to
   the bottom, centred, clear of a drawer's sticky footer (its Save) and of the player bar when one is showing. */
@media (max-width: 600px) {
  .toast-stack { top: auto; right: 16px; left: 16px; bottom: calc(84px + var(--player-height, 0px)); align-items: center; }
  .toast { min-width: 0; max-width: 100%; }
}

.toast {
  min-width: 260px;
  max-width: 380px;
  padding: 12px 16px;
  border-radius: 10px;
  background: #111827;
  color: #f9fafb;
  font-size: 14px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
  pointer-events: auto;
  animation: toast-in 120ms ease-out;
}

.toast--error { background: #991b1b; }
.toast--warn { background: #b45309; }
.toast--success { background: #15803d; }

@keyframes toast-in {
  from { transform: translateY(-6px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

/* --- Generic controls ------------------------------------------------- */

/* `hidden` must win. A class that sets `display` silently defeats the HTML
   attribute -- a button given hidden=true stayed on screen, which is the kind
   of bug that reads as "the script did not run". */
.btn[hidden] { display: none; }
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 18px;
  border-radius: 8px;
  border: 1px solid transparent;
  font-weight: 600;
  font-size: 14px;
  background: #eff6ff;
  color: #1e3a8a;
  transition: background 120ms ease;
}

.btn:hover { background: #e0e7ff; }
/* Every variant, not only the three that dim their own background. A ghost
   button had no disabled rule at all, so a deliberately blocked action looked
   exactly like an available one and pressing it simply did nothing. */
.btn:disabled { opacity: 0.55; cursor: not-allowed; }
/* window.busyButton (csrf.js): the spinner a running action shows on its own control. currentColor,
   so it reads on a primary, a ghost or a kebab button alike. PPBX-953. */
.btn-spin { display: inline-block; width: 12px; height: 12px; flex: none; border: 2px solid currentColor;
            border-top-color: transparent; border-radius: 50%; animation: btn-spin .8s linear infinite; }
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn-spin { animation: none; border-top-color: currentColor; opacity: .6; } }
[aria-busy="true"]:disabled { cursor: progress; }
/* Scoped to ghost: a blanket rule would strip the dimmed background the
   primary/secondary/accent variants set for their own disabled state. */
.btn--ghost:disabled:hover { background: transparent; }
.btn--primary { background: #1e40af; color: #ffffff; }
.btn--primary:hover { background: #1e3a8a; }
.btn--primary:disabled { background: #93c5fd; cursor: not-allowed; }
.btn--secondary { background: #475569; color: #ffffff; }
.btn--secondary:hover { background: #334155; }
.btn--secondary:disabled { background: #94a3b8; cursor: not-allowed; }
/* Same orange as the app's other secondary brand CTA (agents.css'
   .btn--agent-cta / the logo's wave-mark gradient) — reused here rather
   than invented, so "Save" reads as on-brand, not a random third hue. */
.btn--accent { background: #f97316; color: #ffffff; }
.btn--accent:hover { background: #ea580c; }
.btn--accent:disabled { background: #fdba74; cursor: not-allowed; }
.btn--ghost { background: transparent; color: #374151; border-color: #e5e7eb; }
.btn--ghost:hover { background: #f3f4f6; }
.btn--danger { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
.btn--danger:hover { background: #fecaca; }
.btn--sm { padding: 6px 10px; font-size: 13px; }
.btn--icon { padding: 6px; }

/* A square icon-only action button: fixed 30x30 box, own border, indigo
   hover -- distinct from `.btn--icon` above, which only trims the padding
   of a `.btn--ghost`/`.btn--primary`/etc button down to icon size and keeps
   that variant's own colour. Moved here from ivr-designer.css (2026-09-05):
   it is the IVR designer's own convention for a header icon button (rename,
   undo/redo, the ⋯ overflow trigger), and belongs in the shared sheet on the
   day another page's header first needs the same treatment, rather than
   living inside one page's stylesheet until then. */
.ivr-iconbtn {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 8px;
  border: 1px solid #e5e7eb; background: #fff; color: #4338ca;
  cursor: pointer; font-size: 14px; line-height: 1;
}
.ivr-iconbtn:hover:not(:disabled) { background: #eef2ff; border-color: #c7d2fe; }
.ivr-iconbtn:disabled { color: #cbd5e1; cursor: not-allowed; }
/* [hidden] loses to the flex/inline-flex above at equal specificity -- see
   CLAUDE.md's `el.hidden` note. */
.ivr-iconbtn[hidden] { display: none; }

input[type="text"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="password"],
input[type="url"],
input[type="tel"],
textarea,
select {
  width: 100%;
  padding: 9px 12px;
  border-radius: 8px;
  border: 1px solid #d1d5db;
  background: #ffffff;
  color: #111827;
}

/* PPBX-467: Edge draws its OWN show/hide eye inside every password input, so a
   field that also draws its own (password-reveal.js's `[data-reveal]`, or the
   Agent Desk PIN's bespoke `#desk-pin-eye` button) shows two side by side.
   Hidden ONLY on those two -- every other password field in the app (Resend/
   SMTP keys, trunk/SIP secrets, Zalo tokens, the cluster DB password...) has
   NO app-drawn toggle, and Edge's native one is the only way a user there can
   check what they typed; hiding it globally would be a regression, not a fix.
   Read from the code (every `type="password"` input was enumerated, and which
   ones the app already gives a toggle to), not confirmed in Edge -- nobody
   writing this had it open. `-ms-reveal` is Edge/IE-only; no other engine
   defines it, so this is a no-op everywhere else. */
input[data-reveal]::-ms-reveal,
#desk-pin::-ms-reveal {
  display: none;
}

/* A dropdown draws its own chevron inside its right padding, so the 12px
   that suits a text input leaves the arrow crowded against the border --
   and each component stylesheet that restyles form controls made it worse
   (.ab-input 10px, .pbx-form select 11px). Reported directly, page-wide.
   Fixed HERE rather than per component so one rule covers every select in
   the app, including ones nobody has looked at.

   `:not([multiple]):not([size])` is not a specificity trick for its own
   sake, though it is also what lets this outrank `.pbx-form select` (0,1,1)
   and `.ab-input` (0,1,0) without `!important`: a multiple/sized select is
   a LIST BOX, which renders no chevron at all, so it genuinely must not get
   the extra room. */
select:not([multiple]):not([size]) { padding-right: 30px; }

/* PPBX-442: padding moves the TEXT away from the border, not the native chevron, which
   Chrome draws a few pixels from the edge whatever the padding is -- that is the
   "too close to the right border" every page reported. The Agent Assist picker
   (searchable-select.js) draws its own chevron 13px in, so every plain select now does
   the same. The two components that already draw their own (.subtitle-controls__select,
   .vp-scope-mobile) are excluded inside :where() so the exclusion costs no specificity. The
   longhands are !important because ~45 component rules set the `background` SHORTHAND on a
   select (`.sch-chip .sch-time-select select` ties this rule's specificity and loads later), and a
   shorthand resets image, repeat, position and size: appearance:none would then have removed the
   native chevron and this one would be gone too.
   Forced-colors mode drops background images, so it gets the native control back. */
select:not([multiple]):not([size]):where(:not(.subtitle-controls__select):not(.vp-scope-mobile)) {
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%236b7280' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>") !important;
  background-repeat: no-repeat !important;
  background-position: right 13px center !important;
  background-size: 12px 12px !important;
}
@media (forced-colors: active) {
  select:not([multiple]):not([size]) { -webkit-appearance: auto; appearance: auto; background-image: none !important; }
}

input:focus,
textarea:focus,
select:focus {
  outline: 2px solid #60a5fa;
  outline-offset: 1px;
  border-color: #60a5fa;
}

/* A field marked invalid must LOOK invalid. `aria-invalid` was already being
   set by pbx-agent-desk.js's fieldError() and by the agents drawer, and no
   stylesheet in the app had a rule for it -- so a rejected field rendered
   byte-identically to an accepted one and only a screen reader knew. That is
   the "a state class only JavaScript sets is a class nobody checks exists"
   defect CLAUDE.md records, arriving through an ARIA attribute instead of a
   class.

   Not colour alone: the border WEIGHT changes too, because a red border on a
   grey one is the commonest thing a colour-blind operator cannot see, and this
   is the control telling them which box to go back to. */
input[aria-invalid="true"],
textarea[aria-invalid="true"],
select[aria-invalid="true"] {
  /* `!important` deliberately, and this is the case it exists for: a
     VALIDATION STATE has to beat COMPONENT styling wherever the control
     happens to live, and component rules are more specific by nature.
     Measured in the browser rather than assumed -- without it,
     `.pbx-form input[type="text"]` in pbx.css (0,2,1) beat this rule (0,1,1)
     and base.css loads first, so a field marked invalid inside any PBX drawer
     rendered with the ordinary grey border and only the box-shadow showed.
     That is most of the way back to the bug: the one place this marker is used
     most is a drawer. */
  border-color: #dc2626 !important;
  box-shadow: 0 0 0 1px #dc2626;
}
input[aria-invalid="true"]:focus,
textarea[aria-invalid="true"]:focus,
select[aria-invalid="true"]:focus {
  outline: 2px solid #dc2626;
  outline-offset: 1px;
  border-color: #dc2626;
}

/* Same gap as .btn:disabled above, for form controls instead of buttons: no
   rule at all meant a disabled select/input kept its plain white background,
   full-opacity text and a pointer cursor -- indistinguishable from a live
   one. Reported directly against the extension drawer's forwarding tab
   (strategy/confirm-digit selects, the per-row confirm checkbox), 2026-08-27.
   Muted colours, not opacity: the earlier `.disabled { opacity: .7 }` on a
   WHITE control barely changes anything a user notices, and the value shown
   in a disabled field is often the information the operator needs, so it
   must stay legible rather than faded. */
input:disabled,
textarea:disabled,
select:disabled {
  background: #f3f4f6;
  color: #6b7280;
  border-color: #e5e7eb;
  cursor: not-allowed;
}

/* Native checkboxes/radios aren't covered by the background/color rule above
   (they have no visible fill of their own), and this app draws them with no
   custom appearance, so the only signal available is dimming the control and
   its label text together. `:has()` is already used elsewhere in this
   codebase (agent-conversations.css, admin-showcase.css). */
input[type="checkbox"]:disabled,
input[type="radio"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
label:has(input[type="checkbox"]:disabled),
label:has(input[type="radio"]:disabled) {
  color: #9ca3af;
  cursor: not-allowed;
}

.form-row { margin-bottom: 12px; }
.form-row label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: #4b5563;
  margin-bottom: 4px;
}
/* A CHECKBOX is not a text field, and the rule above is wrong for one.
   `display: block` on the label is right when the control sits UNDER its
   caption -- an input, a select -- and puts a checkbox on the line BELOW its
   own label, floating with nothing beside it. Reported on /admin/settings'
   Contact Center tab, where every checkbox on the page rendered that way.

   Fixed here rather than in the 30 call sites, because it is the shared rule
   that is wrong: any `.form-row` anywhere that holds a checkbox has always had
   this, and a page written next week would have it too.

   `order` and not `flex-direction: row-reverse`: these rows are not always two
   children. Several carry a `.form-row__hint` as well, and reversing would
   fling the hint to the front of the line. Ordering only the box leaves every
   other child in document order. The hint then takes a full row of its own so
   it stays UNDER the control it explains, which is the whole point of it.

   DOM order is untouched, so the label still precedes its input for anything
   reading the markup; they are bound by `for`/`id`, which is what decides the
   accessible name -- not adjacency. */
.form-row:has(> input[type="checkbox"]) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.form-row:has(> input[type="checkbox"]) > input[type="checkbox"] {
  order: -1;
  flex: 0 0 auto;
  margin: 0;
}
.form-row:has(> input[type="checkbox"]) > label {
  display: inline;
  margin-bottom: 0;
}
.form-row:has(> input[type="checkbox"]) > .form-row__hint {
  flex-basis: 100%;
  margin-top: 0;
}

/* A note UNDER a field, for the consequence that does not fit in a label and is
   too important to hide in a tooltip -- a tooltip is mouse-only and never seen
   on the first attempt, which is when it is needed. */
.form-row__hint {
  margin: 5px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: #6b7280;
}

/* The `?` variant (form_field's help_toggle). The label and the button sit on
   one line so the button reads as belonging to that field rather than to the
   control below it; `gap` rather than a margin so an empty actions slot
   collapses instead of leaving a hole. */
.form-row__labelline {
  display: flex;
  align-items: center;
  gap: 6px;
}
.form-row__labelline .ab-label { margin-bottom: 0; }
/* Same 20px circle as the agent builder's .ab-help-btn, defined here because
   base.css is where form_field's own markup is styled and this must not
   depend on a page stylesheet that most callers never load. */
.form-row__labelline .ab-help-btn,
.ab-help-btn {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px solid #d1d5db;
  background: #f9fafb;
  color: #6b7280;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background .12s, border-color .12s, color .12s;
}
.ab-help-btn:hover,
.ab-help-btn[aria-expanded="true"] {
  background: #eff6ff;
  border-color: #93c5fd;
  color: #1e40af;
}

/* Two-up form-row used for paired optional selects (gender + region
   in the voice creation modal). Stacks on narrow viewports. */
.form-row--inline {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
.form-row--inline .form-field {
  flex: 1 1 140px;
  min-width: 0;
}
.form-row--inline label {
  font-size: 12px;
}
.form-row--inline select {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  background: #fff;
  font-size: 14px;
}

/* --- Simple tables ---------------------------------------------------- */

/* The admin pages' table, in the app's one data-table look (PPBX-820): the same values as tables.css
   .history-table / -wrap -- 11px uppercase headers on the indigo band, 13px cells, the glass card. Kept as its own
   class only because ~8 admin pages and their JS name it; keep the two in step. */
table.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  background: rgba(255, 255, 255, 0.75);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.85);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 2px 12px rgba(49, 46, 129, 0.06);
}

table.data-table th,
table.data-table td {
  padding: 10px 12px;
  text-align: left;
  border-bottom: 1px solid #f3f4f6;
  font-size: 13px;
  color: #1f2937;
}

table.data-table th {
  background: rgba(240, 242, 255, 0.90);
  border-bottom: 1px solid rgba(99, 102, 241, 0.12);
  padding: 8px 12px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #6b7280;
  white-space: nowrap;
}

table.data-table tbody tr:hover { background: #f9fafb; }
table.data-table tr:last-child td { border-bottom: none; }

.empty-state {
  padding: 40px 20px;
  text-align: center;
  color: #6b7280;
  font-size: 14px;
}
.empty-state[hidden] { display: none; }
/* A search that matched nothing: the page's own "nothing yet" lines give way to the one shared sentence
   (static/js/list-empty.js, PPBX-605). */
.empty-state--nomatch > :not(.empty-state__nomatch) { display: none; }
.empty-state:not(.empty-state--nomatch) > .empty-state__nomatch { display: none; }
.empty-state__nomatch { margin: 0; }

/* --- Responsive ------------------------------------------------------- */

/* .topbar only survives on signed-out pages (login, terms, password reset)
   where it is a brand strip and nothing else. Authenticated pages have no
   header at all — see sidebar.css. */
@media (max-width: 720px) {
  .topbar { padding: 10px 12px; gap: 10px; }
  .topbar__brand-img { height: 48px; }
}

@media (max-width: 480px) {
  .topbar { padding: 8px 10px; gap: 8px; }
  .topbar__brand-img { height: 40px; }
}

/* ---------------------------------------------------------------------------
 * Global full-width page layout.
 *
 * The app shell (sidebar.css) already gives the content column the full
 * remaining width; whether a PAGE looks full-width or boxed is decided by its
 * own content wrapper. Pages that want the edge-to-edge layout (the TTS
 * workarea, Voice Agents, video pages, ...) wrap their content in
 * `<div class="page-full">` instead of a bespoke `max-width: …; margin: auto`
 * box. This is the single source of truth for full-width gutters + sticky
 * player clearance, so any NEW page gets a consistent layout by reusing it.
 * (A page that genuinely needs a narrow reading column can still nest its own
 * constrained inner wrapper.)
 * ------------------------------------------------------------------------- */
.page-full {
  width: 100%;
  margin: 0;
  /* Match the TTS workarea gutters. padding-bottom clears the sticky audio
   * player when present (player.js sets --player-height; 0px when hidden). */
  padding: 16px 24px;
  padding-bottom: calc(24px + var(--player-height, 0px));
}
@media (max-width: 720px) {
  .page-full {
    padding: 12px 14px;
    padding-bottom: calc(16px + var(--player-height, 0px));
  }
}
@media (max-width: 480px) {
  .page-full { padding: 10px 10px; }
}

/* ---------------------------------------------------------------------------
 * Shared compact page header (_page_header.html).
 *
 * One line: title left, actions right, a rule underneath. No eyebrow (the
 * sidebar group is the breadcrumb) and no standing paragraph (it lives behind
 * the title, which is the disclosure control). ~48px of chrome instead of the
 * ~130px the old .page-head + eyebrow + description stack cost on every page.
 * --------------------------------------------------------------------------- */
.pg-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 2px 0 10px;
  margin: 0 0 14px;
  border-bottom: 1px solid rgba(99, 102, 241, 0.16);
}
/* Detail-page back arrow, left of the title. Same size and treatment as the
   agent builder's `.ab-back`, so the two detail-page shells match. */
.pg-head__back {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  border: 1px solid rgba(99, 102, 241, 0.20);
  background: rgba(238, 240, 255, 0.60);
  color: #4f46e5;
  font-size: 16px;
  text-decoration: none;
  transition: background 0.15s;
}
.pg-head__back:hover { background: rgba(238, 240, 255, 0.90); }
.pg-head__title {
  margin: 0;
  /* Absorbs the free space so `justify-content: space-between` still pins the
     actions right once a back arrow makes this a THREE-child flex row --
     without it the title would centre itself between arrow and buttons. With
     the usual two children the result is unchanged. */
  margin-right: auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  /* Match the control height so a title-only header is exactly as tall as one
     with buttons. Without this the bar was 38px on pages with no actions and
     45px on pages with them, and the content below started at a different
     place on every other page. */
  min-height: 32px;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.014em;
  line-height: 1.25;
  /* Deep indigo rather than near-black: it belongs to the same family as the
     rail, the active nav pill and the primary buttons, so the header reads as
     part of the app's chrome instead of as body copy set large. */
  color: #312e81;
}
/* The title is a button when there is help behind it. Stripped of all button
   chrome so it reads as a heading; the `?` is the only affordance. */
.pg-head__titlebtn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
}
.pg-head__titlebtn:hover { color: #4f46e5; }
.pg-head__titlebtn:hover .pg-head__help { color: #4f46e5; }
/* Section glyph, mirroring the sidebar entry for the same page (the map in
   _nav_icons.html is shared). Slightly larger than the text so it reads as a
   mark rather than a letter. */
.pg-head__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  margin-right: 4px;
  color: #4f46e5;
}
.pg-head__help {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  color: #b6bcc9;
  transition: color 120ms ease;
}
.pg-head__help svg { display: block; }
.pg-head__titlebtn[aria-expanded="true"] .pg-head__help { color: #4f46e5; }
/* The header owns how its controls LOOK, not the page that supplies them.
 *
 * Pages hand this slot whatever they already had — a `.btn` that forgot
 * `btn--sm` (39px tall), a `.btn--sm` (32px), a search `input` (38px, and
 * `width:100%` from the global input rule), the PBX Đơn giản/Nâng cao toggle,
 * a `.pbx-conn` status pill (27px). Left alone they line up at five different
 * heights and the header reads differently on every page, which is exactly
 * what a shared component is supposed to prevent. So normalise here: one
 * control height, one radius scale, and no page-specific sizing survives.
 * A page that adds a new control gets the house look for free. */
.pg-head__actions {
  --pg-ctl-h: 32px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  /* NOT flex-shrink: 0. That kept the row at its content width -- 561px on a
   * 390px phone for /functions -- so `flex-wrap` never got a chance to fire
   * and the buttons simply hung off the right edge. Shrinking is what lets it
   * wrap; `min-width: 0` is what lets it shrink below its content at all. */
  min-width: 0;
  justify-content: flex-end;
}
.pg-head__actions .btn,
.pg-head__actions .pbx-mode,
.pg-head__actions .pbx-conn,
.pg-head__actions input,
.pg-head__actions select {
  height: var(--pg-ctl-h);
  min-height: var(--pg-ctl-h);
  font-size: 13px;
  white-space: nowrap;
}
/* Geometry, for the same reason the block below takes over colour: a page
 * hands the header whatever field class it uses in its own forms, and those
 * are built for a form COLUMN. `.ab-input` is `width:100%` with 9px of
 * vertical padding, which in a header meant the control claimed the entire row
 * -- measured at 565px inside a 390px viewport on /functions, pushing the
 * buttons onto a second line on desktop and 185px off-screen on a phone -- and
 * its own text was clipped by the fixed 32px height.
 *
 * `width:auto` alone would collapse a search box to its placeholder, so each
 * kind gets a sane range instead. */
.pg-head__actions input,
.pg-head__actions select {
  width: auto;
  padding: 0 10px;
  max-width: min(240px, 45vw);
}
.pg-head__actions input[type="search"],
.pg-head__actions input[type="text"] { min-width: 150px; }
.pg-head__actions select { min-width: 130px; }
/* Two button styles in a page header, no more: ONE filled primary for the
 * page's main action, and one quiet outlined style for everything else.
 *
 * Pages pass whatever variant they happened to use — `.btn--secondary` is a
 * dark slate slab, `.btn--ghost` is borderless, plain `.btn` is pale blue —
 * so the same row rendered in three different weights depending on which page
 * you were on, and on /pbx/servers a slate button sat directly beside the
 * indigo one. The variant a page asks for is overridden here on purpose: the
 * header decides what its own controls look like. Pages keep their variant
 * classes so the same buttons still render correctly outside a header. */
.pg-head__actions .btn {
  padding: 0 12px;
  border-radius: 8px;
  font-weight: 600;
  background: #ffffff;
  color: #3f4757;
  border: 1px solid #dfe3ee;
}
.pg-head__actions .btn:hover {
  background: #f5f6ff;
  border-color: #c7cbe6;
  color: #312e81;
}
.pg-head__actions .btn--primary {
  background: #4f46e5;
  color: #ffffff;
  border-color: transparent;
}
.pg-head__actions .btn--primary:hover {
  background: #4338ca;
  color: #ffffff;
  border-color: transparent;
}
.pg-head__actions .btn--danger {
  background: #ffffff;
  color: #b91c1c;
  border-color: #f3c9c9;
}
.pg-head__actions .btn--danger:hover {
  background: #fef2f2;
  color: #991b1b;
  border-color: #fca5a5;
}
.pg-head__actions .btn:disabled,
.pg-head__actions .btn--primary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.pg-head__actions .pbx-mode {
  border-color: #dfe3ee;
  border-radius: 8px;
}
.pg-head__actions .pbx-mode__btn {
  height: 100%;
  padding: 0 12px;
  font-size: 13px;
  color: #3f4757;
}
.pg-head__actions .pbx-mode__btn + .pbx-mode__btn {
  border-color: #dfe3ee;
}
.pg-head__actions .pbx-mode__btn.is-active {
  background: #4f46e5;
  color: #ffffff;
}
/* A search box in the header is a control, not a form field: the global
   `input { width: 100% }` would otherwise blow the row apart. */
.pg-head__actions input:not([type="file"]) {
  width: 180px;
  padding: 0 10px;
  margin: 0;
  border-radius: 8px;
}
/* Status pills are not buttons — same height so the row is level, but they
   keep the pill radius so they don't read as something clickable. */
.pg-head__actions .pbx-conn {
  border-radius: 999px;
  padding: 0 12px;
}

.pg-help {
  margin: -6px 0 16px;
  padding: 12px 14px;
  background: #f5f6ff;
  border: 1px solid rgba(99, 102, 241, 0.35);
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.55;
  color: #3f4757;
  max-width: 96ch;
}
.pg-help[hidden] { display: none; }
.pg-help p { margin: 0 0 8px; }
.pg-help p:last-child { margin-bottom: 0; }
.pg-help code {
  background: #e8eaf6;
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 12px;
}

@media (max-width: 640px) {
  .pg-head { padding-bottom: 8px; margin-bottom: 12px; }
  .pg-head__title { font-size: 17px; }
  .pg-head__actions { gap: 8px; }
}

/* ---------------------------------------------------------------------------
 * Shared page-head — standard branded header used on ALL authenticated pages.
 * Eyebrow label on the left, gradient title below it; optional CTA / wave on
 * the right. Defined here (not main.css) so every page gets it for free.
 * --------------------------------------------------------------------------- */
.page-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  /* No padding — the containing .workarea / .page-full / .page-wrap handles gutters. */
}
.page-head::before {
  content: '';
  position: absolute;
  left: -8px;
  top: -18px;
  width: 340px;
  height: 110px;
  background: radial-gradient(ellipse at left,
    rgba(99, 102, 241, 0.16) 0%, rgba(99, 102, 241, 0) 70%);
  pointer-events: none;
  z-index: 0;
}
.page-head__lead,
.page-head__right { position: relative; z-index: 1; }
.page-head__lead {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.page-head__title {
  margin: 0;
  font-size: clamp(22px, 2.4vw, 30px);
  font-weight: 750;
  letter-spacing: -0.022em;
  line-height: 1.1;
}
.page-head__right {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-shrink: 0;
}
@media (max-width: 640px) {
  .page-head__right .wave-mark { display: none; }
  .page-head { padding: 12px 14px 0; }
}

/* ---------------------------------------------------------------------------
 * Glass card — shared morphism surface used by cards, panels, editor areas.
 * Apply alongside a border-radius class or set one inline.
 * --------------------------------------------------------------------------- */
.glass-card {
  background: rgba(255, 255, 255, 0.88);
  -webkit-backdrop-filter: blur(20px) saturate(200%);
  backdrop-filter: blur(20px) saturate(200%);
  border: 1px solid rgba(255, 255, 255, 0.92);
  border-radius: 16px;
  box-shadow:
    0 6px 32px rgba(49, 46, 129, 0.10),
    0 1px 4px rgba(0, 0, 0, 0.04),
    inset 0 1px 0 rgba(255, 255, 255, 1);
}

/* ---------------------------------------------------------------------------
 * Page section — consistent sub-section within a page (heading row + content).
 * --------------------------------------------------------------------------- */
.page-section {
  margin-top: 24px;
}
.page-section__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.page-section__title {
  font-size: 15px;
  font-weight: 700;
  color: #1f2937;
  letter-spacing: -0.01em;
}

/* ---------------------------------------------------------------------------
 * Narrow page wrapper — for pages that want a max-width reading column
 * (profile, settings, dictionaries) rather than full-bleed content.
 * --------------------------------------------------------------------------- */
.page-wrap {
  max-width: 820px;
  margin: 0 auto;
  padding-bottom: 56px;
}

/* ---- Function modal (function-modal.js, used by /functions and the IVR) --- */
.fnm-overlay {
  position: fixed; inset: 0; z-index: 300;
  background: rgba(15, 23, 42, .45);
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.fnm-overlay[hidden] { display: none; }
/* Nearly the whole window: what is inside is the function editor -- a
   parameter rail beside a code surface -- and at 680px the code column was
   narrower than the lines people write in it. */
.fnm-dialog {
  background: #fff; border-radius: 16px; width: 100%;
  max-width: min(1500px, 96vw); height: 92vh;
  display: flex; flex-direction: column;
  box-shadow: 0 24px 64px rgba(0, 0, 0, .18);
}
.fnm-dialog__head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 18px; border-bottom: 1px solid #eef2f7;
}
.fnm-dialog__head strong { font-size: 16px; color: #111827; }
.fnm-dialog__body { padding: 14px 18px; overflow-y: auto; flex: 1 1 auto; }
/* The frame gets the whole area and no padding of its own -- the framed page
   already has margins, and two sets of them read as a misaligned dialog. */
.fnm-dialog__body--frame {
  padding: 0; overflow: hidden; display: flex; min-height: 0;
}

/* The function editor when it is FRAMED by the function modal: its own page
   chrome would be a second copy of what is already on screen behind it. */
.fn-embed { padding: 0; }
body:has(.fn-embed) .sidebar,
body:has(.fn-embed) .topbar,
body:has(.fn-embed) .bg-fx { display: none; }
body:has(.fn-embed) .app-main,
body:has(.fn-embed) .page-full { margin: 0; padding: 10px; }

.fnm-frame { flex: 1 1 auto; width: 100%; border: 0; min-height: 0; }
/* The framed editor fills the dialog: its own page is a column layout that
   only reaches full height when everything above it does too. */
html:has(.fn-embed), body:has(.fn-embed),
body:has(.fn-embed) .app-main, body:has(.fn-embed) .page-full { height: 100%; }
.fnm-dialog__body--frame > p { padding: 14px 18px; }

/* ---------------------------------------------------------------------------
   File inputs — global.

   The browser default is a bare grey "Choose file" chip that matches nothing
   else on the page: different height, different radius, different font from
   every button beside it. There are 27 of these across the app and until now
   each page either left the default or restyled it privately, so the same
   control looked different on /knowledge, /admin/bgm and the campaign builder.

   Styled as a `.btn--ghost` (the neutral button already used for secondary
   actions) sitting inside an `.ab-input`-shaped box, so a file field reads as a
   sibling of the text field above it.

   NOT applied where a page deliberately hides the input to build a drop zone --
   `.vc-drop :where(input[type="file"])` (voice_modal.css) is `opacity: 0` and covers
   its parent, and a more specific selector still wins. Same for any future
   dropzone: hide it in that component's own stylesheet, not here.
   --------------------------------------------------------------------------- */
:where(input[type="file"]:not([hidden])) {
  display: block;
  width: 100%;
  max-width: 100%;
  padding: 7px 10px;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  background: #ffffff;
  font-size: 13.5px;
  color: #6b7280;          /* the "no file chosen" text, deliberately quiet */
  line-height: 1.4;
  cursor: pointer;
}
:where(input[type="file"]):hover { border-color: #9ca3af; }
:where(input[type="file"]):focus-visible {
  outline: none;
  border-color: #6366f1;
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
:where(input[type="file"]):disabled {
  background: #f9fafb;
  color: #9ca3af;
  cursor: not-allowed;
}
/* The button inside the control. ::file-selector-button is the standard;
   ::-webkit-file-upload-button is kept because Safari still needs it and the
   two cannot be combined in one rule -- an unknown pseudo-element invalidates
   the whole selector list, which would silently drop the styling everywhere. */
:where(input[type="file"])::file-selector-button {
  margin: 0 10px 0 0;
  padding: 6px 14px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  background: #f3f4f6;
  color: #374151;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
:where(input[type="file"])::file-selector-button:hover {
  background: #e5e7eb;
  border-color: #d1d5db;
}
:where(input[type="file"])::-webkit-file-upload-button {
  margin: 0 10px 0 0;
  padding: 6px 14px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  background: #f3f4f6;
  color: #374151;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
:where(input[type="file"]):disabled::file-selector-button {
  background: #f3f4f6;
  color: #9ca3af;
  cursor: not-allowed;
}

/* Report notice — "part of this failed" / "your range was narrowed".
   In base.css and not admin.css or pbx-reports.css because BOTH /admin/usage,
   /admin/agent-costs and /pbx/reports render it, and a class defined in one
   page's stylesheet is a class the other page silently drops. CLAUDE.md
   records `history-row--failed` being set by JS and defined in no stylesheet
   at all: every programmatic check passed while failed rows rendered
   identically to healthy ones.

   Light values only — this app is a light theme.

   The tint is NOT the only signal: a left bar carries it too, because colour
   alone is not information. */
.report-notice {
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 13px;
  line-height: 1.5;
  margin-bottom: 14px;
}
.report-notice--warn {
  color: #92400e;
  background: #fffbeb;
  border: 1px solid #fcd34d;
}
/* Ships in the SAME edit as the rules above, per CLAUDE.md: the UA rule
   [hidden]{display:none} loses to any class that sets a `display`, which has
   cost this project four separate bugs. Nothing above sets one today; this is
   what keeps `el.hidden = true` working if somebody adds one tomorrow. */
.report-notice[hidden] { display: none; }

/* English UI text is sentence case everywhere (PPBX-195, owner decision 2026-09-26).
   PPBX-95 once Title-Cased the chrome here with text-transform: capitalize; that
   rule was removed because it left titles, placeholders and filters in sentence
   case beside it. Do not bring it back: tests/test_en_sentence_case.py fails on it. */

/* ---- App dialog (the datastore creation dialog's look, shared) -------------
   First defined inline on /datastores; moved here so /knowledge's create,
   rename and delete dialogs use the SAME dialog rather than a lookalike. Used
   instead of window.prompt/confirm, whose buttons speak the BROWSER's
   language and cannot be styled or emphasise a consequence. */
.ds-modal {
  position: fixed; inset: 0; z-index: 300;
  background: rgba(15, 23, 42, .45);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.ds-modal[hidden] { display: none; }
.ds-modal__dialog {
  background: rgba(255, 255, 255, .92);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, .8);
  border-radius: 18px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, .18);
  width: 100%; max-width: 640px; max-height: 90vh; overflow-y: auto; padding: 24px;
}
.ds-modal__step[hidden] { display: none; }
.ds-modal__actions { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }
.ds-modal__title { margin: 0 0 14px; font-size: 18px; font-weight: 700; color: #1f2937; }
.ds-modal__body { margin: 0; color: #374151; line-height: 1.5; }
.ds-modal__actions--end { justify-content: flex-end; }

/* datalist-combo.js (PPBX-852): the app-drawn suggestions of an <input list> -- at the input's width, above every
   panel and drawer: placeMenu portals it to <body>, fixed, at z-index 110. */
.dlc-menu { margin: 0; padding: 4px 0; list-style: none; box-sizing: border-box;
  max-height: 240px; overflow-y: auto; background: #fff; border: 1px solid #e5e7eb; border-radius: 10px;
  box-shadow: 0 8px 24px rgba(15, 23, 42, .12); font-size: 14px; color: #1f2937; }
.dlc-menu[hidden] { display: none; }
.dlc-menu__item { padding: 7px 12px; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dlc-menu__item:hover, .dlc-menu__item.is-active { background: #eef2ff; color: #3730a3; }
/* PPBX-1023: an option's name beside its number, and the "No results" line (searchable-select's look). */
.dlc-menu__label { margin-left: 8px; color: #64748b; }
.dlc-menu__item.is-active .dlc-menu__label, .dlc-menu__item:hover .dlc-menu__label { color: inherit; }
.dlc-menu__empty { padding: 7px 12px; color: #94a3b8; cursor: default; }

/* The shared LLM picker's "Tuỳ chỉnh…" box (llm-catalog.js), right under its <select> on every page that uses it. */
.llm-custom { margin-top: 6px; }
.llm-custom__input { width: 100%; max-width: 100%; box-sizing: border-box; height: 37px; padding: 0 10px;
                     border: 1px solid #d1d5db; border-radius: 9px; font: inherit; background: #fff; }
.llm-custom__hint { margin: 4px 0 0; font-size: 12.5px; color: #6b7280; }

/* Required-field marker (moved from pbx.css, PPBX-1009): the _form_field macro puts it on any page, and pages that do
   not load pbx.css (datastores, dictionaries, SMS partners) showed no asterisk at all. A ::after so a screen reader
   does not read stray punctuation inside the label; the accessible signal is aria-required on the control. Red is
   paired with the * glyph, never the only cue. */
.ab-label--req::after {
  content: " *";
  color: #dc2626;
  font-weight: 700;
}

/* A locked person's desktop softphone still signed in (PPBX-1006 M1): ONE look on /admin/users, the user page and
   /org -- a short amber pill that does not wrap, and the how-to-fix as plain small text under it. */
.softphone-flag { display: block; margin-top: 4px; }
.softphone-flag__pill { display: inline-block; white-space: nowrap; padding: 2px 8px; border-radius: 999px;
  background: #fef9c3; color: #854d0e; font-size: 12px; font-weight: 500; line-height: 1.5; text-decoration: none; }
a.softphone-flag__pill:hover { text-decoration: underline; }
.softphone-flag__hint { display: block; margin-top: 2px; font-size: 12px; line-height: 1.4; color: #6b7280; }

/* PPBX-1086: this tab shows an identity the session no longer has (another tab switched). Pinned after "Stay here";
   one border, the context bar's warn colours. */
.vx-identity-banner { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 90;
  display: flex; align-items: center; gap: 12px; max-width: calc(100vw - 32px); padding: 10px 14px;
  background: #fffbeb; color: #92400e; border: 1px solid #fde68a; border-radius: 10px;
  box-shadow: 0 4px 16px rgba(15, 23, 42, .12); font-size: 14px; }
.vx-identity-banner[hidden] { display: none; }
