/* ═══════════════════════════════════════════════════════════════════════════
   SRIDIRA CMS · v3.3 — COMPONENT LAYER (increment C)
   Modal · Drawer · Tabs · Table · Skeleton · Empty & Error · KPI

   Loaded AFTER tokens.css and built entirely from its variables, so a theme
   change moves every component at once and nothing here hard-codes a colour.

   Two rules this file keeps to:

   1. NOTHING HERE OWNS DATA OR BEHAVIOUR THAT ALREADY EXISTS. The modal markup
      the portal already ships (.modal / .modal-card / .modal-close) keeps its
      class names; this file gives it structure and correct scrolling, it does
      not ask 30 call sites to change.
   2. MOTION IS OPTIONAL. Every transition is inside a
      prefers-reduced-motion: no-preference guard, so the reduced-motion path
      is the plain one rather than an override fighting a default.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── OVERLAY BACKDROP — shared by modal and drawer ─────────────────────────── */
.modal,
.drawer-backdrop {
  position: fixed; inset: 0;
  background: rgba(15, 23, 42, .55);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-4);
  overscroll-behavior: contain;
}
.modal { z-index: var(--z-modal); }
.drawer-backdrop { z-index: var(--z-drawer); align-items: stretch; padding: 0; }
.modal[hidden], .drawer-backdrop[hidden] { display: none; }

/* ── MODAL ─────────────────────────────────────────────────────────────────
   A column, not a block: the header and footer stay put and only the body
   scrolls. Long forms on a short phone were previously scrolling the whole
   card, which put the close button off-screen. */
.modal-card {
  position: relative;
  display: flex; flex-direction: column;
  width: 100%; max-width: 520px;
  max-height: min(86vh, 720px);
  max-height: min(86dvh, 720px);   /* dvh where supported: a phone's collapsing
                                      URL bar makes 86vh taller than the screen */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-xl);
  /* v3.3.1 — THE CARD SCROLLS UNLESS SOMETHING INSIDE IT DOES.
     This was `overflow: hidden`, which is right only for a card that has a
     .modal-body to scroll. No card in the portal had one, so every modal
     taller than the cap was laid out and then clipped with no way to reach
     the rest — a customer profile's visit timeline on a phone simply ended.
     Clipping is now opt-in (.has-body, below) and reachability is the
     default, so this cannot silently come back. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.modal-card.sm   { max-width: 400px; }
.modal-card.lg   { max-width: 800px; }
.modal-card.wide { max-width: 980px; }
.modal-card.full { max-width: min(1200px, 96vw); max-height: 92vh; }

/* The existing markup puts everything loose inside .modal-card. Padding it
   directly keeps that working; a card that opts into the header/body/footer
   structure below gets scroll containment as well. */
.modal-card { padding: var(--space-6); gap: var(--space-4); }
.modal-card > .modal-head,
.modal-card > .modal-body,
.modal-card > .modal-foot { margin: 0; }
/* A card that has a body keeps its head, footer and close button still and
   scrolls only the body. Deliberately THREE rules, not one selector list:
   a list containing :has() is dropped whole where :has() is unsupported, and
   the .has-body class is what components.js sets, so the behaviour must not
   depend on :has() at all. */
.modal-card.has-body            { padding: 0; gap: 0; overflow: hidden; }
.modal-card:has(> .modal-body)  { padding: 0; gap: 0; overflow: hidden; }
/* The loose children used to be flex items of the card and took its gap with
   them. Inside the wrapper they keep it, and they do not shrink: a column
   flex item that compresses is how a form ends up with unreadable rows. */
.modal-card.has-body > .modal-body {
  display: flex; flex-direction: column; gap: var(--space-4);
}
.modal-card.has-body > .modal-body > * { flex-shrink: 0; }

