:root {
  --primary: #1F4FFF;
  --primary-soft: rgba(31, 79, 255, 0.10);
  --primary-border: rgba(31, 79, 255, 0.25);
  --primary-fg: #ffffff;
}

/* P20/R1 — minimal RTL flips for the chrome. Most layout is Tailwind utilities (pl-*, ml-*)
   which don't auto-flip; full bidi polish is a P21 carry-over. This block is enough to make
   Arabic text read right-aligned by default, and the locale-switcher select align right so its
   chevron isn't visually orphaned in the corner. */
[dir="rtl"] { text-align: right; }
[dir="rtl"] .lang-switcher select { text-align: right; }
html, body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { font-feature-settings: "cv11", "ss01"; }
.text-primary { color: var(--primary); }
.bg-primary { background-color: var(--primary); }
.bg-primary-soft { background-color: var(--primary-soft); }
.border-primary { border-color: var(--primary); }
.ring-primary { --tw-ring-color: var(--primary); }
.btn-primary {
  background-color: var(--primary);
  color: var(--primary-fg);
}
.btn-primary:hover { filter: brightness(0.95); }
.chip-primary {
  background-color: var(--primary-soft);
  color: var(--primary);
  border: 1px solid var(--primary-border);
}

/* Tenant logo in the top header (rendered when the request is in tenant scope and the tenant
   has uploaded a logo via /{slug}/admin/branding). Keeps the header bar height stable. */
.tenant-logo {
  max-height: 28px;
  max-width: 140px;
  object-fit: contain;
  vertical-align: middle;
}

.row-divider > *:not(:last-child) { border-bottom: 1px solid; }
.light .row-divider > *:not(:last-child) { border-color: rgb(241 245 249); }
.dark .row-divider > *:not(:last-child) { border-color: rgb(30 41 59); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: rgba(100,116,139,.25); border-radius: 8px; }
::-webkit-scrollbar-thumb:hover { background: rgba(100,116,139,.45); }

.internal-note {
  background-color: #FEF9C3;
  border: 1px solid #EAB308;
}
.dark .internal-note {
  background-color: rgba(234, 179, 8, 0.12);
  border-color: rgba(234, 179, 8, 0.55);
}

.biz-cell { transition: background-color .12s ease; }

.ticket-row:hover .row-preview { display: block; }
.row-preview { display: none; }

input:focus, textarea:focus, select:focus, button:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}

/* Visual marker for required form fields. Three layouts appear across templates:
   - <label for="x">Subject</label><input id="x" required>             (sibling)
   - <label>Subject<input required></label>                            (wrapped)
   - <label>Description</label><div class="toolbar"><textarea required>(label sibling wraps the field — common for rich-text toolbars)
   The HTML5 `required` attribute already exposes `aria-required` to assistive tech, so the
   pseudo-element only adds the visual "*"; assistive readers don't see it. */
label:has(> input[required])::after,
label:has(> textarea[required])::after,
label:has(> select[required])::after,
label:has(+ input[required])::after,
label:has(+ textarea[required])::after,
label:has(+ select[required])::after,
label:has(+ * input[required])::after,
label:has(+ * textarea[required])::after,
label:has(+ * select[required])::after {
  content: " *";
  color: #ef4444;
  margin-left: 2px;
}

.nav-scroll::-webkit-scrollbar { display: none; }

[data-lucide] { stroke-width: 1.75; }

.priority-dot {
  width: 8px; height: 8px; border-radius: 999px; display: inline-block;
}

.dropzone {
  border: 1.5px dashed;
  transition: all .15s ease;
}
.light .dropzone { border-color: rgb(203 213 225); background: rgb(248 250 252); }
.dark .dropzone { border-color: rgb(51 65 85); background: rgb(15 23 42); }

.seg button[aria-pressed="true"],
.seg a[aria-pressed="true"] {
  background: white;
  color: rgb(15 23 42);
  box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 0 0 1px rgba(15,23,42,.06);
}
.dark .seg button[aria-pressed="true"],
.dark .seg a[aria-pressed="true"] {
  background: rgb(30 41 59);
  color: rgb(241 245 249);
  box-shadow: 0 1px 2px rgba(0,0,0,.4), 0 0 0 1px rgba(255,255,255,.06);
}

/* Persona switcher + tenant chip (V2 Phase 3). The chip uses an inline background derived from
   the tenant's brand colour (set via style="background:..."); the .tenant-chip class only sets
   the layout + sizing + typography so the inline color doesn't collide with utility classes. */