.modal-head {
  flex: 0 0 auto;
  padding: var(--space-5) var(--space-6);
  padding-right: var(--space-12);              /* room for the close button */
  border-bottom: 1px solid var(--border-subtle);
}
/* The third selector matters: components.js wraps a loose card's children in
   .modal-body, which moves the heading one level down and would otherwise
   hand it back to portal.css's 18px margin-bottom on top of the flex gap. */
.modal-head h3, .modal-card > h3, .modal-card > .modal-body > h3 {
  font-size: var(--text-xl); font-weight: var(--weight-semibold);
  color: var(--text-primary); letter-spacing: var(--tracking-tight); margin: 0;
}
.modal-head .modal-sub {
  display: block; margin-top: var(--space-1);
  font-size: var(--text-sm); color: var(--text-secondary);
}
.modal-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: var(--space-6);
}
.modal-foot {
  flex: 0 0 auto;
  display: flex; gap: var(--space-2); justify-content: flex-end; flex-wrap: wrap;
  padding: var(--space-4) var(--space-6);
  border-top: 1px solid var(--border-subtle);
  background: var(--surface-secondary);
}
.modal-foot .spacer { margin-right: auto; }

.modal-close {
  position: absolute; top: var(--space-4); right: var(--space-4); z-index: 1;
  width: 32px; height: 32px; display: grid; place-items: center;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); color: var(--text-secondary);
  font-size: var(--text-base); line-height: 1; cursor: pointer;
}
.modal-close:hover { border-color: var(--danger-border); color: var(--danger-text); background: var(--danger-bg); }
.modal-close:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* ── DRAWER — the same machinery, anchored to an edge ───────────────────────
   Used for filters and detail panes where a modal would fight the page. On a
   phone it becomes a bottom sheet, because a 320px-wide side panel is a modal
   wearing a different name. */
.drawer {
  position: relative; display: flex; flex-direction: column;
  margin-left: auto; height: 100%;
  width: min(460px, 92vw);
  background: var(--surface);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
}
.drawer.left  { margin-left: 0; margin-right: auto; border-left: none; border-right: 1px solid var(--border); }
.drawer.wide  { width: min(640px, 96vw); }
.drawer > .modal-head { padding-right: var(--space-12); }
.drawer > .modal-body { padding: var(--space-5) var(--space-6); }

@media (max-width: 640px) {
  .drawer-backdrop { align-items: flex-end; }
  .drawer, .drawer.left {
    width: 100%; height: auto; max-height: 88vh;
    margin: 0; border: none; border-top: 1px solid var(--border);
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  }
  /* The grab handle is the affordance that says "this came from the bottom". */
  .drawer::before {
    content: ''; position: absolute; top: var(--space-2); left: 50%;
    transform: translateX(-50%); width: 36px; height: 4px;
    border-radius: var(--radius-full); background: var(--border-strong); opacity: .5;
  }
  .drawer > .modal-head { padding-top: var(--space-5); }
}

/* ── TABS ──────────────────────────────────────────────────────────────────
   Scrollable on narrow screens rather than wrapping into two rows, which
   moves the content down and makes the selected tab hard to find. */
.tabs {
  display: flex; gap: var(--space-1);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-5);
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  flex: 0 0 auto;
  padding: var(--space-3) var(--space-4);
  border: none; background: none; cursor: pointer;
  font-family: inherit; font-size: var(--text-base); font-weight: var(--weight-medium);
  color: var(--text-secondary); white-space: nowrap;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--text-primary); }
.tab:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.tab[aria-selected="true"] { color: var(--primary); border-bottom-color: var(--primary); }
.tab .tab-count {
  display: inline-block; margin-left: var(--space-2);
  padding: 0 var(--space-2); border-radius: var(--radius-full);
  background: var(--surface-tertiary); color: var(--text-secondary);
  font-size: var(--text-2xs); font-variant-numeric: tabular-nums;
}
.tab[aria-selected="true"] .tab-count { background: var(--primary-tint); color: var(--primary); }
.tab-panel[hidden] { display: none; }

/* Segmented control — the same idea where the choice is a filter, not a page. */
.segmented {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--surface-tertiary); border-radius: var(--radius-md);
}
.segmented button {
  border: none; background: none; cursor: pointer; font-family: inherit;
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
  font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary);
  white-space: nowrap;
}
.segmented button[aria-pressed="true"] {
  background: var(--surface); color: var(--text-primary); box-shadow: var(--shadow-xs);
}
.segmented button:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* ── TABLE — sticky header, sortable columns, density ──────────────────────
   .data-table already exists and is styled in tokens.css. This adds only the
   parts that were missing, so existing tables are unaffected until they opt in. */
.table-scroll.sticky-head thead th {
  position: sticky; top: 0; z-index: var(--z-sticky);
  background: var(--surface-secondary);
  box-shadow: inset 0 -1px 0 var(--border);
}
.data-table th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.data-table th.sortable:hover { color: var(--text-primary); }
.data-table th.sortable:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.data-table th.sortable::after {
  content: '↕'; margin-left: var(--space-2); opacity: .35; font-size: .9em;
}
.data-table th[aria-sort="ascending"]::after  { content: '↑'; opacity: 1; color: var(--primary); }
.data-table th[aria-sort="descending"]::after { content: '↓'; opacity: 1; color: var(--primary); }
.data-table.dense tbody td { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.data-table.zebra tbody tr:nth-child(even) td { background: var(--surface-secondary); }
.data-table.zebra tbody tr:nth-child(even):hover td { background: var(--surface-tertiary); }

/* ── SKELETON ──────────────────────────────────────────────────────────────
   A shape, not a spinner: it says what is coming and stops the layout jumping
   when it arrives. The shimmer is decorative and disappears under
   reduced-motion, leaving a flat placeholder that still does the job. */
.skeleton {
  display: block; border-radius: var(--radius-sm);
  background: var(--surface-tertiary);
  position: relative; overflow: hidden;
}
.skeleton.text   { height: .8em; margin: .25em 0; }
.skeleton.title  { height: 1.4em; width: 40%; }
.skeleton.line   { height: .8em; width: 100%; }
.skeleton.line.short { width: 60%; }
.skeleton.circle { border-radius: var(--radius-full); }
.skeleton-row { display: flex; gap: var(--space-4); align-items: center; padding: var(--space-3) 0; }
.skeleton-row > .skeleton { flex: 1 1 auto; }
.skeleton-card { padding: var(--space-4); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); }

@media (prefers-reduced-motion: no-preference) {
  .skeleton::after {
    content: ''; position: absolute; inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .55), transparent);
    animation: cms-shimmer 1.4s ease-in-out infinite;
  }
  @keyframes cms-shimmer { 100% { transform: translateX(100%); } }
}
:root[data-theme="dark"] .skeleton::after,
:root:not([data-theme="light"]) .skeleton::after {
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .07), transparent);
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .skeleton::after {
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .55), transparent);
  }
}

/* ── EMPTY AND ERROR STATES ────────────────────────────────────────────────
   Deliberately one component with two tones. An empty list and a failed load
   look almost identical to a user until you tell them which it is, and the
   old code said "No records" for both. */
.state {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-3); padding: var(--space-10) var(--space-6);
  color: var(--text-secondary);
}
.state .state-icon {
  width: 44px; height: 44px; display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--surface-tertiary); color: var(--text-tertiary);
}
.state .state-title {
  font-size: var(--text-lg); font-weight: var(--weight-semibold); color: var(--text-primary);
}
.state .state-body { font-size: var(--text-base); max-width: 46ch; line-height: var(--leading-relaxed, 1.6); }
.state .state-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: center; margin-top: var(--space-2); }
.state.compact { padding: var(--space-6) var(--space-4); }
.state.compact .state-icon { width: 34px; height: 34px; }

.state.is-error .state-icon { background: var(--danger-bg); color: var(--danger-text); }
.state.is-error .state-title { color: var(--danger-text); }
.state.is-warning .state-icon { background: var(--warning-bg); color: var(--warning-text); }