.tenant-chip {
  display: inline-flex; align-items: center; gap: 4px;
  max-width: 140px;
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.4;
  border: 1px solid transparent;
  background: var(--primary-soft);
  color: var(--primary);
}
.tenant-chip .tenant-chip-dot {
  width: 6px; height: 6px; border-radius: 999px;
  background: currentColor;
  flex-shrink: 0;
  opacity: 0.85;
}
.tenant-chip .tenant-chip-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Smaller variant used inline (e.g. breadcrumb marker on detail pages). */
.tenant-chip-sm {
  font-size: 10px;
  padding: 0 5px;
  max-width: 110px;
}

/* Sidebar group heading (admin sidebar groups: TICKETS / PEOPLE / OPERATIONS / SETTINGS). */
.sidebar-group-heading {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-weight: 600;
  color: rgb(100 116 139);
  padding: 0 0.5rem;
  margin-bottom: 0.5rem;
}
.dark .sidebar-group-heading { color: rgb(148 163 184); }

/* V2 Phase 9 (E-3) — comment timeline rail. The `.timeline` element is the comments list (an
   <ol> for the requester portal, a <div> wrapping <ol> for agent/detail.jte which is off-limits
   to this phase). The vertical rail sits at left:15px which lines up with the centre of the
   existing per-entry icon-circle (rendered inline at left-2 w-6 h-6 → centre ≈ 15px). The rail
   colour uses var(--primary) at low opacity so the accent picker keeps theming this chrome. */
.timeline { position: relative; }
.timeline::before {
  content: '';
  position: absolute;
  left: 15px;
  top: 8px;
  bottom: 8px;
  width: 1px;
  background: var(--primary);
  opacity: .18;
}
/* Semantic marker on each comment <li>. The visible dot is the inline icon-in-circle <span>
   already rendered by ticket/detail.jte + partials/_comment.jte; this class is a future hook
   for theming the per-entry marker without re-templating both files. */
.timeline-item { position: relative; }