/* The technical detail is available but not shouted — an executive does not
   need a stack trace, and an administrator should not have to ask for one. */
.state details { margin-top: var(--space-2); font-size: var(--text-sm); }
/* --text-tertiary measured 4.44:1 here — it is rated for white surfaces and
   this sits on --surface-secondary inside a state block. */
.state details summary { cursor: pointer; color: var(--text-secondary); }
.state details summary:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-xs); }
.state details pre {
  margin-top: var(--space-2); padding: var(--space-3); text-align: left;
  background: var(--surface-tertiary); border-radius: var(--radius-sm);
  font-size: var(--text-xs); white-space: pre-wrap; overflow-wrap: anywhere;
  max-width: 60ch; color: var(--text-secondary);
}
/* A state inside a table has to be a cell, so it needs its own reset. */
td > .state, .data-table .state { padding: var(--space-8) var(--space-4); }

/* ── KPI — trend and delta ─────────────────────────────────────────────────
   The number alone answers "how many"; the delta answers "is that good", which
   is the question people actually open a dashboard with. */
.kpi-delta {
  display: inline-flex; align-items: center; gap: var(--space-1);
  margin-top: var(--space-2);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
}
.kpi-delta.up   { color: var(--success-text); }
.kpi-delta.down { color: var(--danger-text); }
.kpi-delta.flat { color: var(--text-tertiary); }
/* Direction is never carried by colour alone — the arrow is the real signal. */
.kpi-delta.up::before   { content: '▲'; font-size: .8em; }
.kpi-delta.down::before { content: '▼'; font-size: .8em; }
.kpi-delta.flat::before { content: '–'; }
.kpi-delta .kpi-delta-note { color: var(--text-tertiary); font-weight: var(--weight-normal); }
/* The hero KPI sits on --brand-gradient (#2563EB → #4F46E5), NOT on --chrome.
   --on-chrome-muted is rated 8.7:1 against the near-black rail and measured
   2.51:1 here — the same class of mistake as the dark-mode sidebar: a token
   used against a surface it was never measured on. White is 5.17:1 against the
   lightest stop, and the note stays subordinate by weight rather than by
   colour, because dimming it any further drops it below 4.5:1. */
.kpi.hero .kpi-delta,
.kpi.hero .kpi-delta .kpi-delta-note { color: #FFFFFF; }
.kpi.hero .kpi-delta .kpi-delta-note { font-weight: var(--weight-normal); opacity: 1; }

/* A KPI that is still loading keeps its own height, so the row does not jump. */
.kpi .skeleton.kpi-skeleton { height: var(--text-3xl); width: 3.5ch; margin: 0; }

/* ── MOTION ────────────────────────────────────────────────────────────────
   Entrances only, and only when the viewer has not asked for stillness. */
@media (prefers-reduced-motion: no-preference) {
  .modal:not([hidden]),
  .drawer-backdrop:not([hidden]) { animation: cms-fade var(--duration-fast) var(--ease-out); }
  .modal:not([hidden]) .modal-card { animation: cms-pop var(--duration-base) var(--ease-out); }
  .drawer-backdrop:not([hidden]) .drawer { animation: cms-slide-in var(--duration-base) var(--ease-out); }
  .drawer-backdrop:not([hidden]) .drawer.left { animation: cms-slide-in-left var(--duration-base) var(--ease-out); }
  @keyframes cms-fade { from { opacity: 0; } }
  @keyframes cms-pop  { from { opacity: 0; transform: translateY(8px) scale(.985); } }
  @keyframes cms-slide-in      { from { transform: translateX(100%); } }
  @keyframes cms-slide-in-left { from { transform: translateX(-100%); } }
  @media (max-width: 640px) {
    .drawer-backdrop:not([hidden]) .drawer,
    .drawer-backdrop:not([hidden]) .drawer.left { animation: cms-slide-up var(--duration-base) var(--ease-out); }
    @keyframes cms-slide-up { from { transform: translateY(100%); } }
  }
}

/* ── SCROLL LOCK ───────────────────────────────────────────────────────────
   A class rather than an inline style, so nested overlays cannot clear each
   other's lock — see the reference counter in components.js. */
body.cms-scroll-locked { overflow: hidden; }

/* ═══════════════════════════════════════════════════════════════════════════
   v3.3 · increment D — VIEWS
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── KPI grid on a phone ───────────────────────────────────────────────────
   The admin dashboard carries twelve KPIs. One per row put the first chart
   roughly two full screens down on a 390px phone, which is where the sales
   team actually reads it. Two up halves that without shrinking the figure
   below comfortable reading size. */
@media (max-width: 720px) {
  .kpi-grid { grid-template-columns: repeat(auto-fill, minmax(min(148px, 100%), 1fr)); gap: var(--space-3); }
  .kpi { padding: var(--space-4); }
  .kpi-val { font-size: var(--text-2xl); }
  .kpi-label { font-size: var(--text-2xs); }
  .kpi-grid .kpi.hero { grid-column: 1 / -1; }   /* the headline keeps its width */
}

/* ── Tables become cards on a phone ────────────────────────────────────────
   A ten-column table inside a horizontally scrolling box is technically
   readable and practically useless on a phone: you scroll sideways, lose the
   name column, and cannot tell which row you are in. Below 640px each row
   becomes a card and every cell is prefixed with its column name, which
   CMS.ui.responsiveTable() copies out of the real <thead> at render time — so
   this works on every existing table without touching a single renderer. */
@media (max-width: 640px) {
  .data-table.as-cards { border-collapse: separate; border-spacing: 0; font-size: var(--text-base); }
  /* v4.6.2 - In card mode the rows are blocks and the cells are flex boxes, so
     table layout has nothing left to lay out - but the TABLE box itself was still
     display:table, and a table's used width is never smaller than its intrinsic
     (header-driven) minimum, so it grew past its container on phones (Verify
     Records: 388px table in a 281px panel at 375px). Make the card list a block
     that honours width:100%, and let the two-line customer cell wrap instead of
     pushing its ID past the card edge. */
  .data-table.as-cards { display: block; width: 100%; max-width: 100%; }
  .data-table.as-cards tbody { display: block; }
  .data-table.as-cards tbody td { min-width: 0; flex-wrap: wrap; }
  .data-table.as-cards tbody td > * { min-width: 0; overflow-wrap: anywhere; }
  .data-table.as-cards tbody td > .cell-strong { flex: 1 1 0; text-align: right; }
  .data-table.as-cards tbody td > .cell-sub { flex: 1 1 100%; text-align: right; margin-top: 0; }
  .data-table.as-cards tbody td.nowrap { white-space: normal; }
  .data-table.as-cards thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .data-table.as-cards tbody tr {
    display: block; margin-bottom: var(--space-3); padding: var(--space-3) var(--space-4);
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xs);
  }
  .data-table.as-cards tbody tr:hover td { background: transparent; }
  .data-table.as-cards tbody td {
    display: flex; gap: var(--space-3); align-items: baseline; justify-content: space-between;
    padding: var(--space-1) 0; border: none; text-align: right;
  }
  .data-table.as-cards tbody td::before {
    content: attr(data-label);
    flex: 0 0 40%; text-align: left;
    font-size: var(--text-xs); font-weight: var(--weight-medium);
    color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--tracking-wide, .02em);
  }
  /* A cell with no column name (checkboxes, the actions cell) spans the card. */
  .data-table.as-cards tbody td[data-label=""]::before,
  .data-table.as-cards tbody td:not([data-label])::before { content: none; }
  /* Split deliberately. A browser that does not understand :has() discards the
     ENTIRE selector list it appears in, so pairing it with a plain class in one
     rule would take the plain class down with it. */
  .data-table.as-cards tbody td.row-actions-cell { justify-content: flex-end; padding-top: var(--space-3); }
  .data-table.as-cards tbody td.row-actions-cell::before { content: none; }
  .data-table.as-cards tbody td:has(.row-actions) { justify-content: flex-end; padding-top: var(--space-3); }
  .data-table.as-cards tbody td:has(.row-actions)::before { content: none; }
  /* The first cell is the card's title, so it reads as a heading not a field. */
  .data-table.as-cards tbody td:first-child {
    font-weight: var(--weight-semibold); font-size: var(--text-md);
    color: var(--text-primary); text-align: left; padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border-subtle); margin-bottom: var(--space-2);
  }
  .data-table.as-cards tbody td:first-child::before { content: none; }
  /* A state row must not be dressed as a record. The .state-row class is set by
     CMS.ui.tableState(), so this works without :has(); the :has() rule below is
     a bonus for state markup injected by other code. */
  .data-table.as-cards tbody tr.state-row { padding: 0; border: none; box-shadow: none; background: none; }
  .data-table.as-cards tbody tr.state-row td { display: block; text-align: center; }
  .data-table.as-cards tbody tr.state-row td::before { content: none; }
  .data-table.as-cards tbody tr:has(.state) { padding: 0; border: none; box-shadow: none; background: none; }
  .data-table.as-cards tbody tr:has(.state) td { display: block; text-align: center; }
  .data-table.as-cards tbody tr:has(.state) td::before { content: none; }
  /* .has-cards is added by CMS.ui.responsiveTable(); without it a card list
     would sit inside a horizontally scrolling box, which is the exact thing
     card mode exists to escape — too important to leave to :has(). */
  .table-scroll.has-cards { overflow-x: visible; border: none; background: none; }
  .table-scroll:has(.data-table.as-cards) { overflow-x: visible; border: none; background: none; }
}