.logo-tile {
  background: linear-gradient(135deg, var(--primary) 0%, color-mix(in oklab, var(--primary) 60%, #0f172a) 100%);
  color: white;
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* CXRoot wordmark used in the app chrome (login / top header / auth / error pages).
   Same asset as the email header; shown at its natural brand colours (no recolour). */
.app-logo { height: 24px; width: auto; display: block; }

.nav-tabs { scrollbar-width: none; }

body { background-color: rgb(248 250 252); }
.dark body, html.dark { background-color: rgb(2 6 23); }
.dark { color-scheme: dark; }

.chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 500; letter-spacing: 0.01em;
  padding: 2px 8px; border-radius: 4px;
  border: 1px solid transparent;
}

.st-received  { background:#e0f2fe; color:#0369a1; border-color:#bae6fd; }
.st-triaged   { background:#e0e7ff; color:#3730a3; border-color:#c7d2fe; }
.st-progress  { background:#fef3c7; color:#92400e; border-color:#fde68a; }
.st-waiting   { background:#fef3c7; color:#92400e; border-color:#fde68a; }
.st-testing   { background:#ede9fe; color:#5b21b6; border-color:#ddd6fe; }
.st-resolved  { background:#d1fae5; color:#065f46; border-color:#a7f3d0; }
.dark .st-received  { background:rgba(14,165,233,.12); color:#7dd3fc; border-color:rgba(14,165,233,.35); }
.dark .st-triaged   { background:rgba(99,102,241,.14); color:#a5b4fc; border-color:rgba(99,102,241,.4); }
.dark .st-progress, .dark .st-waiting { background:rgba(245,158,11,.12); color:#fcd34d; border-color:rgba(245,158,11,.35); }
.dark .st-testing   { background:rgba(139,92,246,.14); color:#c4b5fd; border-color:rgba(139,92,246,.4); }
.dark .st-resolved  { background:rgba(16,185,129,.12); color:#6ee7b7; border-color:rgba(16,185,129,.35); }

/* Neutral grey chip used for non-status badges: archived tenants, unassigned, etc. */
.chip-neutral { background:#f1f5f9; color:#475569; border-color:#e2e8f0; }
.dark .chip-neutral { background:rgba(100,116,139,.18); color:#cbd5e1; border-color:rgba(100,116,139,.4); }

/* Origin badge: shown when a ticket was created from an inbound email (TicketOrigin.EMAIL).
   Neutral slate palette — same family as .chip-neutral — so it reads as metadata, not a status. */
.origin-email { background:#f1f5f9; color:#475569; border-color:#e2e8f0; }
.dark .origin-email { background:rgba(100,116,139,.18); color:#cbd5e1; border-color:rgba(100,116,139,.4); }

.sla-ok    { background:#d1fae5; color:#065f46; border:1px solid #a7f3d0; }
.sla-warn  { background:#fef3c7; color:#92400e; border:1px solid #fde68a; }
.sla-bad   { background:#fee2e2; color:#991b1b; border:1px solid #fecaca; }
.dark .sla-ok    { background:rgba(16,185,129,.12); color:#6ee7b7; border-color:rgba(16,185,129,.35); }
.dark .sla-warn  { background:rgba(245,158,11,.12); color:#fcd34d; border-color:rgba(245,158,11,.35); }
.dark .sla-bad   { background:rgba(239,68,68,.14); color:#fca5a5; border-color:rgba(239,68,68,.4); }

.pri-low      { background:#94a3b8; }
.pri-medium   { background:#0ea5e9; }
.pri-high     { background:#f59e0b; }
.pri-critical { background:#ef4444; }

/* Stretched-link: makes whole parent row clickable via an invisible overlay
 * pseudo-element. Parent must be `position: relative`. Interactive descendants
 * (checkboxes, nested buttons/forms) must sit at z-index >= 1 to stay clickable. */
.stretched-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
}
.stretched-link-above { position: relative; z-index: 1; }

.navtab[aria-current="page"] {
  color: var(--primary);
  border-color: var(--primary);
}

.thead {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
}

kbd {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  padding: 1px 5px;
  border-radius: 3px;
  border: 1px solid;
}
.light kbd { background: white; border-color: rgb(226 232 240); color: rgb(71 85 105); }
.dark kbd { background: rgb(15 23 42); border-color: rgb(51 65 85); color: rgb(148 163 184); }

.avatar {
  display:inline-flex; align-items:center; justify-content:center;
  border-radius: 999px; font-size: 10px; font-weight: 600;
  width: 22px; height: 22px;
}

/* HTMX loading indicators. Element with .htmx-indicator is hidden by default and
   fades in while htmx is processing. The .htmx-request marker is applied by htmx
   to the requesting element or to anything named in hx-indicator. */
.htmx-indicator {
  opacity: 0;
  transition: opacity 120ms ease-in;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
}
.htmx-request button[type="submit"],
.htmx-request.btn-primary {
  pointer-events: none;
}

.spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 999px;
  animation: spin 600ms linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Notification bell + slide-over + list rows. V2 Phase 9 (E-5): the badge is now a small chip
   in --primary-soft (was red #ef4444 in V1) — matches the C-26 slide-over palette and stays
   consistent with the rest of the accent-themed chrome. The badge text caps at "99+" — see
   layout/base.jte for the template-side overflow expression. */
.notif-badge {
  position: absolute;
  top: 0;
  right: 0;
  min-width: 16px;
  height: 16px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--primary-soft);
  color: var(--primary);
  border: 1px solid var(--primary-border);
  font-size: 9.5px;
  font-weight: 600;
  line-height: 14px;
  text-align: center;
  pointer-events: none;
  letter-spacing: -0.01em;
}
/* .notif-panel (V1 dropdown shell) was removed in V2 Phase 9 (C-26) — the bell now opens the
   slide-over (.slide-over) instead. The list-row rules below still drive the slide-over body. */
.notif-row {
  display: block;
  width: 100%;
  padding: 10px 12px;
  background: transparent;
  border: 0;
  border-left: 3px solid transparent;
  cursor: pointer;
  transition: background-color .12s ease;
}
.notif-row:hover { background: rgb(248 250 252); }
.dark .notif-row:hover { background: rgb(30 41 59); }
.notif-unread { border-left-color: var(--primary); }
.notif-read { opacity: 0.78; }
.notif-row.notif-dense { padding: 8px 12px; }

.audit-chip {
  display: inline-flex; align-items: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10.5px; font-weight: 500;
  padding: 1px 6px; border-radius: 3px;
  background: rgb(241 245 249); color: rgb(51 65 85);
  border: 1px solid rgb(226 232 240);
}
.dark .audit-chip {
  background: rgba(100, 116, 139, 0.18);
  color: rgb(203 213 225);
  border-color: rgba(100, 116, 139, 0.35);
}

/* Tag chips — used on the agent ticket detail header and inline on queue rows.
   Default treatment uses the primary accent var so the in-app color picker
   keeps working; per-tag custom colors override via inline style. */
.tag-chip {
  display: inline-flex; align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 9999px;
  font-size: 11.5px; font-weight: 500;
  line-height: 1.4;
  background: var(--primary-soft);
  color: var(--primary);
  border: 1px solid var(--primary-border);
  white-space: nowrap;
}
.tag-chip-sm {
  font-size: 10.5px;
  padding: 0 6px;
  line-height: 1.35;
}
.tag-chip-remove {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px;
  margin-left: 1px;
  border-radius: 9999px;
  background: transparent;
  border: 0;
  opacity: 0.6;
  cursor: pointer;
  color: inherit;
}
.tag-chip-remove:hover { opacity: 1; background: rgba(0,0,0,0.06); }
.dark .tag-chip-remove:hover { background: rgba(255,255,255,0.10); }

.macro-picker {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

/* P15 R4: agent-queue bulk action bar. Sticky inside the queue's scroll
   container so it remains visible while scrolling through long pages. */
.bulk-bar {
  position: sticky;
  top: 0;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid rgb(226 232 240);
  border-radius: 8px;
  padding: 8px 12px;
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  z-index: 5;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
  backdrop-filter: blur(6px);
}
.dark .bulk-bar {
  background: rgba(15, 23, 42, 0.96);
  border-color: rgb(30 41 59);
}
.bulk-bar.hidden { display: none; }

/* Global toast stack — used by the htmx:responseError / htmx:sendError handler in base.jte
   to surface failed background requests that would otherwise vanish silently. Stacks at the
   bottom-right (bottom-left in RTL) and auto-dismisses after 4s; clickable to dismiss early. */
.toast-stack {
  position: fixed;
  bottom: 16px;
  right: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 100;
  pointer-events: none;
  max-width: calc(100vw - 32px);
}
[dir="rtl"] .toast-stack { right: auto; left: 16px; }
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: rgb(15 23 42);
  color: #fff;
  font-size: 12.5px;
  line-height: 1.4;
  padding: 10px 12px;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  min-width: 220px;
  max-width: 360px;
  animation: toast-in 160ms ease-out;
}
.toast.toast-error { background: #b91c1c; }
.toast-close {
  background: transparent;
  border: 0;
  color: inherit;
  opacity: 0.7;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
}
.toast-close:hover { opacity: 1; }
@keyframes toast-in {
  from { transform: translateY(8px); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
}

/* V2 Phase 9 (C-26) — notifications slide-over panel. Replaces the V1 dropdown attached to the
   bell. The slide-over lives off-canvas (transform: translateX(100%)) and slides in when the
   `.open` class is toggled on the root element. A sibling `.slide-over-overlay` dims the page
   and captures off-panel clicks to dismiss.

   Why a slide-over instead of a dropdown:
   - Larger content surface (380px wide × 100vh) — fits grouped sections + filters.
   - Consistent with the V2 mockup pattern for global notifications (matches Settings drawer,
     mobile meta-panel, etc.). Bell badge + dropdown was the V1 stub the worksheet flagged.

   Accessibility note: the panel is keyboard-toggleable via the bell <button>; ESC handling is
   wired in base.jte's inline script so users can dismiss without reaching for the mouse. */
.slide-over {
  position: fixed;
  right: 0;
  top: 0;
  height: 100vh;
  width: 380px;
  max-width: 92vw;
  transform: translateX(100%);
  transition: transform 200ms ease-out;
  background: #fff;
  border-left: 1px solid rgb(226 232 240);
  box-shadow: -8px 0 24px rgba(15, 23, 42, 0.10);
  z-index: 60;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.slide-over.open {
  transform: translateX(0);
}
.dark .slide-over {
  background: rgb(15 23 42);
  border-left-color: rgb(51 65 85);
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.50);
}
[dir="rtl"] .slide-over {
  right: auto;
  left: 0;
  border-left: 0;
  border-right: 1px solid rgb(226 232 240);
  transform: translateX(-100%);
  box-shadow: 8px 0 24px rgba(15, 23, 42, 0.10);
}
[dir="rtl"] .slide-over.open { transform: translateX(0); }
[dir="rtl"] .dark .slide-over { border-right-color: rgb(51 65 85); }

.slide-over-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease-out;
  z-index: 55;
}
.slide-over-overlay.open {
  opacity: 1;
  pointer-events: auto;
}
.dark .slide-over-overlay { background: rgba(0, 0, 0, 0.55); }

.slide-over-header {
  flex: 0 0 auto;
  padding: 14px 16px;
  border-bottom: 1px solid rgb(226 232 240);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.dark .slide-over-header { border-bottom-color: rgb(30 41 59); }
.slide-over-body {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.slide-over-footer {
  flex: 0 0 auto;
  padding: 10px 16px;
  border-top: 1px solid rgb(226 232 240);
  text-align: center;
}
.dark .slide-over-footer { border-top-color: rgb(30 41 59); }

/* Notification date-bucket headings inside the slide-over. */
.notif-group-heading {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-weight: 600;
  color: rgb(100 116 139);
  padding: 10px 16px 6px;
  background: rgb(248 250 252);
}
.dark .notif-group-heading {
  color: rgb(148 163 184);
  background: rgb(2 6 23);
}
@media (prefers-reduced-motion: reduce) {
  .slide-over, .slide-over-overlay { transition: none; }
}

/* Custom confirm modal — replaces native window.confirm. Markup lives in
   layout/_confirm_modal.jte; JS driver in /js/confirm-modal.js. */
.cx-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.cx-modal.hidden { display: none; }
.cx-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(15, 23, 42, 0.55);
  backdrop-filter: blur(2px);
}
.cx-modal-panel {
  position: relative;
  background: #ffffff;
  border-radius: 10px;
  width: 100%;
  max-width: 420px;
  box-shadow: 0 24px 60px -12px rgba(15, 23, 42, 0.35), 0 8px 24px -6px rgba(15, 23, 42, 0.18);
  border: 1px solid rgb(226 232 240);
  padding: 20px 22px 16px;
}
.dark .cx-modal-panel {
  background: rgb(15 23 42);
  border-color: rgb(30 41 59);
  color: rgb(241 245 249);
}
.cx-modal-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.cx-modal-head-text { min-width: 0; }
.cx-modal-icon-tile {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.cx-modal-icon-danger {
  background: rgb(255 228 230);
  color: rgb(190 18 60);
}
.dark .cx-modal-icon-danger {
  background: rgba(244 63 94 / 0.20);
  color: rgb(253 164 175);
}
.cx-modal-icon-default {
  background: var(--primary-soft);
  color: var(--primary);
}
.cx-modal-panel h3 {
  font-size: 15px;
  font-weight: 600;
  margin: 0 0 4px;
  letter-spacing: -0.01em;
}
.cx-modal-panel p {
  font-size: 13px;
  color: rgb(71 85 105);
  margin: 0 0 18px;
  line-height: 1.55;
}
.dark .cx-modal-panel p { color: rgb(148 163 184); }
.cx-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.cx-modal-actions button {
  height: 32px;
  padding: 0 14px;
  font-size: 12.5px;
  font-weight: 500;
  border-radius: 6px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: filter 0.12s ease, background-color 0.12s ease, border-color 0.12s ease;
}
.cx-modal-actions [data-cx-confirm-cancel-btn] {
  background: transparent;
  color: rgb(51 65 85);
  border-color: rgb(203 213 225);
}
.cx-modal-actions [data-cx-confirm-cancel-btn]:hover {
  background: rgb(241 245 249);
}
.dark .cx-modal-actions [data-cx-confirm-cancel-btn] {
  color: rgb(203 213 225);
  border-color: rgb(51 65 85);
}
.dark .cx-modal-actions [data-cx-confirm-cancel-btn]:hover {
  background: rgb(30 41 59);
}
.cx-modal-actions .cx-modal-ok-default {
  background: var(--primary);
  color: var(--primary-fg);
}
.cx-modal-actions .cx-modal-ok-default:hover { filter: brightness(0.92); }
.cx-modal-actions .cx-modal-ok-danger {
  background: rgb(220 38 38);
  color: #ffffff;
}
.cx-modal-actions .cx-modal-ok-danger:hover { background: rgb(185 28 28); }
body.cx-modal-open { overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  .cx-modal-backdrop { backdrop-filter: none; }
}