/* ── Timeline — call history, activity, transcripts ────────────────────────
   One rail, one dot per event. The rail is what makes a list of timestamps
   read as a sequence rather than as rows. */
.timeline { position: relative; margin: 0; padding: 0 0 0 var(--space-6); list-style: none; }
.timeline::before {
  content: ''; position: absolute; left: 7px; top: 6px; bottom: 6px;
  width: 2px; background: var(--border);
}
.timeline > li { position: relative; padding: 0 0 var(--space-5) 0; }
.timeline > li:last-child { padding-bottom: 0; }
.timeline > li::before {
  content: ''; position: absolute; left: calc(-1 * var(--space-6) + 2px); top: 5px;
  width: 12px; height: 12px; border-radius: var(--radius-full);
  background: var(--surface); border: 2px solid var(--border-strong);
}
.timeline > li.is-in::before      { border-color: var(--success); }
.timeline > li.is-out::before     { border-color: var(--primary); }
.timeline > li.is-missed::before  { border-color: var(--danger); }
.timeline .tl-head { display: flex; gap: var(--space-2); align-items: baseline; flex-wrap: wrap; }
.timeline .tl-title { font-weight: var(--weight-semibold); color: var(--text-primary); font-size: var(--text-base); }
.timeline .tl-time { font-size: var(--text-xs); color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
.timeline .tl-body { margin-top: var(--space-1); font-size: var(--text-sm); color: var(--text-secondary); }

/* ── Transcript ────────────────────────────────────────────────────────────
   Two speakers, distinguished by side and label — not by colour alone, so it
   still reads in forced-colors and for a colourblind user. */
.transcript { display: flex; flex-direction: column; gap: var(--space-3); }
.transcript .ts-turn { max-width: 82%; }
.transcript .ts-turn.agent { align-self: flex-start; }
.transcript .ts-turn.customer { align-self: flex-end; }
.transcript .ts-who {
  font-size: var(--text-2xs); font-weight: var(--weight-semibold); text-transform: uppercase;
  letter-spacing: var(--tracking-wide, .02em); color: var(--text-tertiary); margin-bottom: var(--space-1);
}
.transcript .ts-turn.customer .ts-who { text-align: right; }
.transcript .ts-text {
  padding: var(--space-3) var(--space-4); border-radius: var(--radius-lg);
  font-size: var(--text-base); line-height: var(--leading-relaxed, 1.6);
  background: var(--surface-secondary); color: var(--text-primary);
  border: 1px solid var(--border-subtle);
}
.transcript .ts-turn.customer .ts-text { background: var(--primary-tint); border-color: var(--primary-light); }
.transcript .ts-time { font-size: var(--text-2xs); color: var(--text-tertiary); margin-top: 2px; font-variant-numeric: tabular-nums; }
.transcript .ts-turn.customer .ts-time { text-align: right; }

/* ── Live feed ─────────────────────────────────────────────────────────────
   A "new since you looked" marker, because a feed that silently reorders is
   indistinguishable from one that is broken. */
.feed-new {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-3); border-radius: var(--radius-full);
  background: var(--primary-tint); color: var(--primary);
  border: 1px solid var(--primary-light);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
}
.feed-new .dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--primary); }
@media (prefers-reduced-motion: no-preference) {
  .feed-new .dot { animation: cms-pulse 2s ease-in-out infinite; }
  @keyframes cms-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
}

/* ── FOCUS RINGS MUST BE INSTANT ───────────────────────────────────────────
   The legacy stylesheet uses `transition: all` on .nav-item, .btn-ghost and
   others. `all` includes outline-width and outline-color, so the keyboard focus
   ring FADED IN over ~300 ms: measured 0px at 0 ms, 1px at 100 ms, full 2px at
   300 ms. It is technically present the whole time and axe cannot see the
   problem at all — but somebody tabbing at speed through nineteen nav items
   never sees a fully drawn ring, which is the entire point of having one.

   Zero specificity via :where(*), so this only ever loses to something more
   specific, and it applies to the focused element alone. */
:where(*):focus-visible {
  transition-property: background-color, color, border-color, box-shadow, opacity, transform;
}

/* ═══════════════════════════════════════════════════════════════════════════
   v3.4 · ATTENDANCE + ROUND ROBIN
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Attendance calendar ──────────────────────────────────────────────────── */
.att-toolbar { align-items: center; }
.att-monthnav { display: flex; align-items: center; gap: var(--space-2); }
.att-monthnav strong { min-width: 130px; text-align: center; font-size: var(--text-base); }
.att-legend {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin: 0 0 var(--space-3);
}
.att-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--text-xs); color: var(--text-secondary);
}
.att-chip::before {
  content: ''; width: 12px; height: 12px; border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--surface-secondary);
}
.att-chip.is-P::before { background: var(--success-bg); border-color: var(--success-border); }
.att-chip.is-A::before { background: var(--danger-bg);  border-color: var(--danger-border); }
.att-chip.is-L::before { background: var(--warning-bg); border-color: var(--warning-border); }

.att-scroll { max-height: min(62vh, 640px); overflow: auto; }
/* Deliberately NOT a .data-table: the auto card transform (v3.3) would fold a
   month grid into 31 stacked rows per person. A calendar stays a grid on every
   width; on a phone it pans inside .att-scroll with the name column pinned. */
.att-cal { border-collapse: collapse; width: max-content; min-width: 100%; font-size: var(--text-sm); }
.att-cal th, .att-cal td { border-bottom: 1px solid var(--border-subtle); }
.att-cal th.att-name { padding: var(--space-2) var(--space-3); }
.att-cal tbody tr:hover th.att-name { background: var(--surface-secondary); }
.att-cal th.att-name, .att-cal td:first-child { text-align: left; }
.att-cal th.att-name {
  position: sticky; left: 0; z-index: 2;
  background: var(--surface); min-width: 140px; font-weight: var(--weight-semibold);
}
.att-cal thead th { position: sticky; top: 0; z-index: 3; background: var(--surface); }
.att-cal thead th.att-name { z-index: 4; }
.att-cal .att-proj { display: block; font-size: var(--text-xs); color: var(--text-tertiary); font-weight: var(--weight-normal); }
.att-cal th.att-day { text-align: center; padding: var(--space-1) 2px; min-width: 34px; }
.att-cal th.att-day .att-dow { display: block; font-size: 10px; color: var(--text-tertiary); font-weight: var(--weight-normal); }
.att-cal th.att-day.is-sun .att-dow { color: var(--danger-text); }
.att-cal th.att-day.is-today { color: var(--primary); }
.att-cal td { padding: 2px; text-align: center; }
.att-cell {
  display: inline-grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--radius-md);
  border: 1px solid var(--border-subtle);
  background: var(--surface); color: var(--text-tertiary);
  font-size: var(--text-xs); font-weight: var(--weight-semibold); line-height: 1;
}
button.att-cell { cursor: pointer; }
button.att-cell:hover { border-color: var(--primary); }
.att-cell.is-P { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); }
.att-cell.is-A { background: var(--danger-bg);  border-color: var(--danger-border);  color: var(--danger-text); }
.att-cell.is-L { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); }
.att-cell.is-today { box-shadow: 0 0 0 2px var(--primary); }

/* ── Round Robin admin screen ─────────────────────────────────────────────── */
.rr-statusbar { margin: 0 0 var(--space-4); display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.rr-panel h3 { margin-top: 0; }
.rr-master { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-4); }
.rr-days { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.rr-day {
  display: inline-flex; align-items: center; gap: 6px;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius-full);
  font-size: var(--text-sm); cursor: pointer;
}
/* The portal's base input rules make text fields full-width pills; a checkbox
   inside these controls must stay a checkbox. */
.rr-day input, .rr-master input, .rr-pool-toggle input {
  width: 16px; height: 16px; padding: 0; margin: 0;
  accent-color: var(--primary); flex: 0 0 auto;
}
.att-toolbar .btn-primary, .rr-actions .btn-primary { width: auto; }
.rr-day:has(input:checked) { background: var(--primary-tint); border-color: var(--primary); }
.rr-actions { margin-top: var(--space-4); }
.rr-pools { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: var(--space-4); margin: var(--space-4) 0; }
.rr-pool-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.rr-pool-head h3 { margin: 0; }
.rr-pool-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm); cursor: pointer; }
.rr-members { margin: var(--space-3) 0; display: flex; flex-direction: column; gap: var(--space-1); }
.rr-member {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-subtle); border-radius: var(--radius-md);
  background: var(--surface-secondary);
}
.rr-member.inactive { opacity: .55; }
.rr-ord { font-size: var(--text-xs); color: var(--text-tertiary); min-width: 16px; }
.rr-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border); flex: 0 0 auto; }
.rr-dot.on { background: var(--success-solid); }
.rr-mname { flex: 1 1 auto; font-size: var(--text-sm); }
.rr-mbtns { display: flex; gap: 4px; }
.rrw-line { font-size: var(--text-sm); color: var(--text-secondary); margin-top: var(--space-1); }
.rrw-line b, .rrw-line strong { color: var(--text-primary); }

/* ── Dashboard widget ─────────────────────────────────────────────────────── */
.rr-widget { margin: 0 0 var(--space-5); }
.rrw-head { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.rrw-head h3 { margin: 0; }
.rrw-hours { font-size: var(--text-xs); color: var(--text-tertiary); margin-left: auto; }
.rrw-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: var(--space-4); margin-top: var(--space-3); }
.rrw-pool strong { font-size: var(--text-sm); }

@media (max-width: 640px) {
  .att-monthnav strong { min-width: 0; }
  .att-cal th.att-name { min-width: 110px; }
  .rrw-hours { margin-left: 0; width: 100%; }
}

/* ── v4.0 · App shell ────────────────────────────────────────────────────── */
#offlineBar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 400;
  padding: 8px 14px; text-align: center;
  font-size: .78rem; font-weight: 600; letter-spacing: .02em;
  background: #8a6d1f; color: #fff8e6;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
}
#offlineBar[hidden] { display: none; }
